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::{ColumnTrait, 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;
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    Column as LambdaColumn, Function as LambdaFunction,
15};
16pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
17pub use crate::ui::cfn::{
18    DetailTab as CfnDetailTab, State as CfnState, StatusFilter as CfnStatusFilter,
19};
20pub use crate::ui::cw::alarms::{AlarmTab, AlarmViewMode};
21pub use crate::ui::ecr::{State as EcrState, Tab as EcrTab};
22use crate::ui::iam::{GroupTab, RoleTab, State as IamState, UserTab};
23pub use crate::ui::lambda::{
24    ApplicationState as LambdaApplicationState, DetailTab as LambdaDetailTab, State as LambdaState,
25};
26pub use crate::ui::s3::{BucketType as S3BucketType, ObjectTab as S3ObjectTab, State as S3State};
27pub use crate::ui::{
28    CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, EventFilterFocus,
29    LogGroupColumn, Preferences, StreamColumn, StreamSort, TimeUnit,
30};
31use rusticity_core::{
32    AlarmsClient, AwsConfig, CloudFormationClient, CloudWatchClient, EcrClient, IamClient,
33    LambdaClient, LogEvent, LogGroup, LogStream, S3Client,
34};
35
36#[derive(Clone)]
37pub struct Tab {
38    pub service: Service,
39    pub title: String,
40    pub breadcrumb: String,
41}
42
43pub struct App {
44    pub running: bool,
45    pub mode: Mode,
46    pub config: AwsConfig,
47    pub cloudwatch_client: CloudWatchClient,
48    pub s3_client: S3Client,
49    pub alarms_client: AlarmsClient,
50    pub ecr_client: EcrClient,
51    pub iam_client: IamClient,
52    pub lambda_client: LambdaClient,
53    pub cloudformation_client: CloudFormationClient,
54    pub current_service: Service,
55    pub tabs: Vec<Tab>,
56    pub current_tab: usize,
57    pub tab_picker_selected: usize,
58    pub tab_filter: String,
59    pub pending_key: Option<char>,
60    pub log_groups_state: CloudWatchLogGroupsState,
61    pub insights_state: CloudWatchInsightsState,
62    pub alarms_state: CloudWatchAlarmsState,
63    pub s3_state: S3State,
64    pub ecr_state: EcrState,
65    pub lambda_state: LambdaState,
66    pub lambda_application_state: LambdaApplicationState,
67    pub cfn_state: CfnState,
68    pub iam_state: IamState,
69    pub service_picker: ServicePickerState,
70    pub service_selected: bool,
71    pub profile: String,
72    pub region: String,
73    pub region_selector_index: usize,
74    pub visible_columns: Vec<LogGroupColumn>,
75    pub all_columns: Vec<LogGroupColumn>,
76    pub column_selector_index: usize,
77    pub preference_section: Preferences,
78    pub visible_stream_columns: Vec<StreamColumn>,
79    pub all_stream_columns: Vec<StreamColumn>,
80    pub visible_event_columns: Vec<EventColumn>,
81    pub all_event_columns: Vec<EventColumn>,
82    pub visible_alarm_columns: Vec<AlarmColumn>,
83    pub all_alarm_columns: Vec<AlarmColumn>,
84    pub visible_bucket_columns: Vec<S3BucketColumn>,
85    pub all_bucket_columns: Vec<S3BucketColumn>,
86    pub visible_ecr_columns: Vec<EcrColumn>,
87    pub all_ecr_columns: Vec<EcrColumn>,
88    pub visible_ecr_image_columns: Vec<EcrImageColumn>,
89    pub all_ecr_image_columns: Vec<EcrImageColumn>,
90    pub visible_lambda_application_columns: Vec<LambdaApplicationColumn>,
91    pub all_lambda_application_columns: Vec<LambdaApplicationColumn>,
92    pub visible_deployment_columns: Vec<DeploymentColumn>,
93    pub all_deployment_columns: Vec<DeploymentColumn>,
94    pub visible_resource_columns: Vec<ResourceColumn>,
95    pub all_resource_columns: Vec<ResourceColumn>,
96    pub visible_cfn_columns: Vec<CfnColumn>,
97    pub all_cfn_columns: Vec<CfnColumn>,
98    pub visible_iam_columns: Vec<String>,
99    pub all_iam_columns: Vec<String>,
100    pub visible_role_columns: Vec<String>,
101    pub all_role_columns: Vec<String>,
102    pub visible_group_columns: Vec<String>,
103    pub all_group_columns: Vec<String>,
104    pub visible_policy_columns: Vec<String>,
105    pub all_policy_columns: Vec<String>,
106    pub view_mode: ViewMode,
107    pub error_message: Option<String>,
108    pub page_input: String,
109    pub calendar_date: Option<time::Date>,
110    pub calendar_selecting: CalendarField,
111    pub cursor_pos: usize,
112    pub current_session: Option<crate::session::Session>,
113    pub sessions: Vec<crate::session::Session>,
114    pub session_picker_selected: usize,
115    pub session_filter: String,
116    pub region_filter: String,
117    pub region_picker_selected: usize,
118    pub region_latencies: std::collections::HashMap<String, u64>,
119    pub profile_filter: String,
120    pub profile_picker_selected: usize,
121    pub available_profiles: Vec<AwsProfile>,
122    pub snapshot_requested: bool,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq)]
126pub enum CalendarField {
127    StartDate,
128    EndDate,
129}
130
131pub struct CloudWatchInsightsState {
132    pub insights: InsightsState,
133    pub loading: bool,
134}
135
136pub struct CloudWatchAlarmsState {
137    pub table: crate::table::TableState<Alarm>,
138    pub alarm_tab: AlarmTab,
139    pub view_as: AlarmViewMode,
140    pub wrap_lines: bool,
141    pub sort_column: String,
142    pub sort_direction: SortDirection,
143    pub input_focus: InputFocus,
144}
145
146impl ColumnTrait for S3ObjectColumn {
147    fn name(&self) -> &'static str {
148        self.name()
149    }
150}
151
152impl PageSize {
153    pub fn value(&self) -> usize {
154        match self {
155            PageSize::Ten => 10,
156            PageSize::TwentyFive => 25,
157            PageSize::Fifty => 50,
158            PageSize::OneHundred => 100,
159        }
160    }
161
162    pub fn next(&self) -> Self {
163        match self {
164            PageSize::Ten => PageSize::TwentyFive,
165            PageSize::TwentyFive => PageSize::Fifty,
166            PageSize::Fifty => PageSize::OneHundred,
167            PageSize::OneHundred => PageSize::Ten,
168        }
169    }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq)]
173pub enum S3ObjectColumn {
174    Name,
175    Type,
176    LastModified,
177    Size,
178    StorageClass,
179}
180
181impl S3ObjectColumn {
182    pub fn name(&self) -> &'static str {
183        match self {
184            S3ObjectColumn::Name => "Name",
185            S3ObjectColumn::Type => "Type",
186            S3ObjectColumn::LastModified => "Last modified",
187            S3ObjectColumn::Size => "Size",
188            S3ObjectColumn::StorageClass => "Storage class",
189        }
190    }
191
192    pub fn all() -> Vec<S3ObjectColumn> {
193        vec![
194            S3ObjectColumn::Name,
195            S3ObjectColumn::Type,
196            S3ObjectColumn::LastModified,
197            S3ObjectColumn::Size,
198            S3ObjectColumn::StorageClass,
199        ]
200    }
201}
202
203pub struct ServicePickerState {
204    pub filter: String,
205    pub selected: usize,
206    pub services: Vec<&'static str>,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq)]
210pub enum ViewMode {
211    List,
212    Detail,
213    Events,
214    InsightsResults,
215    PolicyView,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq)]
219pub enum Service {
220    CloudWatchLogGroups,
221    CloudWatchInsights,
222    CloudWatchAlarms,
223    S3Buckets,
224    EcrRepositories,
225    LambdaFunctions,
226    LambdaApplications,
227    CloudFormationStacks,
228    IamUsers,
229    IamRoles,
230    IamUserGroups,
231}
232
233impl Service {
234    pub fn name(&self) -> &str {
235        match self {
236            Service::CloudWatchLogGroups => "CloudWatch > Log Groups",
237            Service::CloudWatchInsights => "CloudWatch > Logs Insights",
238            Service::CloudWatchAlarms => "CloudWatch > Alarms",
239            Service::S3Buckets => "S3 > Buckets",
240            Service::EcrRepositories => "ECR > Repositories",
241            Service::LambdaFunctions => "Lambda > Functions",
242            Service::LambdaApplications => "Lambda > Applications",
243            Service::CloudFormationStacks => "CloudFormation > Stacks",
244            Service::IamUsers => "IAM > Users",
245            Service::IamRoles => "IAM > Roles",
246            Service::IamUserGroups => "IAM > User Groups",
247        }
248    }
249}
250
251fn copy_to_clipboard(text: &str) {
252    use std::io::Write;
253    use std::process::{Command, Stdio};
254    if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
255        if let Some(mut stdin) = child.stdin.take() {
256            let _ = stdin.write_all(text.as_bytes());
257        }
258        let _ = child.wait();
259    }
260}
261
262fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
263    if max > 0 {
264        *selected = (*selected + page_size).min(max - 1);
265    }
266}
267
268impl App {
269    fn get_active_filter_mut(&mut self) -> Option<&mut String> {
270        if self.current_service == Service::CloudWatchAlarms {
271            Some(&mut self.alarms_state.table.filter)
272        } else if self.current_service == Service::S3Buckets {
273            if self.s3_state.current_bucket.is_some() {
274                Some(&mut self.s3_state.object_filter)
275            } else {
276                Some(&mut self.s3_state.buckets.filter)
277            }
278        } else if self.current_service == Service::EcrRepositories {
279            if self.ecr_state.current_repository.is_some() {
280                Some(&mut self.ecr_state.images.filter)
281            } else {
282                Some(&mut self.ecr_state.repositories.filter)
283            }
284        } else if self.current_service == Service::LambdaFunctions {
285            if self.lambda_state.current_version.is_some()
286                && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
287            {
288                Some(&mut self.lambda_state.alias_table.filter)
289            } else if self.lambda_state.current_function.is_some()
290                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
291            {
292                Some(&mut self.lambda_state.version_table.filter)
293            } else if self.lambda_state.current_function.is_some()
294                && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
295            {
296                Some(&mut self.lambda_state.alias_table.filter)
297            } else {
298                Some(&mut self.lambda_state.table.filter)
299            }
300        } else if self.current_service == Service::LambdaApplications {
301            if self.lambda_application_state.current_application.is_some() {
302                if self.lambda_application_state.detail_tab
303                    == crate::ui::lambda::ApplicationDetailTab::Deployments
304                {
305                    Some(&mut self.lambda_application_state.deployments.filter)
306                } else {
307                    Some(&mut self.lambda_application_state.resources.filter)
308                }
309            } else {
310                Some(&mut self.lambda_application_state.table.filter)
311            }
312        } else if self.current_service == Service::CloudFormationStacks {
313            Some(&mut self.cfn_state.table.filter)
314        } else if self.current_service == Service::IamUsers {
315            if self.iam_state.current_user.is_some() {
316                if self.iam_state.user_tab == UserTab::Tags {
317                    Some(&mut self.iam_state.user_tags.filter)
318                } else {
319                    Some(&mut self.iam_state.policies.filter)
320                }
321            } else {
322                Some(&mut self.iam_state.users.filter)
323            }
324        } else if self.current_service == Service::IamRoles {
325            if self.iam_state.current_role.is_some() {
326                if self.iam_state.role_tab == RoleTab::Tags {
327                    Some(&mut self.iam_state.tags.filter)
328                } else if self.iam_state.role_tab == RoleTab::LastAccessed {
329                    Some(&mut self.iam_state.last_accessed_filter)
330                } else {
331                    Some(&mut self.iam_state.policies.filter)
332                }
333            } else {
334                Some(&mut self.iam_state.roles.filter)
335            }
336        } else if self.current_service == Service::IamUserGroups {
337            if self.iam_state.current_group.is_some() {
338                if self.iam_state.group_tab == GroupTab::Permissions {
339                    Some(&mut self.iam_state.policies.filter)
340                } else if self.iam_state.group_tab == GroupTab::Users {
341                    Some(&mut self.iam_state.group_users.filter)
342                } else {
343                    None
344                }
345            } else {
346                Some(&mut self.iam_state.groups.filter)
347            }
348        } else if self.view_mode == ViewMode::List {
349            Some(&mut self.log_groups_state.log_groups.filter)
350        } else if self.view_mode == ViewMode::Detail
351            && self.log_groups_state.detail_tab == DetailTab::LogStreams
352        {
353            Some(&mut self.log_groups_state.stream_filter)
354        } else {
355            None
356        }
357    }
358
359    pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
360        let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
361            .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
362
363        std::env::set_var("AWS_PROFILE", &profile_name);
364
365        let config = AwsConfig::new(region).await?;
366        let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
367        let s3_client = S3Client::new(config.clone());
368        let alarms_client = AlarmsClient::new(config.clone());
369        let ecr_client = EcrClient::new(config.clone());
370        let iam_client = IamClient::new(config.clone());
371        let lambda_client = LambdaClient::new(config.clone());
372        let cloudformation_client = CloudFormationClient::new(config.clone());
373        let region_name = config.region.clone();
374
375        Ok(Self {
376            running: true,
377            mode: Mode::ServicePicker,
378            config,
379            cloudwatch_client,
380            s3_client,
381            alarms_client,
382            ecr_client,
383            iam_client,
384            lambda_client,
385            cloudformation_client,
386            current_service: Service::CloudWatchLogGroups,
387            tabs: Vec::new(),
388            current_tab: 0,
389            tab_picker_selected: 0,
390            tab_filter: String::new(),
391            pending_key: None,
392            log_groups_state: CloudWatchLogGroupsState::new(),
393            insights_state: CloudWatchInsightsState::new(),
394            alarms_state: CloudWatchAlarmsState::new(),
395            s3_state: S3State::new(),
396            ecr_state: EcrState::new(),
397            lambda_state: LambdaState::new(),
398            lambda_application_state: LambdaApplicationState::new(),
399            cfn_state: CfnState::new(),
400            iam_state: IamState::new(),
401            service_picker: ServicePickerState::new(),
402            service_selected: false,
403            profile: profile_name,
404            region: region_name,
405            region_selector_index: 0,
406            visible_columns: LogGroupColumn::default_visible(),
407            all_columns: LogGroupColumn::all(),
408            column_selector_index: 0,
409            visible_stream_columns: StreamColumn::default_visible(),
410            all_stream_columns: StreamColumn::all(),
411            visible_event_columns: EventColumn::default_visible(),
412            all_event_columns: EventColumn::all(),
413            visible_alarm_columns: vec![
414                AlarmColumn::Name,
415                AlarmColumn::State,
416                AlarmColumn::LastStateUpdate,
417                AlarmColumn::Conditions,
418                AlarmColumn::Actions,
419            ],
420            all_alarm_columns: AlarmColumn::all(),
421            visible_bucket_columns: vec![
422                S3BucketColumn::Name,
423                S3BucketColumn::Region,
424                S3BucketColumn::CreationDate,
425            ],
426            all_bucket_columns: S3BucketColumn::all(),
427            visible_ecr_columns: EcrColumn::all(),
428            all_ecr_columns: EcrColumn::all(),
429            visible_ecr_image_columns: EcrImageColumn::all(),
430            all_ecr_image_columns: EcrImageColumn::all(),
431            visible_lambda_application_columns: vec![
432                LambdaApplicationColumn::Name,
433                LambdaApplicationColumn::Description,
434                LambdaApplicationColumn::Status,
435                LambdaApplicationColumn::LastModified,
436            ],
437            all_lambda_application_columns: LambdaApplicationColumn::all(),
438            visible_deployment_columns: DeploymentColumn::all(),
439            all_deployment_columns: DeploymentColumn::all(),
440            visible_resource_columns: ResourceColumn::all(),
441            all_resource_columns: ResourceColumn::all(),
442            visible_cfn_columns: vec![
443                CfnColumn::Name,
444                CfnColumn::Status,
445                CfnColumn::CreatedTime,
446                CfnColumn::Description,
447            ],
448            all_cfn_columns: CfnColumn::all(),
449            visible_iam_columns: vec![
450                "User name".to_string(),
451                "Path".to_string(),
452                "Groups".to_string(),
453                "Last activity".to_string(),
454                "MFA".to_string(),
455                "Password age".to_string(),
456                "Console last sign-in".to_string(),
457                "Access key ID".to_string(),
458                "Active key age".to_string(),
459                "Access key last used".to_string(),
460                "ARN".to_string(),
461            ],
462            all_iam_columns: vec![
463                "User name".to_string(),
464                "Path".to_string(),
465                "Groups".to_string(),
466                "Last activity".to_string(),
467                "MFA".to_string(),
468                "Password age".to_string(),
469                "Console last sign-in".to_string(),
470                "Access key ID".to_string(),
471                "Active key age".to_string(),
472                "Access key last used".to_string(),
473                "ARN".to_string(),
474                "Creation time".to_string(),
475                "Console access".to_string(),
476                "Signing certs".to_string(),
477            ],
478            visible_role_columns: vec![
479                "Role name".to_string(),
480                "Trusted entities".to_string(),
481                "Creation time".to_string(),
482            ],
483            all_role_columns: vec![
484                "Role name".to_string(),
485                "Path".to_string(),
486                "Trusted entities".to_string(),
487                "ARN".to_string(),
488                "Creation time".to_string(),
489                "Description".to_string(),
490                "Max session duration".to_string(),
491            ],
492            visible_group_columns: vec![
493                "Group name".to_string(),
494                "Users".to_string(),
495                "Permissions".to_string(),
496                "Creation time".to_string(),
497            ],
498            all_group_columns: vec![
499                "Group name".to_string(),
500                "Path".to_string(),
501                "Users".to_string(),
502                "Permissions".to_string(),
503                "Creation time".to_string(),
504            ],
505            visible_policy_columns: vec![
506                "Policy name".to_string(),
507                "Type".to_string(),
508                "Attached via".to_string(),
509            ],
510            all_policy_columns: vec![
511                "Policy name".to_string(),
512                "Type".to_string(),
513                "Attached via".to_string(),
514                "Attached entities".to_string(),
515                "Description".to_string(),
516                "Creation time".to_string(),
517                "Edited time".to_string(),
518            ],
519            preference_section: Preferences::Columns,
520            view_mode: ViewMode::List,
521            error_message: None,
522            page_input: String::new(),
523            calendar_date: None,
524            calendar_selecting: CalendarField::StartDate,
525            cursor_pos: 0,
526            current_session: None,
527            sessions: Vec::new(),
528            session_picker_selected: 0,
529            session_filter: String::new(),
530            region_filter: String::new(),
531            region_picker_selected: 0,
532            region_latencies: std::collections::HashMap::new(),
533            profile_filter: String::new(),
534            profile_picker_selected: 0,
535            available_profiles: Vec::new(),
536            snapshot_requested: false,
537        })
538    }
539
540    pub fn new_without_client(profile: String, region: Option<String>) -> Self {
541        let config = AwsConfig::dummy(region.clone());
542        Self {
543            running: true,
544            mode: Mode::ServicePicker,
545            config: config.clone(),
546            cloudwatch_client: CloudWatchClient::dummy(config.clone()),
547            s3_client: S3Client::new(config.clone()),
548            alarms_client: AlarmsClient::new(config.clone()),
549            ecr_client: EcrClient::new(config.clone()),
550            iam_client: IamClient::new(config.clone()),
551            lambda_client: LambdaClient::new(config.clone()),
552            cloudformation_client: CloudFormationClient::new(config.clone()),
553            current_service: Service::CloudWatchLogGroups,
554            tabs: Vec::new(),
555            current_tab: 0,
556            tab_picker_selected: 0,
557            tab_filter: String::new(),
558            pending_key: None,
559            log_groups_state: CloudWatchLogGroupsState::new(),
560            insights_state: CloudWatchInsightsState::new(),
561            alarms_state: CloudWatchAlarmsState::new(),
562            s3_state: S3State::new(),
563            ecr_state: EcrState::new(),
564            lambda_state: LambdaState::new(),
565            lambda_application_state: LambdaApplicationState::new(),
566            cfn_state: CfnState::new(),
567            iam_state: IamState::new(),
568            service_picker: ServicePickerState::new(),
569            service_selected: false,
570            profile,
571            region: region.unwrap_or_default(),
572            region_selector_index: 0,
573            visible_columns: LogGroupColumn::default_visible(),
574            all_columns: LogGroupColumn::all(),
575            column_selector_index: 0,
576            preference_section: Preferences::Columns,
577            visible_stream_columns: StreamColumn::default_visible(),
578            all_stream_columns: StreamColumn::all(),
579            visible_event_columns: EventColumn::default_visible(),
580            all_event_columns: EventColumn::all(),
581            visible_alarm_columns: vec![
582                AlarmColumn::Name,
583                AlarmColumn::State,
584                AlarmColumn::LastStateUpdate,
585                AlarmColumn::Conditions,
586                AlarmColumn::Actions,
587            ],
588            all_alarm_columns: AlarmColumn::all(),
589            visible_bucket_columns: vec![
590                S3BucketColumn::Name,
591                S3BucketColumn::Region,
592                S3BucketColumn::CreationDate,
593            ],
594            all_bucket_columns: S3BucketColumn::all(),
595            visible_ecr_columns: EcrColumn::all(),
596            all_ecr_columns: EcrColumn::all(),
597            visible_ecr_image_columns: EcrImageColumn::all(),
598            all_ecr_image_columns: EcrImageColumn::all(),
599            visible_lambda_application_columns: vec![
600                LambdaApplicationColumn::Name,
601                LambdaApplicationColumn::Description,
602                LambdaApplicationColumn::Status,
603                LambdaApplicationColumn::LastModified,
604            ],
605            all_lambda_application_columns: LambdaApplicationColumn::all(),
606            visible_deployment_columns: DeploymentColumn::all(),
607            all_deployment_columns: DeploymentColumn::all(),
608            visible_resource_columns: ResourceColumn::all(),
609            all_resource_columns: ResourceColumn::all(),
610            visible_cfn_columns: vec![
611                CfnColumn::Name,
612                CfnColumn::Status,
613                CfnColumn::CreatedTime,
614                CfnColumn::Description,
615            ],
616            all_cfn_columns: CfnColumn::all(),
617            visible_iam_columns: vec![
618                "User name".to_string(),
619                "Path".to_string(),
620                "Groups".to_string(),
621                "Last activity".to_string(),
622                "MFA".to_string(),
623                "Password age".to_string(),
624                "Console last sign-in".to_string(),
625                "Access key ID".to_string(),
626                "Active key age".to_string(),
627                "Access key last used".to_string(),
628                "ARN".to_string(),
629            ],
630            all_iam_columns: vec![
631                "User name".to_string(),
632                "Path".to_string(),
633                "Groups".to_string(),
634                "Last activity".to_string(),
635                "MFA".to_string(),
636                "Password age".to_string(),
637                "Console last sign-in".to_string(),
638                "Access key ID".to_string(),
639                "Active key age".to_string(),
640                "Access key last used".to_string(),
641                "ARN".to_string(),
642                "Creation time".to_string(),
643                "Console access".to_string(),
644                "Signing certs".to_string(),
645            ],
646            visible_role_columns: vec![
647                "Role name".to_string(),
648                "Trusted entities".to_string(),
649                "Creation time".to_string(),
650            ],
651            all_role_columns: vec![
652                "Role name".to_string(),
653                "Path".to_string(),
654                "Trusted entities".to_string(),
655                "ARN".to_string(),
656                "Creation time".to_string(),
657                "Description".to_string(),
658                "Max session duration".to_string(),
659            ],
660            visible_group_columns: vec![
661                "Group name".to_string(),
662                "Users".to_string(),
663                "Permissions".to_string(),
664                "Creation time".to_string(),
665            ],
666            all_group_columns: vec![
667                "Group name".to_string(),
668                "Path".to_string(),
669                "Users".to_string(),
670                "Permissions".to_string(),
671                "Creation time".to_string(),
672            ],
673            visible_policy_columns: vec![
674                "Policy name".to_string(),
675                "Type".to_string(),
676                "Attached via".to_string(),
677            ],
678            all_policy_columns: vec![
679                "Policy name".to_string(),
680                "Type".to_string(),
681                "Attached via".to_string(),
682                "Attached entities".to_string(),
683                "Description".to_string(),
684                "Creation time".to_string(),
685                "Edited time".to_string(),
686            ],
687            view_mode: ViewMode::List,
688            error_message: None,
689            page_input: String::new(),
690            calendar_date: None,
691            calendar_selecting: CalendarField::StartDate,
692            cursor_pos: 0,
693            current_session: None,
694            sessions: Vec::new(),
695            session_picker_selected: 0,
696            session_filter: String::new(),
697            region_filter: String::new(),
698            region_picker_selected: 0,
699            region_latencies: std::collections::HashMap::new(),
700            profile_filter: String::new(),
701            profile_picker_selected: 0,
702            available_profiles: Vec::new(),
703            snapshot_requested: false,
704        }
705    }
706
707    pub fn handle_action(&mut self, action: Action) {
708        match action {
709            Action::Quit => {
710                self.save_current_session();
711                self.running = false;
712            }
713            Action::CloseService => {
714                if !self.tabs.is_empty() {
715                    // Close the current tab
716                    self.tabs.remove(self.current_tab);
717
718                    if self.tabs.is_empty() {
719                        // Last tab closed - show service picker
720                        self.service_selected = false;
721                        self.current_tab = 0;
722                        self.mode = Mode::ServicePicker;
723                    } else {
724                        // Tabs remain - switch to adjacent tab
725                        if self.current_tab >= self.tabs.len() {
726                            self.current_tab = self.tabs.len() - 1;
727                        }
728                        self.current_service = self.tabs[self.current_tab].service;
729                        self.service_selected = true;
730                        self.mode = Mode::Normal;
731                    }
732                } else {
733                    // No tabs - just close service picker if open
734                    self.service_selected = false;
735                    self.mode = Mode::Normal;
736                }
737                self.service_picker.filter.clear();
738                self.service_picker.selected = 0;
739            }
740            Action::NextItem => self.next_item(),
741            Action::PrevItem => self.prev_item(),
742            Action::PageUp => self.page_up(),
743            Action::PageDown => self.page_down(),
744            Action::NextPane => self.next_pane(),
745            Action::PrevPane => self.prev_pane(),
746            Action::Select => self.select_item(),
747            Action::OpenSpaceMenu => {
748                self.mode = Mode::SpaceMenu;
749                self.service_picker.filter.clear();
750                self.service_picker.selected = 0;
751            }
752            Action::CloseMenu => {
753                self.mode = Mode::Normal;
754                self.service_picker.filter.clear();
755            }
756            Action::NextTab => {
757                if !self.tabs.is_empty() {
758                    self.current_tab = (self.current_tab + 1) % self.tabs.len();
759                    self.current_service = self.tabs[self.current_tab].service;
760                }
761            }
762            Action::PrevTab => {
763                if !self.tabs.is_empty() {
764                    self.current_tab = if self.current_tab == 0 {
765                        self.tabs.len() - 1
766                    } else {
767                        self.current_tab - 1
768                    };
769                    self.current_service = self.tabs[self.current_tab].service;
770                }
771            }
772            Action::CloseTab => {
773                if !self.tabs.is_empty() {
774                    self.tabs.remove(self.current_tab);
775                    if self.tabs.is_empty() {
776                        // Last tab closed - show service picker
777                        self.service_selected = false;
778                        self.current_tab = 0;
779                        self.service_picker.filter.clear();
780                        self.service_picker.selected = 0;
781                        self.mode = Mode::ServicePicker;
782                    } else {
783                        // If we closed the last tab, move to the left
784                        // Otherwise stay at same index (which is now the next tab to the right)
785                        if self.current_tab >= self.tabs.len() {
786                            self.current_tab = self.tabs.len() - 1;
787                        }
788                        self.current_service = self.tabs[self.current_tab].service;
789                        self.service_selected = true;
790                        self.mode = Mode::Normal;
791                    }
792                }
793            }
794            Action::OpenTabPicker => {
795                if !self.tabs.is_empty() {
796                    self.tab_picker_selected = self.current_tab;
797                    self.mode = Mode::TabPicker;
798                } else {
799                    self.mode = Mode::Normal;
800                }
801            }
802            Action::OpenSessionPicker => {
803                self.save_current_session();
804                self.sessions = crate::session::Session::list_all().unwrap_or_default();
805                self.session_picker_selected = 0;
806                self.mode = Mode::SessionPicker;
807            }
808            Action::LoadSession => {
809                let filtered_sessions = self.get_filtered_sessions();
810                if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
811                    let session = session.clone();
812                    // Load the session
813                    self.profile = session.profile.clone();
814                    self.region = session.region.clone();
815                    self.config.account_id = session.account_id.clone();
816                    self.config.role_arn = session.role_arn.clone();
817
818                    // Clear existing tabs and load session tabs
819                    self.tabs.clear();
820                    for session_tab in &session.tabs {
821                        // Parse service from string
822                        let service = match session_tab.service.as_str() {
823                            "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
824                            "CloudWatchInsights" => Service::CloudWatchInsights,
825                            "CloudWatchAlarms" => Service::CloudWatchAlarms,
826                            "S3Buckets" => Service::S3Buckets,
827                            "EcrRepositories" => Service::EcrRepositories,
828                            "LambdaFunctions" => Service::LambdaFunctions,
829                            "LambdaApplications" => Service::LambdaApplications,
830                            "CloudFormationStacks" => Service::CloudFormationStacks,
831                            "IamUsers" => Service::IamUsers,
832                            "IamRoles" => Service::IamRoles,
833                            "IamUserGroups" => Service::IamUserGroups,
834                            _ => continue,
835                        };
836
837                        self.tabs.push(Tab {
838                            service,
839                            title: session_tab.title.clone(),
840                            breadcrumb: session_tab.breadcrumb.clone(),
841                        });
842
843                        // Restore filter if present
844                        if let Some(filter) = &session_tab.filter {
845                            if service == Service::CloudWatchLogGroups {
846                                self.log_groups_state.log_groups.filter = filter.clone();
847                            }
848                        }
849                    }
850
851                    if !self.tabs.is_empty() {
852                        self.current_tab = 0;
853                        self.current_service = self.tabs[0].service;
854                        self.service_selected = true;
855                        self.current_session = Some(session.clone());
856                    }
857                }
858                self.mode = Mode::Normal;
859            }
860            Action::SaveSession => {
861                // TODO: Implement session saving
862            }
863            Action::OpenServicePicker => {
864                if self.mode == Mode::ServicePicker {
865                    self.tabs.push(Tab {
866                        service: Service::S3Buckets,
867                        title: "S3 > Buckets".to_string(),
868                        breadcrumb: "S3 > Buckets".to_string(),
869                    });
870                    self.current_tab = self.tabs.len() - 1;
871                    self.current_service = Service::S3Buckets;
872                    self.view_mode = ViewMode::List;
873                    self.service_selected = true;
874                    self.mode = Mode::Normal;
875                } else {
876                    self.mode = Mode::ServicePicker;
877                    self.service_picker.filter.clear();
878                    self.service_picker.selected = 0;
879                }
880            }
881            Action::OpenCloudWatch => {
882                self.current_service = Service::CloudWatchLogGroups;
883                self.view_mode = ViewMode::List;
884                self.service_selected = true;
885                self.mode = Mode::Normal;
886            }
887            Action::OpenCloudWatchSplit => {
888                self.current_service = Service::CloudWatchInsights;
889                self.view_mode = ViewMode::InsightsResults;
890                self.service_selected = true;
891                self.mode = Mode::Normal;
892            }
893            Action::OpenCloudWatchAlarms => {
894                self.current_service = Service::CloudWatchAlarms;
895                self.view_mode = ViewMode::List;
896                self.service_selected = true;
897                self.mode = Mode::Normal;
898            }
899            Action::FilterInput(c) => {
900                if self.mode == Mode::TabPicker {
901                    self.tab_filter.push(c);
902                    self.tab_picker_selected = 0;
903                } else if self.mode == Mode::RegionPicker {
904                    self.region_filter.push(c);
905                    self.region_picker_selected = 0;
906                } else if self.mode == Mode::ProfilePicker {
907                    self.profile_filter.push(c);
908                    self.profile_picker_selected = 0;
909                } else if self.mode == Mode::SessionPicker {
910                    self.session_filter.push(c);
911                    self.session_picker_selected = 0;
912                } else if self.mode == Mode::ServicePicker {
913                    self.service_picker.filter.push(c);
914                    self.service_picker.selected = 0;
915                } else if self.mode == Mode::InsightsInput {
916                    use crate::app::InsightsFocus;
917                    match self.insights_state.insights.insights_focus {
918                        InsightsFocus::Query => {
919                            self.insights_state.insights.query_text.push(c);
920                        }
921                        InsightsFocus::LogGroupSearch => {
922                            self.insights_state.insights.log_group_search.push(c);
923                            // Update matches
924                            if !self.insights_state.insights.log_group_search.is_empty() {
925                                self.insights_state.insights.log_group_matches = self
926                                    .log_groups_state
927                                    .log_groups
928                                    .items
929                                    .iter()
930                                    .filter(|g| {
931                                        g.name.to_lowercase().contains(
932                                            &self
933                                                .insights_state
934                                                .insights
935                                                .log_group_search
936                                                .to_lowercase(),
937                                        )
938                                    })
939                                    .take(50)
940                                    .map(|g| g.name.clone())
941                                    .collect();
942                                self.insights_state.insights.show_dropdown = true;
943                            } else {
944                                self.insights_state.insights.log_group_matches.clear();
945                                self.insights_state.insights.show_dropdown = false;
946                            }
947                        }
948                        _ => {}
949                    }
950                } else if self.mode == Mode::FilterInput {
951                    // Check if we should capture digits for page navigation
952                    let is_pagination_focused =
953                        if self.current_service == Service::LambdaApplications {
954                            if self.lambda_application_state.current_application.is_some() {
955                                if self.lambda_application_state.detail_tab
956                                    == crate::ui::lambda::ApplicationDetailTab::Deployments
957                                {
958                                    self.lambda_application_state.deployment_input_focus
959                                        == InputFocus::Pagination
960                                } else {
961                                    self.lambda_application_state.resource_input_focus
962                                        == InputFocus::Pagination
963                                }
964                            } else {
965                                self.lambda_application_state.input_focus == InputFocus::Pagination
966                            }
967                        } else if self.current_service == Service::CloudFormationStacks {
968                            self.cfn_state.input_focus == InputFocus::Pagination
969                        } else if self.current_service == Service::IamRoles
970                            && self.iam_state.current_role.is_none()
971                        {
972                            self.iam_state.role_input_focus == InputFocus::Pagination
973                        } else if self.view_mode == ViewMode::PolicyView {
974                            self.iam_state.policy_input_focus == InputFocus::Pagination
975                        } else if self.current_service == Service::CloudWatchAlarms {
976                            self.alarms_state.input_focus == InputFocus::Pagination
977                        } else if self.current_service == Service::CloudWatchLogGroups {
978                            self.log_groups_state.input_focus == InputFocus::Pagination
979                        } else if self.current_service == Service::EcrRepositories
980                            && self.ecr_state.current_repository.is_none()
981                        {
982                            self.ecr_state.input_focus == InputFocus::Pagination
983                        } else if self.current_service == Service::LambdaFunctions {
984                            if self.lambda_state.current_function.is_some()
985                                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
986                            {
987                                self.lambda_state.version_input_focus == InputFocus::Pagination
988                            } else if self.lambda_state.current_function.is_none() {
989                                self.lambda_state.input_focus == InputFocus::Pagination
990                            } else {
991                                false
992                            }
993                        } else {
994                            false
995                        };
996
997                    if is_pagination_focused && c.is_ascii_digit() {
998                        self.page_input.push(c);
999                    } else if self.current_service == Service::LambdaApplications {
1000                        let is_input_focused =
1001                            if self.lambda_application_state.current_application.is_some() {
1002                                if self.lambda_application_state.detail_tab
1003                                    == crate::ui::lambda::ApplicationDetailTab::Deployments
1004                                {
1005                                    self.lambda_application_state.deployment_input_focus
1006                                        == InputFocus::Filter
1007                                } else {
1008                                    self.lambda_application_state.resource_input_focus
1009                                        == InputFocus::Filter
1010                                }
1011                            } else {
1012                                self.lambda_application_state.input_focus == InputFocus::Filter
1013                            };
1014                        if is_input_focused {
1015                            if let Some(filter) = self.get_active_filter_mut() {
1016                                filter.push(c);
1017                            }
1018                        }
1019                    } else if self.current_service == Service::CloudFormationStacks {
1020                        if self.cfn_state.input_focus == InputFocus::Filter {
1021                            if let Some(filter) = self.get_active_filter_mut() {
1022                                filter.push(c);
1023                            }
1024                        }
1025                    } else if self.current_service == Service::EcrRepositories
1026                        && self.ecr_state.current_repository.is_none()
1027                    {
1028                        if self.ecr_state.input_focus == InputFocus::Filter {
1029                            if let Some(filter) = self.get_active_filter_mut() {
1030                                filter.push(c);
1031                            }
1032                        }
1033                    } else if self.current_service == Service::IamRoles
1034                        && self.iam_state.current_role.is_none()
1035                    {
1036                        if self.iam_state.role_input_focus == InputFocus::Filter {
1037                            if let Some(filter) = self.get_active_filter_mut() {
1038                                filter.push(c);
1039                            }
1040                        }
1041                    } else if self.view_mode == ViewMode::PolicyView {
1042                        if self.iam_state.policy_input_focus == InputFocus::Filter {
1043                            if let Some(filter) = self.get_active_filter_mut() {
1044                                filter.push(c);
1045                            }
1046                        }
1047                    } else if self.current_service == Service::LambdaFunctions
1048                        && self.lambda_state.current_version.is_some()
1049                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
1050                    {
1051                        if self.lambda_state.alias_input_focus == InputFocus::Filter {
1052                            if let Some(filter) = self.get_active_filter_mut() {
1053                                filter.push(c);
1054                            }
1055                        }
1056                    } else if self.current_service == Service::LambdaFunctions
1057                        && self.lambda_state.current_function.is_some()
1058                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1059                    {
1060                        if self.lambda_state.version_input_focus == InputFocus::Filter {
1061                            if let Some(filter) = self.get_active_filter_mut() {
1062                                filter.push(c);
1063                            }
1064                        }
1065                    } else if self.current_service == Service::LambdaFunctions
1066                        && self.lambda_state.current_function.is_some()
1067                        && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
1068                    {
1069                        if self.lambda_state.alias_input_focus == InputFocus::Filter {
1070                            if let Some(filter) = self.get_active_filter_mut() {
1071                                filter.push(c);
1072                            }
1073                        }
1074                    } else if let Some(filter) = self.get_active_filter_mut() {
1075                        filter.push(c);
1076                    }
1077                } else if self.mode == Mode::EventFilterInput {
1078                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1079                        self.log_groups_state.event_filter.push(c);
1080                    } else if c.is_ascii_digit() {
1081                        self.log_groups_state.relative_amount.push(c);
1082                    }
1083                } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1084                    self.page_input.push(c);
1085                }
1086            }
1087            Action::FilterBackspace => {
1088                if self.mode == Mode::TabPicker {
1089                    self.tab_filter.pop();
1090                    self.tab_picker_selected = 0;
1091                } else if self.mode == Mode::RegionPicker {
1092                    self.region_filter.pop();
1093                    self.region_picker_selected = 0;
1094                } else if self.mode == Mode::ProfilePicker {
1095                    self.profile_filter.pop();
1096                    self.profile_picker_selected = 0;
1097                } else if self.mode == Mode::SessionPicker {
1098                    self.session_filter.pop();
1099                    self.session_picker_selected = 0;
1100                } else if self.mode == Mode::InsightsInput {
1101                    use crate::app::InsightsFocus;
1102                    match self.insights_state.insights.insights_focus {
1103                        InsightsFocus::Query => {
1104                            self.insights_state.insights.query_text.pop();
1105                        }
1106                        InsightsFocus::LogGroupSearch => {
1107                            self.insights_state.insights.log_group_search.pop();
1108                            // Update matches
1109                            if !self.insights_state.insights.log_group_search.is_empty() {
1110                                self.insights_state.insights.log_group_matches = self
1111                                    .log_groups_state
1112                                    .log_groups
1113                                    .items
1114                                    .iter()
1115                                    .filter(|g| {
1116                                        g.name.to_lowercase().contains(
1117                                            &self
1118                                                .insights_state
1119                                                .insights
1120                                                .log_group_search
1121                                                .to_lowercase(),
1122                                        )
1123                                    })
1124                                    .take(50)
1125                                    .map(|g| g.name.clone())
1126                                    .collect();
1127                                self.insights_state.insights.show_dropdown = true;
1128                            } else {
1129                                self.insights_state.insights.log_group_matches.clear();
1130                                self.insights_state.insights.show_dropdown = false;
1131                            }
1132                        }
1133                        _ => {}
1134                    }
1135                } else if self.mode == Mode::FilterInput {
1136                    // Only allow backspace when focus is on the input field
1137                    if self.current_service == Service::CloudFormationStacks {
1138                        if self.cfn_state.input_focus == InputFocus::Filter {
1139                            if let Some(filter) = self.get_active_filter_mut() {
1140                                filter.pop();
1141                            }
1142                        }
1143                    } else if let Some(filter) = self.get_active_filter_mut() {
1144                        filter.pop();
1145                    }
1146                } else if self.mode == Mode::EventFilterInput {
1147                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1148                        self.log_groups_state.event_filter.pop();
1149                    } else {
1150                        self.log_groups_state.relative_amount.pop();
1151                    }
1152                }
1153            }
1154            Action::DeleteWord => {
1155                let text = if self.mode == Mode::ServicePicker {
1156                    &mut self.service_picker.filter
1157                } else if self.mode == Mode::InsightsInput {
1158                    use crate::app::InsightsFocus;
1159                    match self.insights_state.insights.insights_focus {
1160                        InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1161                        InsightsFocus::LogGroupSearch => {
1162                            &mut self.insights_state.insights.log_group_search
1163                        }
1164                        _ => return,
1165                    }
1166                } else if self.mode == Mode::FilterInput {
1167                    if let Some(filter) = self.get_active_filter_mut() {
1168                        filter
1169                    } else {
1170                        return;
1171                    }
1172                } else if self.mode == Mode::EventFilterInput {
1173                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1174                        &mut self.log_groups_state.event_filter
1175                    } else {
1176                        &mut self.log_groups_state.relative_amount
1177                    }
1178                } else {
1179                    return;
1180                };
1181
1182                if text.is_empty() {
1183                    return;
1184                }
1185
1186                let mut chars: Vec<char> = text.chars().collect();
1187                while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1188                    chars.pop();
1189                }
1190                while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1191                    chars.pop();
1192                }
1193                *text = chars.into_iter().collect();
1194            }
1195            Action::WordLeft => {
1196                // Not implemented - would need cursor position tracking
1197            }
1198            Action::WordRight => {
1199                // Not implemented - would need cursor position tracking
1200            }
1201            Action::OpenColumnSelector => {
1202                // If we have page input, apply it instead of opening column selector
1203                if !self.page_input.is_empty() {
1204                    if let Ok(page) = self.page_input.parse::<usize>() {
1205                        self.go_to_page(page);
1206                    }
1207                    self.page_input.clear();
1208                } else {
1209                    self.mode = Mode::ColumnSelector;
1210                    self.column_selector_index = 0;
1211                }
1212            }
1213            Action::ToggleColumn => {
1214                if self.current_service == Service::S3Buckets
1215                    && self.s3_state.current_bucket.is_none()
1216                {
1217                    if let Some(&col) = self.all_bucket_columns.get(self.column_selector_index) {
1218                        if let Some(pos) =
1219                            self.visible_bucket_columns.iter().position(|&c| c == col)
1220                        {
1221                            self.visible_bucket_columns.remove(pos);
1222                        } else {
1223                            self.visible_bucket_columns.push(col);
1224                        }
1225                    }
1226                } else if self.current_service == Service::CloudWatchAlarms {
1227                    // Map flat list index to actual item
1228                    // 0: Columns header, 1-16: columns, 17: empty, 18: ViewAs header, 19-20: view options
1229                    // 21: empty, 22: PageSize header, 23-25: page sizes, 26: empty, 27: WrapLines header, 28: wrap option
1230                    let idx = self.column_selector_index;
1231                    if (1..=16).contains(&idx) {
1232                        // Column toggle
1233                        if let Some(&col) = self.all_alarm_columns.get(idx - 1) {
1234                            if let Some(pos) =
1235                                self.visible_alarm_columns.iter().position(|&c| c == col)
1236                            {
1237                                self.visible_alarm_columns.remove(pos);
1238                            } else {
1239                                self.visible_alarm_columns.push(col);
1240                            }
1241                        }
1242                    } else if idx == 19 {
1243                        self.alarms_state.view_as = AlarmViewMode::Table;
1244                    } else if idx == 20 {
1245                        self.alarms_state.view_as = AlarmViewMode::Cards;
1246                    } else if idx == 23 {
1247                        self.alarms_state.table.page_size = PageSize::Ten;
1248                    } else if idx == 24 {
1249                        self.alarms_state.table.page_size = PageSize::TwentyFive;
1250                    } else if idx == 25 {
1251                        self.alarms_state.table.page_size = PageSize::Fifty;
1252                    } else if idx == 26 {
1253                        self.alarms_state.table.page_size = PageSize::OneHundred;
1254                    } else if idx == 29 {
1255                        self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1256                    }
1257                } else if self.current_service == Service::EcrRepositories {
1258                    if self.ecr_state.current_repository.is_some() {
1259                        // Images view - columns + page size
1260                        let idx = self.column_selector_index;
1261                        if let Some(&col) = self.all_ecr_image_columns.get(idx) {
1262                            if let Some(pos) = self
1263                                .visible_ecr_image_columns
1264                                .iter()
1265                                .position(|&c| c == col)
1266                            {
1267                                self.visible_ecr_image_columns.remove(pos);
1268                            } else {
1269                                self.visible_ecr_image_columns.push(col);
1270                            }
1271                        }
1272                    } else {
1273                        // Repositories view - just columns
1274                        if let Some(&col) = self.all_ecr_columns.get(self.column_selector_index) {
1275                            if let Some(pos) =
1276                                self.visible_ecr_columns.iter().position(|&c| c == col)
1277                            {
1278                                self.visible_ecr_columns.remove(pos);
1279                            } else {
1280                                self.visible_ecr_columns.push(col);
1281                            }
1282                        }
1283                    }
1284                } else if self.current_service == Service::LambdaFunctions {
1285                    let idx = self.column_selector_index;
1286                    // Check if we're in Versions tab
1287                    if self.lambda_state.current_function.is_some()
1288                        && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Versions
1289                    {
1290                        // Version columns
1291                        if idx > 0 && idx <= self.lambda_state.all_version_columns.len() {
1292                            if let Some(&col) = self.lambda_state.all_version_columns.get(idx - 1) {
1293                                if let Some(pos) = self
1294                                    .lambda_state
1295                                    .visible_version_columns
1296                                    .iter()
1297                                    .position(|&c| c == col)
1298                                {
1299                                    self.lambda_state.visible_version_columns.remove(pos);
1300                                } else {
1301                                    self.lambda_state.visible_version_columns.push(col);
1302                                }
1303                            }
1304                        } else if idx == self.lambda_state.all_version_columns.len() + 3 {
1305                            self.lambda_state.version_table.page_size = PageSize::Ten;
1306                        } else if idx == self.lambda_state.all_version_columns.len() + 4 {
1307                            self.lambda_state.version_table.page_size = PageSize::TwentyFive;
1308                        } else if idx == self.lambda_state.all_version_columns.len() + 5 {
1309                            self.lambda_state.version_table.page_size = PageSize::Fifty;
1310                        } else if idx == self.lambda_state.all_version_columns.len() + 6 {
1311                            self.lambda_state.version_table.page_size = PageSize::OneHundred;
1312                        }
1313                    } else if (self.lambda_state.current_function.is_some()
1314                        && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Aliases)
1315                        || (self.lambda_state.current_version.is_some()
1316                            && self.lambda_state.detail_tab
1317                                == crate::app::LambdaDetailTab::Configuration)
1318                    {
1319                        // Alias columns
1320                        if idx > 0 && idx <= self.lambda_state.all_alias_columns.len() {
1321                            if let Some(&col) = self.lambda_state.all_alias_columns.get(idx - 1) {
1322                                if let Some(pos) = self
1323                                    .lambda_state
1324                                    .visible_alias_columns
1325                                    .iter()
1326                                    .position(|&c| c == col)
1327                                {
1328                                    self.lambda_state.visible_alias_columns.remove(pos);
1329                                } else {
1330                                    self.lambda_state.visible_alias_columns.push(col);
1331                                }
1332                            }
1333                        } else if idx == self.lambda_state.all_alias_columns.len() + 3 {
1334                            self.lambda_state.alias_table.page_size = PageSize::Ten;
1335                        } else if idx == self.lambda_state.all_alias_columns.len() + 4 {
1336                            self.lambda_state.alias_table.page_size = PageSize::TwentyFive;
1337                        } else if idx == self.lambda_state.all_alias_columns.len() + 5 {
1338                            self.lambda_state.alias_table.page_size = PageSize::Fifty;
1339                        } else if idx == self.lambda_state.all_alias_columns.len() + 6 {
1340                            self.lambda_state.alias_table.page_size = PageSize::OneHundred;
1341                        }
1342                    } else {
1343                        // Function columns
1344                        if idx > 0 && idx <= self.lambda_state.all_columns.len() {
1345                            if let Some(&col) = self.lambda_state.all_columns.get(idx - 1) {
1346                                if let Some(pos) = self
1347                                    .lambda_state
1348                                    .visible_columns
1349                                    .iter()
1350                                    .position(|&c| c == col)
1351                                {
1352                                    self.lambda_state.visible_columns.remove(pos);
1353                                } else {
1354                                    self.lambda_state.visible_columns.push(col);
1355                                }
1356                            }
1357                        } else if idx == self.lambda_state.all_columns.len() + 3 {
1358                            self.lambda_state.table.page_size = PageSize::Ten;
1359                        } else if idx == self.lambda_state.all_columns.len() + 4 {
1360                            self.lambda_state.table.page_size = PageSize::TwentyFive;
1361                        } else if idx == self.lambda_state.all_columns.len() + 5 {
1362                            self.lambda_state.table.page_size = PageSize::Fifty;
1363                        } else if idx == self.lambda_state.all_columns.len() + 6 {
1364                            self.lambda_state.table.page_size = PageSize::OneHundred;
1365                        }
1366                    }
1367                } else if self.current_service == Service::LambdaApplications {
1368                    if self.lambda_application_state.current_application.is_some() {
1369                        // In detail view - handle resource or deployment columns
1370                        if self.lambda_application_state.detail_tab
1371                            == crate::ui::lambda::ApplicationDetailTab::Overview
1372                        {
1373                            // Resources columns
1374                            let idx = self.column_selector_index;
1375                            if idx > 0 && idx <= self.all_resource_columns.len() {
1376                                if let Some(&col) = self.all_resource_columns.get(idx - 1) {
1377                                    if let Some(pos) =
1378                                        self.visible_resource_columns.iter().position(|&c| c == col)
1379                                    {
1380                                        self.visible_resource_columns.remove(pos);
1381                                    } else {
1382                                        self.visible_resource_columns.push(col);
1383                                    }
1384                                }
1385                            } else if idx == self.all_resource_columns.len() + 3 {
1386                                self.lambda_application_state.resources.page_size = PageSize::Ten;
1387                            } else if idx == self.all_resource_columns.len() + 4 {
1388                                self.lambda_application_state.resources.page_size =
1389                                    PageSize::TwentyFive;
1390                            } else if idx == self.all_resource_columns.len() + 5 {
1391                                self.lambda_application_state.resources.page_size = PageSize::Fifty;
1392                            }
1393                        } else {
1394                            // Deployments columns
1395                            let idx = self.column_selector_index;
1396                            if idx > 0 && idx <= self.all_deployment_columns.len() {
1397                                if let Some(&col) = self.all_deployment_columns.get(idx - 1) {
1398                                    if let Some(pos) = self
1399                                        .visible_deployment_columns
1400                                        .iter()
1401                                        .position(|&c| c == col)
1402                                    {
1403                                        self.visible_deployment_columns.remove(pos);
1404                                    } else {
1405                                        self.visible_deployment_columns.push(col);
1406                                    }
1407                                }
1408                            } else if idx == self.all_deployment_columns.len() + 3 {
1409                                self.lambda_application_state.deployments.page_size = PageSize::Ten;
1410                            } else if idx == self.all_deployment_columns.len() + 4 {
1411                                self.lambda_application_state.deployments.page_size =
1412                                    PageSize::TwentyFive;
1413                            } else if idx == self.all_deployment_columns.len() + 5 {
1414                                self.lambda_application_state.deployments.page_size =
1415                                    PageSize::Fifty;
1416                            }
1417                        }
1418                    } else {
1419                        // In list view - handle application columns
1420                        let idx = self.column_selector_index;
1421                        if idx > 0 && idx <= self.all_lambda_application_columns.len() {
1422                            if let Some(&col) = self.all_lambda_application_columns.get(idx - 1) {
1423                                if let Some(pos) = self
1424                                    .visible_lambda_application_columns
1425                                    .iter()
1426                                    .position(|&c| c == col)
1427                                {
1428                                    self.visible_lambda_application_columns.remove(pos);
1429                                } else {
1430                                    self.visible_lambda_application_columns.push(col);
1431                                }
1432                            }
1433                        } else if idx == self.all_lambda_application_columns.len() + 3 {
1434                            self.lambda_application_state.table.page_size = PageSize::Ten;
1435                        } else if idx == self.all_lambda_application_columns.len() + 4 {
1436                            self.lambda_application_state.table.page_size = PageSize::TwentyFive;
1437                        } else if idx == self.all_lambda_application_columns.len() + 5 {
1438                            self.lambda_application_state.table.page_size = PageSize::Fifty;
1439                        }
1440                    }
1441                } else if self.view_mode == ViewMode::Events {
1442                    if let Some(&col) = self.all_event_columns.get(self.column_selector_index) {
1443                        if let Some(pos) = self.visible_event_columns.iter().position(|&c| c == col)
1444                        {
1445                            self.visible_event_columns.remove(pos);
1446                        } else {
1447                            self.visible_event_columns.push(col);
1448                        }
1449                    }
1450                } else if self.view_mode == ViewMode::Detail {
1451                    if let Some(&col) = self.all_stream_columns.get(self.column_selector_index) {
1452                        if let Some(pos) =
1453                            self.visible_stream_columns.iter().position(|&c| c == col)
1454                        {
1455                            self.visible_stream_columns.remove(pos);
1456                        } else {
1457                            self.visible_stream_columns.push(col);
1458                        }
1459                    }
1460                } else if self.current_service == Service::CloudFormationStacks {
1461                    let idx = self.column_selector_index;
1462                    if idx > 0 && idx <= self.all_cfn_columns.len() {
1463                        if let Some(&col) = self.all_cfn_columns.get(idx - 1) {
1464                            if let Some(pos) =
1465                                self.visible_cfn_columns.iter().position(|&c| c == col)
1466                            {
1467                                self.visible_cfn_columns.remove(pos);
1468                            } else {
1469                                self.visible_cfn_columns.push(col);
1470                            }
1471                        }
1472                    } else if idx == self.all_cfn_columns.len() + 3 {
1473                        self.cfn_state.table.page_size = PageSize::Ten;
1474                    } else if idx == self.all_cfn_columns.len() + 4 {
1475                        self.cfn_state.table.page_size = PageSize::TwentyFive;
1476                    } else if idx == self.all_cfn_columns.len() + 5 {
1477                        self.cfn_state.table.page_size = PageSize::Fifty;
1478                    } else if idx == self.all_cfn_columns.len() + 6 {
1479                        self.cfn_state.table.page_size = PageSize::OneHundred;
1480                    }
1481                } else if self.current_service == Service::IamUsers {
1482                    let idx = self.column_selector_index;
1483                    if self.iam_state.current_user.is_some() {
1484                        // Policy columns
1485                        if idx > 0 && idx <= self.all_policy_columns.len() {
1486                            if let Some(col) = self.all_policy_columns.get(idx - 1) {
1487                                if let Some(pos) =
1488                                    self.visible_policy_columns.iter().position(|c| c == col)
1489                                {
1490                                    self.visible_policy_columns.remove(pos);
1491                                } else {
1492                                    self.visible_policy_columns.push(col.clone());
1493                                }
1494                            }
1495                        } else if idx == self.all_policy_columns.len() + 3 {
1496                            self.iam_state.policies.page_size = PageSize::Ten;
1497                        } else if idx == self.all_policy_columns.len() + 4 {
1498                            self.iam_state.policies.page_size = PageSize::TwentyFive;
1499                        } else if idx == self.all_policy_columns.len() + 5 {
1500                            self.iam_state.policies.page_size = PageSize::Fifty;
1501                        }
1502                    } else {
1503                        // User columns
1504                        if idx > 0 && idx <= self.all_iam_columns.len() {
1505                            if let Some(col) = self.all_iam_columns.get(idx - 1) {
1506                                if let Some(pos) =
1507                                    self.visible_iam_columns.iter().position(|c| c == col)
1508                                {
1509                                    self.visible_iam_columns.remove(pos);
1510                                } else {
1511                                    self.visible_iam_columns.push(col.clone());
1512                                }
1513                            }
1514                        } else if idx == self.all_iam_columns.len() + 3 {
1515                            self.iam_state.users.page_size = PageSize::Ten;
1516                        } else if idx == self.all_iam_columns.len() + 4 {
1517                            self.iam_state.users.page_size = PageSize::TwentyFive;
1518                        } else if idx == self.all_iam_columns.len() + 5 {
1519                            self.iam_state.users.page_size = PageSize::Fifty;
1520                        }
1521                    }
1522                } else if self.current_service == Service::IamRoles {
1523                    let idx = self.column_selector_index;
1524                    if self.iam_state.current_role.is_some() {
1525                        // Policy columns
1526                        if idx > 0 && idx <= self.all_policy_columns.len() {
1527                            if let Some(col) = self.all_policy_columns.get(idx - 1) {
1528                                if let Some(pos) =
1529                                    self.visible_policy_columns.iter().position(|c| c == col)
1530                                {
1531                                    self.visible_policy_columns.remove(pos);
1532                                } else {
1533                                    self.visible_policy_columns.push(col.clone());
1534                                }
1535                            }
1536                        } else if idx == self.all_policy_columns.len() + 3 {
1537                            self.iam_state.policies.page_size = PageSize::Ten;
1538                        } else if idx == self.all_policy_columns.len() + 4 {
1539                            self.iam_state.policies.page_size = PageSize::TwentyFive;
1540                        } else if idx == self.all_policy_columns.len() + 5 {
1541                            self.iam_state.policies.page_size = PageSize::Fifty;
1542                        }
1543                    } else {
1544                        // Role columns
1545                        if idx > 0 && idx <= self.all_role_columns.len() {
1546                            if let Some(col) = self.all_role_columns.get(idx - 1) {
1547                                if let Some(pos) =
1548                                    self.visible_role_columns.iter().position(|c| c == col)
1549                                {
1550                                    self.visible_role_columns.remove(pos);
1551                                } else {
1552                                    self.visible_role_columns.push(col.clone());
1553                                }
1554                            }
1555                        } else if idx == self.all_role_columns.len() + 3 {
1556                            self.iam_state.roles.page_size = PageSize::Ten;
1557                        } else if idx == self.all_role_columns.len() + 4 {
1558                            self.iam_state.roles.page_size = PageSize::TwentyFive;
1559                        } else if idx == self.all_role_columns.len() + 5 {
1560                            self.iam_state.roles.page_size = PageSize::Fifty;
1561                        }
1562                    }
1563                } else if self.current_service == Service::IamUserGroups {
1564                    let idx = self.column_selector_index;
1565                    if idx > 0 && idx <= self.all_group_columns.len() {
1566                        if let Some(col) = self.all_group_columns.get(idx - 1) {
1567                            if let Some(pos) =
1568                                self.visible_group_columns.iter().position(|c| c == col)
1569                            {
1570                                self.visible_group_columns.remove(pos);
1571                            } else {
1572                                self.visible_group_columns.push(col.clone());
1573                            }
1574                        }
1575                    } else if idx == self.all_group_columns.len() + 3 {
1576                        self.iam_state.groups.page_size = PageSize::Ten;
1577                    } else if idx == self.all_group_columns.len() + 4 {
1578                        self.iam_state.groups.page_size = PageSize::TwentyFive;
1579                    } else if idx == self.all_group_columns.len() + 5 {
1580                        self.iam_state.groups.page_size = PageSize::Fifty;
1581                    }
1582                } else if let Some(&col) = self.all_columns.get(self.column_selector_index) {
1583                    if let Some(pos) = self.visible_columns.iter().position(|&c| c == col) {
1584                        self.visible_columns.remove(pos);
1585                    } else {
1586                        self.visible_columns.push(col);
1587                    }
1588                }
1589            }
1590            Action::NextPreferences => {
1591                if self.current_service == Service::CloudWatchAlarms {
1592                    // Jump to next section: Columns(0), ViewAs(18), PageSize(22), WrapLines(28)
1593                    if self.column_selector_index < 18 {
1594                        self.column_selector_index = 18; // ViewAs header
1595                    } else if self.column_selector_index < 22 {
1596                        self.column_selector_index = 22; // PageSize header
1597                    } else if self.column_selector_index < 28 {
1598                        self.column_selector_index = 28; // WrapLines header
1599                    } else {
1600                        self.column_selector_index = 0; // Back to Columns header
1601                    }
1602                } else if self.current_service == Service::EcrRepositories
1603                    && self.ecr_state.current_repository.is_some()
1604                {
1605                    // Images view: Columns(0), PageSize(columns.len() + 2)
1606                    let page_size_idx = self.all_ecr_image_columns.len() + 2;
1607                    if self.column_selector_index < page_size_idx {
1608                        self.column_selector_index = page_size_idx;
1609                    } else {
1610                        self.column_selector_index = 0;
1611                    }
1612                } else if self.current_service == Service::LambdaFunctions {
1613                    // Lambda: Columns(0), PageSize(columns.len() + 2)
1614                    let page_size_idx = self.lambda_state.all_columns.len() + 2;
1615                    if self.column_selector_index < page_size_idx {
1616                        self.column_selector_index = page_size_idx;
1617                    } else {
1618                        self.column_selector_index = 0;
1619                    }
1620                } else if self.current_service == Service::LambdaApplications {
1621                    // Lambda Applications: Columns(0), PageSize(columns.len() + 2)
1622                    let page_size_idx = self.all_lambda_application_columns.len() + 2;
1623                    if self.column_selector_index < page_size_idx {
1624                        self.column_selector_index = page_size_idx;
1625                    } else {
1626                        self.column_selector_index = 0;
1627                    }
1628                } else if self.current_service == Service::CloudFormationStacks {
1629                    // CloudFormation: Columns(0), PageSize(columns.len() + 2)
1630                    let page_size_idx = self.all_cfn_columns.len() + 2;
1631                    if self.column_selector_index < page_size_idx {
1632                        self.column_selector_index = page_size_idx;
1633                    } else {
1634                        self.column_selector_index = 0;
1635                    }
1636                } else if self.current_service == Service::IamUsers {
1637                    if self.iam_state.current_user.is_some() {
1638                        // Only Permissions tab has column preferences
1639                        if self.iam_state.user_tab == UserTab::Permissions {
1640                            let page_size_idx = self.all_policy_columns.len() + 2;
1641                            if self.column_selector_index < page_size_idx {
1642                                self.column_selector_index = page_size_idx;
1643                            } else {
1644                                self.column_selector_index = 0;
1645                            }
1646                        }
1647                        // Other tabs (Groups, Tags, Security Credentials, Last Accessed) have no preferences
1648                    } else {
1649                        // User columns: Columns(0), PageSize(columns.len() + 2)
1650                        let page_size_idx = self.all_iam_columns.len() + 2;
1651                        if self.column_selector_index < page_size_idx {
1652                            self.column_selector_index = page_size_idx;
1653                        } else {
1654                            self.column_selector_index = 0;
1655                        }
1656                    }
1657                } else if self.current_service == Service::IamRoles {
1658                    if self.iam_state.current_role.is_some() {
1659                        // Policy columns: Columns(0), PageSize(columns.len() + 2)
1660                        let page_size_idx = self.all_policy_columns.len() + 2;
1661                        if self.column_selector_index < page_size_idx {
1662                            self.column_selector_index = page_size_idx;
1663                        } else {
1664                            self.column_selector_index = 0;
1665                        }
1666                    } else {
1667                        // Role columns: Columns(0), PageSize(columns.len() + 2)
1668                        let page_size_idx = self.all_role_columns.len() + 2;
1669                        if self.column_selector_index < page_size_idx {
1670                            self.column_selector_index = page_size_idx;
1671                        } else {
1672                            self.column_selector_index = 0;
1673                        }
1674                    }
1675                } else if self.current_service == Service::IamUserGroups {
1676                    // Group columns: Columns(0), PageSize(columns.len() + 2)
1677                    let page_size_idx = self.all_group_columns.len() + 2;
1678                    if self.column_selector_index < page_size_idx {
1679                        self.column_selector_index = page_size_idx;
1680                    } else {
1681                        self.column_selector_index = 0;
1682                    }
1683                } else if let Some(&col) = self.all_columns.get(self.column_selector_index) {
1684                    if let Some(pos) = self.visible_columns.iter().position(|&c| c == col) {
1685                        self.visible_columns.remove(pos);
1686                    } else {
1687                        self.visible_columns.push(col);
1688                    }
1689                }
1690            }
1691            Action::CloseColumnSelector => {
1692                self.mode = Mode::Normal;
1693                self.preference_section = Preferences::Columns;
1694            }
1695            Action::NextDetailTab => {
1696                if self.current_service == Service::LambdaApplications
1697                    && self.lambda_application_state.current_application.is_some()
1698                {
1699                    self.lambda_application_state.detail_tab =
1700                        self.lambda_application_state.detail_tab.next();
1701                } else if self.current_service == Service::IamRoles
1702                    && self.iam_state.current_role.is_some()
1703                {
1704                    self.iam_state.role_tab = self.iam_state.role_tab.next();
1705                    if self.iam_state.role_tab == RoleTab::Tags {
1706                        self.iam_state.tags.loading = true;
1707                    }
1708                } else if self.current_service == Service::IamUsers
1709                    && self.iam_state.current_user.is_some()
1710                {
1711                    self.iam_state.user_tab = self.iam_state.user_tab.next();
1712                    if self.iam_state.user_tab == UserTab::Tags {
1713                        self.iam_state.user_tags.loading = true;
1714                    }
1715                } else if self.current_service == Service::IamUserGroups
1716                    && self.iam_state.current_group.is_some()
1717                {
1718                    self.iam_state.group_tab = self.iam_state.group_tab.next();
1719                } else if self.view_mode == ViewMode::Detail {
1720                    self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
1721                } else if self.current_service == Service::S3Buckets {
1722                    if self.s3_state.current_bucket.is_some() {
1723                        self.s3_state.object_tab = self.s3_state.object_tab.next();
1724                    } else {
1725                        self.s3_state.bucket_type = match self.s3_state.bucket_type {
1726                            S3BucketType::GeneralPurpose => S3BucketType::Directory,
1727                            S3BucketType::Directory => S3BucketType::GeneralPurpose,
1728                        };
1729                        self.s3_state.buckets.reset();
1730                    }
1731                } else if self.current_service == Service::CloudWatchAlarms {
1732                    self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
1733                        AlarmTab::AllAlarms => AlarmTab::InAlarm,
1734                        AlarmTab::InAlarm => AlarmTab::AllAlarms,
1735                    };
1736                    self.alarms_state.table.reset();
1737                } else if self.current_service == Service::EcrRepositories
1738                    && self.ecr_state.current_repository.is_none()
1739                {
1740                    self.ecr_state.tab = self.ecr_state.tab.next();
1741                    self.ecr_state.repositories.reset();
1742                    self.ecr_state.repositories.loading = true;
1743                } else if self.current_service == Service::LambdaFunctions
1744                    && self.lambda_state.current_function.is_some()
1745                {
1746                    if self.lambda_state.current_version.is_some() {
1747                        // Version view: only Code and Configuration tabs
1748                        self.lambda_state.detail_tab = match self.lambda_state.detail_tab {
1749                            LambdaDetailTab::Code => LambdaDetailTab::Configuration,
1750                            _ => LambdaDetailTab::Code,
1751                        };
1752                    } else {
1753                        self.lambda_state.detail_tab = self.lambda_state.detail_tab.next();
1754                    }
1755                } else if self.current_service == Service::CloudFormationStacks
1756                    && self.cfn_state.current_stack.is_some()
1757                {
1758                    self.cfn_state.detail_tab = self.cfn_state.detail_tab.next();
1759                }
1760            }
1761            Action::PrevDetailTab => {
1762                if self.current_service == Service::LambdaApplications
1763                    && self.lambda_application_state.current_application.is_some()
1764                {
1765                    self.lambda_application_state.detail_tab =
1766                        self.lambda_application_state.detail_tab.prev();
1767                } else if self.current_service == Service::IamRoles
1768                    && self.iam_state.current_role.is_some()
1769                {
1770                    self.iam_state.role_tab = self.iam_state.role_tab.prev();
1771                } else if self.current_service == Service::IamUsers
1772                    && self.iam_state.current_user.is_some()
1773                {
1774                    self.iam_state.user_tab = self.iam_state.user_tab.prev();
1775                } else if self.current_service == Service::IamUserGroups
1776                    && self.iam_state.current_group.is_some()
1777                {
1778                    self.iam_state.group_tab = self.iam_state.group_tab.prev();
1779                } else if self.view_mode == ViewMode::Detail {
1780                    self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
1781                } else if self.current_service == Service::S3Buckets {
1782                    if self.s3_state.current_bucket.is_some() {
1783                        self.s3_state.object_tab = self.s3_state.object_tab.prev();
1784                    }
1785                } else if self.current_service == Service::CloudWatchAlarms {
1786                    self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
1787                        AlarmTab::AllAlarms => AlarmTab::InAlarm,
1788                        AlarmTab::InAlarm => AlarmTab::AllAlarms,
1789                    };
1790                } else if self.current_service == Service::EcrRepositories
1791                    && self.ecr_state.current_repository.is_none()
1792                {
1793                    self.ecr_state.tab = self.ecr_state.tab.prev();
1794                    self.ecr_state.repositories.reset();
1795                    self.ecr_state.repositories.loading = true;
1796                } else if self.current_service == Service::LambdaFunctions
1797                    && self.lambda_state.current_function.is_some()
1798                {
1799                    if self.lambda_state.current_version.is_some() {
1800                        // Version view: only Code and Configuration tabs
1801                        self.lambda_state.detail_tab = match self.lambda_state.detail_tab {
1802                            LambdaDetailTab::Configuration => LambdaDetailTab::Code,
1803                            _ => LambdaDetailTab::Configuration,
1804                        };
1805                    } else {
1806                        self.lambda_state.detail_tab = self.lambda_state.detail_tab.prev();
1807                    }
1808                } else if self.current_service == Service::CloudFormationStacks
1809                    && self.cfn_state.current_stack.is_some()
1810                {
1811                    self.cfn_state.detail_tab = self.cfn_state.detail_tab.prev();
1812                }
1813            }
1814            Action::StartFilter => {
1815                if self.current_service == Service::CloudWatchInsights {
1816                    self.mode = Mode::InsightsInput;
1817                } else if self.current_service == Service::CloudWatchAlarms {
1818                    self.mode = Mode::FilterInput;
1819                } else if self.current_service == Service::S3Buckets {
1820                    self.mode = Mode::FilterInput;
1821                    self.log_groups_state.filter_mode = true;
1822                } else if self.current_service == Service::EcrRepositories
1823                    || self.current_service == Service::IamUsers
1824                    || self.current_service == Service::IamUserGroups
1825                {
1826                    self.mode = Mode::FilterInput;
1827                    if self.current_service == Service::EcrRepositories
1828                        && self.ecr_state.current_repository.is_none()
1829                    {
1830                        self.ecr_state.input_focus = InputFocus::Filter;
1831                    }
1832                } else if self.current_service == Service::LambdaFunctions {
1833                    self.mode = Mode::FilterInput;
1834                    if self.lambda_state.current_version.is_some()
1835                        && self.lambda_state.detail_tab
1836                            == crate::app::LambdaDetailTab::Configuration
1837                    {
1838                        self.lambda_state.alias_input_focus = InputFocus::Filter;
1839                    } else if self.lambda_state.current_function.is_some()
1840                        && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Versions
1841                    {
1842                        self.lambda_state.version_input_focus = InputFocus::Filter;
1843                    } else if self.lambda_state.current_function.is_none() {
1844                        self.lambda_state.input_focus = InputFocus::Filter;
1845                    }
1846                } else if self.current_service == Service::LambdaApplications {
1847                    self.mode = Mode::FilterInput;
1848                    use crate::ui::lambda::ApplicationDetailTab;
1849                    if self.lambda_application_state.current_application.is_some() {
1850                        // In detail view - check which tab
1851                        if self.lambda_application_state.detail_tab
1852                            == ApplicationDetailTab::Overview
1853                        {
1854                            self.lambda_application_state.resource_input_focus = InputFocus::Filter;
1855                        } else {
1856                            self.lambda_application_state.deployment_input_focus =
1857                                InputFocus::Filter;
1858                        }
1859                    } else {
1860                        self.lambda_application_state.input_focus = InputFocus::Filter;
1861                    }
1862                } else if self.current_service == Service::IamRoles {
1863                    self.mode = Mode::FilterInput;
1864                } else if self.current_service == Service::CloudFormationStacks {
1865                    self.mode = Mode::FilterInput;
1866                    self.cfn_state.input_focus = InputFocus::Filter;
1867                } else if self.view_mode == ViewMode::List
1868                    || (self.view_mode == ViewMode::Detail
1869                        && self.log_groups_state.detail_tab == DetailTab::LogStreams)
1870                {
1871                    self.mode = Mode::FilterInput;
1872                    self.log_groups_state.filter_mode = true;
1873                    self.log_groups_state.input_focus = InputFocus::Filter;
1874                }
1875            }
1876            Action::StartEventFilter => {
1877                if self.current_service == Service::CloudWatchInsights {
1878                    self.mode = Mode::InsightsInput;
1879                } else if self.view_mode == ViewMode::List {
1880                    self.mode = Mode::FilterInput;
1881                    self.log_groups_state.filter_mode = true;
1882                    self.log_groups_state.input_focus = InputFocus::Filter;
1883                } else if self.view_mode == ViewMode::Events {
1884                    self.mode = Mode::EventFilterInput;
1885                } else if self.view_mode == ViewMode::Detail
1886                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
1887                {
1888                    self.mode = Mode::FilterInput;
1889                    self.log_groups_state.filter_mode = true;
1890                    self.log_groups_state.input_focus = InputFocus::Filter;
1891                }
1892            }
1893            Action::NextFilterFocus => {
1894                if self.mode == Mode::FilterInput
1895                    && self.current_service == Service::LambdaApplications
1896                {
1897                    use crate::ui::lambda::FILTER_CONTROLS;
1898                    if self.lambda_application_state.current_application.is_some() {
1899                        if self.lambda_application_state.detail_tab
1900                            == crate::ui::lambda::ApplicationDetailTab::Deployments
1901                        {
1902                            self.lambda_application_state.deployment_input_focus = self
1903                                .lambda_application_state
1904                                .deployment_input_focus
1905                                .next(&FILTER_CONTROLS);
1906                        } else {
1907                            self.lambda_application_state.resource_input_focus = self
1908                                .lambda_application_state
1909                                .resource_input_focus
1910                                .next(&FILTER_CONTROLS);
1911                        }
1912                    } else {
1913                        self.lambda_application_state.input_focus = self
1914                            .lambda_application_state
1915                            .input_focus
1916                            .next(&FILTER_CONTROLS);
1917                    }
1918                } else if self.mode == Mode::FilterInput
1919                    && self.current_service == Service::IamRoles
1920                    && self.iam_state.current_role.is_some()
1921                {
1922                    use crate::ui::iam::POLICY_FILTER_CONTROLS;
1923                    self.iam_state.policy_input_focus = self
1924                        .iam_state
1925                        .policy_input_focus
1926                        .next(&POLICY_FILTER_CONTROLS);
1927                } else if self.mode == Mode::FilterInput
1928                    && self.current_service == Service::IamRoles
1929                    && self.iam_state.current_role.is_none()
1930                {
1931                    use crate::ui::iam::ROLE_FILTER_CONTROLS;
1932                    self.iam_state.role_input_focus =
1933                        self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
1934                } else if self.mode == Mode::InsightsInput {
1935                    use crate::app::InsightsFocus;
1936                    self.insights_state.insights.insights_focus =
1937                        match self.insights_state.insights.insights_focus {
1938                            InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
1939                            InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
1940                            InsightsFocus::LogGroupSearch => InsightsFocus::Query,
1941                            InsightsFocus::Query => InsightsFocus::QueryLanguage,
1942                        };
1943                } else if self.mode == Mode::FilterInput
1944                    && self.current_service == Service::CloudFormationStacks
1945                {
1946                    self.cfn_state.input_focus = self
1947                        .cfn_state
1948                        .input_focus
1949                        .next(&crate::ui::cfn::State::FILTER_CONTROLS);
1950                } else if self.mode == Mode::FilterInput
1951                    && self.current_service == Service::CloudWatchLogGroups
1952                {
1953                    use crate::ui::cw::logs::FILTER_CONTROLS;
1954                    self.log_groups_state.input_focus =
1955                        self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
1956                } else if self.mode == Mode::EventFilterInput {
1957                    self.log_groups_state.event_input_focus =
1958                        self.log_groups_state.event_input_focus.next();
1959                } else if self.mode == Mode::FilterInput
1960                    && self.current_service == Service::CloudWatchAlarms
1961                {
1962                    use crate::ui::cw::alarms::FILTER_CONTROLS;
1963                    self.alarms_state.input_focus =
1964                        self.alarms_state.input_focus.next(&FILTER_CONTROLS);
1965                } else if self.mode == Mode::FilterInput
1966                    && self.current_service == Service::EcrRepositories
1967                    && self.ecr_state.current_repository.is_none()
1968                {
1969                    use crate::ui::ecr::FILTER_CONTROLS;
1970                    self.ecr_state.input_focus = self.ecr_state.input_focus.next(&FILTER_CONTROLS);
1971                } else if self.mode == Mode::FilterInput
1972                    && self.current_service == Service::LambdaFunctions
1973                {
1974                    use crate::ui::lambda::FILTER_CONTROLS;
1975                    if self.lambda_state.current_version.is_some()
1976                        && self.lambda_state.detail_tab
1977                            == crate::app::LambdaDetailTab::Configuration
1978                    {
1979                        self.lambda_state.alias_input_focus =
1980                            self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
1981                    } else if self.lambda_state.current_function.is_some()
1982                        && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Versions
1983                    {
1984                        self.lambda_state.version_input_focus =
1985                            self.lambda_state.version_input_focus.next(&FILTER_CONTROLS);
1986                    } else if self.lambda_state.current_function.is_some()
1987                        && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Aliases
1988                    {
1989                        self.lambda_state.alias_input_focus =
1990                            self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
1991                    } else if self.lambda_state.current_function.is_none() {
1992                        self.lambda_state.input_focus =
1993                            self.lambda_state.input_focus.next(&FILTER_CONTROLS);
1994                    }
1995                }
1996            }
1997            Action::PrevFilterFocus => {
1998                if self.mode == Mode::FilterInput
1999                    && self.current_service == Service::LambdaApplications
2000                {
2001                    use crate::ui::lambda::FILTER_CONTROLS;
2002                    if self.lambda_application_state.current_application.is_some() {
2003                        if self.lambda_application_state.detail_tab
2004                            == crate::ui::lambda::ApplicationDetailTab::Deployments
2005                        {
2006                            self.lambda_application_state.deployment_input_focus = self
2007                                .lambda_application_state
2008                                .deployment_input_focus
2009                                .prev(&FILTER_CONTROLS);
2010                        } else {
2011                            self.lambda_application_state.resource_input_focus = self
2012                                .lambda_application_state
2013                                .resource_input_focus
2014                                .prev(&FILTER_CONTROLS);
2015                        }
2016                    } else {
2017                        self.lambda_application_state.input_focus = self
2018                            .lambda_application_state
2019                            .input_focus
2020                            .prev(&FILTER_CONTROLS);
2021                    }
2022                } else if self.mode == Mode::FilterInput
2023                    && self.current_service == Service::CloudFormationStacks
2024                {
2025                    self.cfn_state.input_focus = self
2026                        .cfn_state
2027                        .input_focus
2028                        .prev(&crate::ui::cfn::State::FILTER_CONTROLS);
2029                } else if self.mode == Mode::FilterInput
2030                    && self.current_service == Service::IamRoles
2031                    && self.iam_state.current_role.is_none()
2032                {
2033                    use crate::ui::iam::ROLE_FILTER_CONTROLS;
2034                    self.iam_state.role_input_focus =
2035                        self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
2036                } else if self.mode == Mode::FilterInput
2037                    && self.current_service == Service::CloudWatchLogGroups
2038                {
2039                    use crate::ui::cw::logs::FILTER_CONTROLS;
2040                    self.log_groups_state.input_focus =
2041                        self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
2042                } else if self.mode == Mode::EventFilterInput {
2043                    self.log_groups_state.event_input_focus =
2044                        self.log_groups_state.event_input_focus.prev();
2045                } else if self.mode == Mode::FilterInput
2046                    && self.current_service == Service::IamRoles
2047                    && self.iam_state.current_role.is_some()
2048                {
2049                    use crate::ui::iam::POLICY_FILTER_CONTROLS;
2050                    self.iam_state.policy_input_focus = self
2051                        .iam_state
2052                        .policy_input_focus
2053                        .prev(&POLICY_FILTER_CONTROLS);
2054                } else if self.mode == Mode::FilterInput
2055                    && self.current_service == Service::CloudWatchAlarms
2056                {
2057                    use crate::ui::cw::alarms::FILTER_CONTROLS;
2058                    self.alarms_state.input_focus =
2059                        self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
2060                } else if self.mode == Mode::FilterInput
2061                    && self.current_service == Service::EcrRepositories
2062                    && self.ecr_state.current_repository.is_none()
2063                {
2064                    use crate::ui::ecr::FILTER_CONTROLS;
2065                    self.ecr_state.input_focus = self.ecr_state.input_focus.prev(&FILTER_CONTROLS);
2066                } else if self.mode == Mode::FilterInput
2067                    && self.current_service == Service::LambdaFunctions
2068                {
2069                    use crate::ui::lambda::FILTER_CONTROLS;
2070                    if self.lambda_state.current_version.is_some()
2071                        && self.lambda_state.detail_tab
2072                            == crate::app::LambdaDetailTab::Configuration
2073                    {
2074                        self.lambda_state.alias_input_focus =
2075                            self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
2076                    } else if self.lambda_state.current_function.is_some()
2077                        && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Versions
2078                    {
2079                        self.lambda_state.version_input_focus =
2080                            self.lambda_state.version_input_focus.prev(&FILTER_CONTROLS);
2081                    } else if self.lambda_state.current_function.is_some()
2082                        && self.lambda_state.detail_tab == crate::app::LambdaDetailTab::Aliases
2083                    {
2084                        self.lambda_state.alias_input_focus =
2085                            self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
2086                    } else if self.lambda_state.current_function.is_none() {
2087                        self.lambda_state.input_focus =
2088                            self.lambda_state.input_focus.prev(&FILTER_CONTROLS);
2089                    }
2090                }
2091            }
2092            Action::ToggleFilterCheckbox => {
2093                if self.mode == Mode::InsightsInput {
2094                    use crate::app::InsightsFocus;
2095                    if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2096                        && self.insights_state.insights.show_dropdown
2097                        && !self.insights_state.insights.log_group_matches.is_empty()
2098                    {
2099                        let selected_idx = self.insights_state.insights.dropdown_selected;
2100                        if let Some(group_name) = self
2101                            .insights_state
2102                            .insights
2103                            .log_group_matches
2104                            .get(selected_idx)
2105                        {
2106                            let group_name = group_name.clone();
2107                            if let Some(pos) = self
2108                                .insights_state
2109                                .insights
2110                                .selected_log_groups
2111                                .iter()
2112                                .position(|g| g == &group_name)
2113                            {
2114                                self.insights_state.insights.selected_log_groups.remove(pos);
2115                            } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2116                                self.insights_state
2117                                    .insights
2118                                    .selected_log_groups
2119                                    .push(group_name);
2120                            }
2121                        }
2122                    }
2123                } else if self.mode == Mode::FilterInput
2124                    && self.current_service == Service::CloudFormationStacks
2125                {
2126                    use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
2127                    match self.cfn_state.input_focus {
2128                        STATUS_FILTER => {
2129                            self.cfn_state.status_filter = self.cfn_state.status_filter.next();
2130                        }
2131                        VIEW_NESTED => {
2132                            self.cfn_state.view_nested = !self.cfn_state.view_nested;
2133                        }
2134                        _ => {}
2135                    }
2136                } else if self.mode == Mode::FilterInput
2137                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2138                {
2139                    match self.log_groups_state.input_focus {
2140                        InputFocus::Checkbox("ExactMatch") => {
2141                            self.log_groups_state.exact_match = !self.log_groups_state.exact_match
2142                        }
2143                        InputFocus::Checkbox("ShowExpired") => {
2144                            self.log_groups_state.show_expired = !self.log_groups_state.show_expired
2145                        }
2146                        _ => {}
2147                    }
2148                } else if self.mode == Mode::EventFilterInput
2149                    && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
2150                {
2151                    self.log_groups_state.relative_unit =
2152                        self.log_groups_state.relative_unit.next();
2153                }
2154            }
2155            Action::CycleSortColumn => {
2156                if self.view_mode == ViewMode::Detail
2157                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2158                {
2159                    self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
2160                        StreamSort::Name => StreamSort::CreationTime,
2161                        StreamSort::CreationTime => StreamSort::LastEventTime,
2162                        StreamSort::LastEventTime => StreamSort::Name,
2163                    };
2164                }
2165            }
2166            Action::ToggleSortDirection => {
2167                if self.view_mode == ViewMode::Detail
2168                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2169                {
2170                    self.log_groups_state.stream_sort_desc =
2171                        !self.log_groups_state.stream_sort_desc;
2172                }
2173            }
2174            Action::ScrollUp => {
2175                if self.view_mode == ViewMode::PolicyView {
2176                    self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
2177                } else if self.current_service == Service::IamRoles
2178                    && self.iam_state.current_role.is_some()
2179                    && self.iam_state.role_tab == RoleTab::TrustRelationships
2180                {
2181                    self.iam_state.trust_policy_scroll =
2182                        self.iam_state.trust_policy_scroll.saturating_sub(10);
2183                } else if self.view_mode == ViewMode::Events {
2184                    if self.log_groups_state.event_scroll_offset == 0
2185                        && self.log_groups_state.has_older_events
2186                    {
2187                        self.log_groups_state.loading = true;
2188                    } else {
2189                        self.log_groups_state.event_scroll_offset =
2190                            self.log_groups_state.event_scroll_offset.saturating_sub(1);
2191                    }
2192                } else if self.view_mode == ViewMode::InsightsResults {
2193                    self.insights_state.insights.results_selected = self
2194                        .insights_state
2195                        .insights
2196                        .results_selected
2197                        .saturating_sub(1);
2198                } else if self.view_mode == ViewMode::Detail {
2199                    self.log_groups_state.selected_stream =
2200                        self.log_groups_state.selected_stream.saturating_sub(1);
2201                    self.log_groups_state.expanded_stream = None;
2202                } else if self.view_mode == ViewMode::List
2203                    && self.current_service == Service::CloudWatchLogGroups
2204                {
2205                    self.log_groups_state.log_groups.selected =
2206                        self.log_groups_state.log_groups.selected.saturating_sub(1);
2207                    self.log_groups_state.log_groups.snap_to_page();
2208                } else if self.current_service == Service::EcrRepositories {
2209                    if self.ecr_state.current_repository.is_some() {
2210                        self.ecr_state.images.page_up();
2211                    } else {
2212                        self.ecr_state.repositories.page_up();
2213                    }
2214                }
2215            }
2216            Action::ScrollDown => {
2217                if self.view_mode == ViewMode::PolicyView {
2218                    let lines = self.iam_state.policy_document.lines().count();
2219                    let max_scroll = lines.saturating_sub(1);
2220                    self.iam_state.policy_scroll =
2221                        (self.iam_state.policy_scroll + 10).min(max_scroll);
2222                } else if self.current_service == Service::IamRoles
2223                    && self.iam_state.current_role.is_some()
2224                    && self.iam_state.role_tab == RoleTab::TrustRelationships
2225                {
2226                    let lines = self.iam_state.trust_policy_document.lines().count();
2227                    let max_scroll = lines.saturating_sub(1);
2228                    self.iam_state.trust_policy_scroll =
2229                        (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
2230                } else if self.view_mode == ViewMode::Events {
2231                    let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
2232                    if self.log_groups_state.event_scroll_offset >= max_scroll {
2233                        // At the end, do nothing
2234                    } else {
2235                        self.log_groups_state.event_scroll_offset =
2236                            (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
2237                    }
2238                } else if self.view_mode == ViewMode::InsightsResults {
2239                    let max = self
2240                        .insights_state
2241                        .insights
2242                        .query_results
2243                        .len()
2244                        .saturating_sub(1);
2245                    self.insights_state.insights.results_selected =
2246                        (self.insights_state.insights.results_selected + 1).min(max);
2247                } else if self.view_mode == ViewMode::Detail {
2248                    let filtered_streams = self.filtered_log_streams();
2249                    let max = filtered_streams.len().saturating_sub(1);
2250                    self.log_groups_state.selected_stream =
2251                        (self.log_groups_state.selected_stream + 1).min(max);
2252                } else if self.view_mode == ViewMode::List
2253                    && self.current_service == Service::CloudWatchLogGroups
2254                {
2255                    let filtered_groups = self.filtered_log_groups();
2256                    self.log_groups_state
2257                        .log_groups
2258                        .next_item(filtered_groups.len());
2259                } else if self.current_service == Service::EcrRepositories {
2260                    if self.ecr_state.current_repository.is_some() {
2261                        let filtered_images = self.filtered_ecr_images();
2262                        self.ecr_state.images.page_down(filtered_images.len());
2263                    } else {
2264                        let filtered_repos = self.filtered_ecr_repositories();
2265                        self.ecr_state.repositories.page_down(filtered_repos.len());
2266                    }
2267                }
2268            }
2269
2270            Action::Refresh => {
2271                if self.mode == Mode::ProfilePicker {
2272                    self.log_groups_state.loading = true;
2273                    self.log_groups_state.loading_message = "Refreshing...".to_string();
2274                } else if self.mode == Mode::RegionPicker {
2275                    self.measure_region_latencies();
2276                } else if self.mode == Mode::SessionPicker {
2277                    self.sessions = crate::session::Session::list_all().unwrap_or_default();
2278                } else if self.current_service == Service::CloudWatchInsights
2279                    && !self.insights_state.insights.selected_log_groups.is_empty()
2280                {
2281                    self.log_groups_state.loading = true;
2282                    self.insights_state.insights.query_completed = true;
2283                } else if self.current_service == Service::LambdaFunctions {
2284                    self.lambda_state.table.loading = true;
2285                } else if self.current_service == Service::LambdaApplications {
2286                    self.lambda_application_state.table.loading = true;
2287                } else if matches!(
2288                    self.view_mode,
2289                    ViewMode::Events | ViewMode::Detail | ViewMode::List
2290                ) {
2291                    self.log_groups_state.loading = true;
2292                }
2293            }
2294            Action::Yank => {
2295                if self.mode == Mode::ErrorModal {
2296                    // Copy error message
2297                    if let Some(error) = &self.error_message {
2298                        copy_to_clipboard(error);
2299                    }
2300                } else if self.view_mode == ViewMode::Events {
2301                    if let Some(event) = self
2302                        .log_groups_state
2303                        .log_events
2304                        .get(self.log_groups_state.event_scroll_offset)
2305                    {
2306                        copy_to_clipboard(&event.message);
2307                    }
2308                } else if self.current_service == Service::EcrRepositories {
2309                    if self.ecr_state.current_repository.is_some() {
2310                        let filtered_images = self.filtered_ecr_images();
2311                        if let Some(image) = self.ecr_state.images.get_selected(&filtered_images) {
2312                            copy_to_clipboard(&image.uri);
2313                        }
2314                    } else {
2315                        let filtered_repos = self.filtered_ecr_repositories();
2316                        if let Some(repo) =
2317                            self.ecr_state.repositories.get_selected(&filtered_repos)
2318                        {
2319                            copy_to_clipboard(&repo.uri);
2320                        }
2321                    }
2322                } else if self.current_service == Service::LambdaFunctions {
2323                    let filtered_functions = crate::ui::lambda::filtered_lambda_functions(self);
2324                    if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
2325                        copy_to_clipboard(&func.arn);
2326                    }
2327                } else if self.current_service == Service::CloudFormationStacks {
2328                    if let Some(stack_name) = &self.cfn_state.current_stack {
2329                        // In detail view - copy current stack ARN
2330                        if let Some(stack) = self
2331                            .cfn_state
2332                            .table
2333                            .items
2334                            .iter()
2335                            .find(|s| &s.name == stack_name)
2336                        {
2337                            copy_to_clipboard(&stack.stack_id);
2338                        }
2339                    } else {
2340                        // In list view - copy selected stack ARN
2341                        let filtered_stacks = self.filtered_cloudformation_stacks();
2342                        if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
2343                            copy_to_clipboard(&stack.stack_id);
2344                        }
2345                    }
2346                } else if self.current_service == Service::IamUsers {
2347                    if self.iam_state.current_user.is_some() {
2348                        if let Some(user_name) = &self.iam_state.current_user {
2349                            if let Some(user) = self
2350                                .iam_state
2351                                .users
2352                                .items
2353                                .iter()
2354                                .find(|u| u.user_name == *user_name)
2355                            {
2356                                copy_to_clipboard(&user.arn);
2357                            }
2358                        }
2359                    } else {
2360                        let filtered_users = crate::ui::iam::filtered_iam_users(self);
2361                        if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
2362                            copy_to_clipboard(&user.arn);
2363                        }
2364                    }
2365                } else if self.current_service == Service::IamRoles {
2366                    if self.iam_state.current_role.is_some() {
2367                        if let Some(role_name) = &self.iam_state.current_role {
2368                            if let Some(role) = self
2369                                .iam_state
2370                                .roles
2371                                .items
2372                                .iter()
2373                                .find(|r| r.role_name == *role_name)
2374                            {
2375                                copy_to_clipboard(&role.arn);
2376                            }
2377                        }
2378                    } else {
2379                        let filtered_roles = crate::ui::iam::filtered_iam_roles(self);
2380                        if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
2381                            copy_to_clipboard(&role.arn);
2382                        }
2383                    }
2384                } else if self.current_service == Service::IamUserGroups {
2385                    if self.iam_state.current_group.is_some() {
2386                        if let Some(group_name) = &self.iam_state.current_group {
2387                            let arn = iam::format_arn(&self.config.account_id, "group", group_name);
2388                            copy_to_clipboard(&arn);
2389                        }
2390                    } else {
2391                        let filtered_groups: Vec<_> = self
2392                            .iam_state
2393                            .groups
2394                            .items
2395                            .iter()
2396                            .filter(|g| {
2397                                if self.iam_state.groups.filter.is_empty() {
2398                                    true
2399                                } else {
2400                                    g.group_name
2401                                        .to_lowercase()
2402                                        .contains(&self.iam_state.groups.filter.to_lowercase())
2403                                }
2404                            })
2405                            .collect();
2406                        if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
2407                            let arn = iam::format_arn(
2408                                &self.config.account_id,
2409                                "group",
2410                                &group.group_name,
2411                            );
2412                            copy_to_clipboard(&arn);
2413                        }
2414                    }
2415                }
2416            }
2417            Action::CopyToClipboard => {
2418                // Request snapshot - will be captured after next render
2419                self.snapshot_requested = true;
2420            }
2421            Action::RetryLoad => {
2422                self.error_message = None;
2423                self.mode = Mode::Normal;
2424                self.log_groups_state.loading = true;
2425            }
2426            Action::ApplyFilter => {
2427                if self.mode == Mode::InsightsInput {
2428                    use crate::app::InsightsFocus;
2429                    if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2430                        && self.insights_state.insights.show_dropdown
2431                    {
2432                        // Close dropdown, exit input mode, and execute query
2433                        self.insights_state.insights.show_dropdown = false;
2434                        self.mode = Mode::Normal;
2435                        if !self.insights_state.insights.selected_log_groups.is_empty() {
2436                            self.log_groups_state.loading = true;
2437                            self.insights_state.insights.query_completed = true;
2438                        }
2439                    }
2440                } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
2441                    if let Ok(page) = self.page_input.parse::<usize>() {
2442                        self.go_to_page(page);
2443                    }
2444                    self.page_input.clear();
2445                } else {
2446                    self.mode = Mode::Normal;
2447                    self.log_groups_state.filter_mode = false;
2448                }
2449            }
2450            Action::ToggleExactMatch => {
2451                if self.view_mode == ViewMode::Detail
2452                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2453                {
2454                    self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
2455                }
2456            }
2457            Action::ToggleShowExpired => {
2458                if self.view_mode == ViewMode::Detail
2459                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2460                {
2461                    self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
2462                }
2463            }
2464            Action::GoBack => {
2465                // ServicePicker: close if we have tabs
2466                if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
2467                    self.mode = Mode::Normal;
2468                    self.service_picker.filter.clear();
2469                }
2470                // S3: pop navigation stack first, then exit bucket
2471                else if self.current_service == Service::S3Buckets
2472                    && self.s3_state.current_bucket.is_some()
2473                {
2474                    if !self.s3_state.prefix_stack.is_empty() {
2475                        self.s3_state.prefix_stack.pop();
2476                        self.s3_state.buckets.loading = true;
2477                    } else {
2478                        self.s3_state.current_bucket = None;
2479                        self.s3_state.objects.clear();
2480                    }
2481                }
2482                // ECR: go back from images to repositories
2483                else if self.current_service == Service::EcrRepositories
2484                    && self.ecr_state.current_repository.is_some()
2485                {
2486                    if self.ecr_state.images.has_expanded_item() {
2487                        self.ecr_state.images.collapse();
2488                    } else {
2489                        self.ecr_state.current_repository = None;
2490                        self.ecr_state.current_repository_uri = None;
2491                        self.ecr_state.images.items.clear();
2492                        self.ecr_state.images.reset();
2493                    }
2494                }
2495                // IAM: go back from user detail to list
2496                else if self.current_service == Service::IamUsers
2497                    && self.iam_state.current_user.is_some()
2498                {
2499                    self.iam_state.current_user = None;
2500                    self.iam_state.policies.items.clear();
2501                    self.iam_state.policies.reset();
2502                    self.update_current_tab_breadcrumb();
2503                }
2504                // IAM: go back from group detail to list
2505                else if self.current_service == Service::IamUserGroups
2506                    && self.iam_state.current_group.is_some()
2507                {
2508                    self.iam_state.current_group = None;
2509                    self.update_current_tab_breadcrumb();
2510                }
2511                // IAM: go back from role detail to list
2512                else if self.current_service == Service::IamRoles {
2513                    if self.view_mode == ViewMode::PolicyView {
2514                        // Go back from policy view to role detail
2515                        self.view_mode = ViewMode::Detail;
2516                        self.iam_state.current_policy = None;
2517                        self.iam_state.policy_document.clear();
2518                        self.iam_state.policy_scroll = 0;
2519                        self.update_current_tab_breadcrumb();
2520                    } else if self.iam_state.current_role.is_some() {
2521                        self.iam_state.current_role = None;
2522                        self.iam_state.policies.items.clear();
2523                        self.iam_state.policies.reset();
2524                        self.update_current_tab_breadcrumb();
2525                    }
2526                }
2527                // Lambda: go back from version detail to function detail
2528                else if self.current_service == Service::LambdaFunctions
2529                    && self.lambda_state.current_version.is_some()
2530                {
2531                    self.lambda_state.current_version = None;
2532                    self.lambda_state.detail_tab = LambdaDetailTab::Versions;
2533                }
2534                // Lambda: go back from alias detail to function detail
2535                else if self.current_service == Service::LambdaFunctions
2536                    && self.lambda_state.current_alias.is_some()
2537                {
2538                    self.lambda_state.current_alias = None;
2539                    self.lambda_state.detail_tab = LambdaDetailTab::Aliases;
2540                }
2541                // Lambda: go back from function detail to list
2542                else if self.current_service == Service::LambdaFunctions
2543                    && self.lambda_state.current_function.is_some()
2544                {
2545                    self.lambda_state.current_function = None;
2546                    self.update_current_tab_breadcrumb();
2547                }
2548                // Lambda Applications: go back from application detail to list
2549                else if self.current_service == Service::LambdaApplications
2550                    && self.lambda_application_state.current_application.is_some()
2551                {
2552                    self.lambda_application_state.current_application = None;
2553                    self.update_current_tab_breadcrumb();
2554                }
2555                // CloudFormation: go back from stack detail to list
2556                else if self.current_service == Service::CloudFormationStacks
2557                    && self.cfn_state.current_stack.is_some()
2558                {
2559                    self.cfn_state.current_stack = None;
2560                    self.update_current_tab_breadcrumb();
2561                }
2562                // From insights results -> collapse if expanded, otherwise back to sidebar
2563                else if self.view_mode == ViewMode::InsightsResults {
2564                    if self.insights_state.insights.expanded_result.is_some() {
2565                        self.insights_state.insights.expanded_result = None;
2566                    }
2567                }
2568                // From alarms view -> collapse if expanded
2569                else if self.current_service == Service::CloudWatchAlarms {
2570                    if self.alarms_state.table.has_expanded_item() {
2571                        self.alarms_state.table.collapse();
2572                    }
2573                }
2574                // From events view -> collapse if expanded, otherwise back to detail view
2575                else if self.view_mode == ViewMode::Events {
2576                    if self.log_groups_state.expanded_event.is_some() {
2577                        self.log_groups_state.expanded_event = None;
2578                    } else {
2579                        self.view_mode = ViewMode::Detail;
2580                        self.log_groups_state.event_filter.clear();
2581                    }
2582                }
2583                // From detail view -> back to list view
2584                else if self.view_mode == ViewMode::Detail {
2585                    self.view_mode = ViewMode::List;
2586                    self.log_groups_state.stream_filter.clear();
2587                    self.log_groups_state.exact_match = false;
2588                    self.log_groups_state.show_expired = false;
2589                }
2590            }
2591            Action::OpenInConsole | Action::OpenInBrowser => {
2592                let url = self.get_console_url();
2593                let _ = webbrowser::open(&url);
2594            }
2595            Action::ShowHelp => {
2596                self.mode = Mode::HelpModal;
2597            }
2598            Action::OpenRegionPicker => {
2599                self.region_filter.clear();
2600                self.region_picker_selected = 0;
2601                self.measure_region_latencies();
2602                self.mode = Mode::RegionPicker;
2603            }
2604            Action::OpenProfilePicker => {
2605                self.profile_filter.clear();
2606                self.profile_picker_selected = 0;
2607                self.available_profiles = Self::load_aws_profiles();
2608                self.mode = Mode::ProfilePicker;
2609            }
2610            Action::OpenCalendar => {
2611                self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
2612                self.calendar_selecting = CalendarField::StartDate;
2613                self.mode = Mode::CalendarPicker;
2614            }
2615            Action::CloseCalendar => {
2616                self.mode = Mode::Normal;
2617                self.calendar_date = None;
2618            }
2619            Action::CalendarPrevDay => {
2620                if let Some(date) = self.calendar_date {
2621                    self.calendar_date = date.checked_sub(time::Duration::days(1));
2622                }
2623            }
2624            Action::CalendarNextDay => {
2625                if let Some(date) = self.calendar_date {
2626                    self.calendar_date = date.checked_add(time::Duration::days(1));
2627                }
2628            }
2629            Action::CalendarPrevWeek => {
2630                if let Some(date) = self.calendar_date {
2631                    self.calendar_date = date.checked_sub(time::Duration::weeks(1));
2632                }
2633            }
2634            Action::CalendarNextWeek => {
2635                if let Some(date) = self.calendar_date {
2636                    self.calendar_date = date.checked_add(time::Duration::weeks(1));
2637                }
2638            }
2639            Action::CalendarPrevMonth => {
2640                if let Some(date) = self.calendar_date {
2641                    self.calendar_date = Some(if date.month() == time::Month::January {
2642                        date.replace_month(time::Month::December)
2643                            .unwrap()
2644                            .replace_year(date.year() - 1)
2645                            .unwrap()
2646                    } else {
2647                        date.replace_month(date.month().previous()).unwrap()
2648                    });
2649                }
2650            }
2651            Action::CalendarNextMonth => {
2652                if let Some(date) = self.calendar_date {
2653                    self.calendar_date = Some(if date.month() == time::Month::December {
2654                        date.replace_month(time::Month::January)
2655                            .unwrap()
2656                            .replace_year(date.year() + 1)
2657                            .unwrap()
2658                    } else {
2659                        date.replace_month(date.month().next()).unwrap()
2660                    });
2661                }
2662            }
2663            Action::CalendarSelect => {
2664                if let Some(date) = self.calendar_date {
2665                    let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
2666                        .unix_timestamp()
2667                        * 1000;
2668                    match self.calendar_selecting {
2669                        CalendarField::StartDate => {
2670                            self.log_groups_state.start_time = Some(timestamp);
2671                            self.calendar_selecting = CalendarField::EndDate;
2672                        }
2673                        CalendarField::EndDate => {
2674                            self.log_groups_state.end_time = Some(timestamp);
2675                            self.mode = Mode::Normal;
2676                            self.calendar_date = None;
2677                        }
2678                    }
2679                }
2680            }
2681        }
2682    }
2683
2684    pub fn filtered_services(&self) -> Vec<&'static str> {
2685        let mut services = if self.service_picker.filter.is_empty() {
2686            self.service_picker.services.clone()
2687        } else {
2688            self.service_picker
2689                .services
2690                .iter()
2691                .filter(|s| {
2692                    s.to_lowercase()
2693                        .contains(&self.service_picker.filter.to_lowercase())
2694                })
2695                .copied()
2696                .collect()
2697        };
2698        services.sort();
2699        services
2700    }
2701
2702    pub fn selected_log_group(&self) -> Option<&LogGroup> {
2703        crate::ui::cw::logs::selected_log_group(self)
2704    }
2705
2706    pub fn filtered_log_streams(&self) -> Vec<&LogStream> {
2707        crate::ui::cw::logs::filtered_log_streams(self)
2708    }
2709
2710    pub fn filtered_log_events(&self) -> Vec<&LogEvent> {
2711        crate::ui::cw::logs::filtered_log_events(self)
2712    }
2713
2714    pub fn filtered_log_groups(&self) -> Vec<&LogGroup> {
2715        crate::ui::cw::logs::filtered_log_groups(self)
2716    }
2717
2718    pub fn filtered_ecr_repositories(&self) -> Vec<&EcrRepository> {
2719        crate::ui::ecr::filtered_ecr_repositories(self)
2720    }
2721
2722    pub fn filtered_ecr_images(&self) -> Vec<&EcrImage> {
2723        crate::ui::ecr::filtered_ecr_images(self)
2724    }
2725
2726    pub fn filtered_cloudformation_stacks(&self) -> Vec<&CfnStack> {
2727        crate::ui::cfn::filtered_cloudformation_stacks(self)
2728    }
2729
2730    pub fn breadcrumbs(&self) -> String {
2731        if !self.service_selected {
2732            return String::new();
2733        }
2734
2735        let mut parts = vec![];
2736
2737        match self.current_service {
2738            Service::CloudWatchLogGroups => {
2739                parts.push("CloudWatch".to_string());
2740                parts.push("Log groups".to_string());
2741
2742                if self.view_mode != ViewMode::List {
2743                    if let Some(group) = self.selected_log_group() {
2744                        parts.push(group.name.clone());
2745                    }
2746                }
2747
2748                if self.view_mode == ViewMode::Events {
2749                    if let Some(stream) = self
2750                        .log_groups_state
2751                        .log_streams
2752                        .get(self.log_groups_state.selected_stream)
2753                    {
2754                        parts.push(stream.name.clone());
2755                    }
2756                }
2757            }
2758            Service::CloudWatchInsights => {
2759                parts.push("CloudWatch".to_string());
2760                parts.push("Insights".to_string());
2761            }
2762            Service::CloudWatchAlarms => {
2763                parts.push("CloudWatch".to_string());
2764                parts.push("Alarms".to_string());
2765            }
2766            Service::S3Buckets => {
2767                parts.push("S3".to_string());
2768                if let Some(bucket) = &self.s3_state.current_bucket {
2769                    parts.push(bucket.clone());
2770                    if let Some(prefix) = self.s3_state.prefix_stack.last() {
2771                        parts.push(prefix.trim_end_matches('/').to_string());
2772                    }
2773                } else {
2774                    parts.push("Buckets".to_string());
2775                }
2776            }
2777            Service::EcrRepositories => {
2778                parts.push("ECR".to_string());
2779                if let Some(repo) = &self.ecr_state.current_repository {
2780                    parts.push(repo.clone());
2781                } else {
2782                    parts.push("Repositories".to_string());
2783                }
2784            }
2785            Service::LambdaFunctions => {
2786                parts.push("Lambda".to_string());
2787                if let Some(func) = &self.lambda_state.current_function {
2788                    parts.push(func.clone());
2789                } else {
2790                    parts.push("Functions".to_string());
2791                }
2792            }
2793            Service::LambdaApplications => {
2794                parts.push("Lambda".to_string());
2795                parts.push("Applications".to_string());
2796            }
2797            Service::CloudFormationStacks => {
2798                parts.push("CloudFormation".to_string());
2799                if let Some(stack_name) = &self.cfn_state.current_stack {
2800                    parts.push(stack_name.clone());
2801                } else {
2802                    parts.push("Stacks".to_string());
2803                }
2804            }
2805            Service::IamUsers => {
2806                parts.push("IAM".to_string());
2807                parts.push("Users".to_string());
2808            }
2809            Service::IamRoles => {
2810                parts.push("IAM".to_string());
2811                parts.push("Roles".to_string());
2812                if let Some(role_name) = &self.iam_state.current_role {
2813                    parts.push(role_name.clone());
2814                    if let Some(policy_name) = &self.iam_state.current_policy {
2815                        parts.push(policy_name.clone());
2816                    }
2817                }
2818            }
2819            Service::IamUserGroups => {
2820                parts.push("IAM".to_string());
2821                parts.push("User Groups".to_string());
2822                if let Some(group_name) = &self.iam_state.current_group {
2823                    parts.push(group_name.clone());
2824                }
2825            }
2826        }
2827
2828        parts.join(" > ")
2829    }
2830
2831    pub fn update_current_tab_breadcrumb(&mut self) {
2832        if !self.tabs.is_empty() {
2833            self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
2834        }
2835    }
2836
2837    pub fn get_console_url(&self) -> String {
2838        use crate::{cfn, cw, ecr, iam, lambda, s3};
2839
2840        match self.current_service {
2841            Service::CloudWatchLogGroups => {
2842                if self.view_mode == ViewMode::Events {
2843                    if let Some(group) = self.selected_log_group() {
2844                        if let Some(stream) = self
2845                            .log_groups_state
2846                            .log_streams
2847                            .get(self.log_groups_state.selected_stream)
2848                        {
2849                            return cw::logs::console_url_stream(
2850                                &self.config.region,
2851                                &group.name,
2852                                &stream.name,
2853                            );
2854                        }
2855                    }
2856                } else if self.view_mode == ViewMode::Detail {
2857                    if let Some(group) = self.selected_log_group() {
2858                        return cw::logs::console_url_detail(&self.config.region, &group.name);
2859                    }
2860                }
2861                cw::logs::console_url_list(&self.config.region)
2862            }
2863            Service::CloudWatchInsights => cw::insights::console_url(
2864                &self.config.region,
2865                &self.config.account_id,
2866                &self.insights_state.insights.query_text,
2867                &self.insights_state.insights.selected_log_groups,
2868            ),
2869            Service::CloudWatchAlarms => {
2870                let view_type = match self.alarms_state.view_as {
2871                    AlarmViewMode::Table | AlarmViewMode::Detail => "table",
2872                    AlarmViewMode::Cards => "card",
2873                };
2874                cw::alarms::console_url(
2875                    &self.config.region,
2876                    view_type,
2877                    self.alarms_state.table.page_size.value(),
2878                    &self.alarms_state.sort_column,
2879                    self.alarms_state.sort_direction.as_str(),
2880                )
2881            }
2882            Service::S3Buckets => {
2883                if let Some(bucket_name) = &self.s3_state.current_bucket {
2884                    let prefix = self.s3_state.prefix_stack.join("");
2885                    s3::console_url_bucket(&self.config.region, bucket_name, &prefix)
2886                } else {
2887                    s3::console_url_buckets(&self.config.region)
2888                }
2889            }
2890            Service::EcrRepositories => {
2891                if let Some(repo_name) = &self.ecr_state.current_repository {
2892                    ecr::console_url_private_repository(
2893                        &self.config.region,
2894                        &self.config.account_id,
2895                        repo_name,
2896                    )
2897                } else {
2898                    ecr::console_url_repositories(&self.config.region)
2899                }
2900            }
2901            Service::LambdaFunctions => {
2902                if let Some(func_name) = &self.lambda_state.current_function {
2903                    if let Some(version) = &self.lambda_state.current_version {
2904                        lambda::console_url_function_version(
2905                            &self.config.region,
2906                            func_name,
2907                            version,
2908                            &self.lambda_state.detail_tab,
2909                        )
2910                    } else {
2911                        lambda::console_url_function_detail(&self.config.region, func_name)
2912                    }
2913                } else {
2914                    lambda::console_url_functions(&self.config.region)
2915                }
2916            }
2917            Service::LambdaApplications => {
2918                if let Some(app_name) = &self.lambda_application_state.current_application {
2919                    lambda::console_url_application_detail(
2920                        &self.config.region,
2921                        app_name,
2922                        &self.lambda_application_state.detail_tab,
2923                    )
2924                } else {
2925                    lambda::console_url_applications(&self.config.region)
2926                }
2927            }
2928            Service::CloudFormationStacks => {
2929                if let Some(stack_name) = &self.cfn_state.current_stack {
2930                    if let Some(stack) = self
2931                        .cfn_state
2932                        .table
2933                        .items
2934                        .iter()
2935                        .find(|s| &s.name == stack_name)
2936                    {
2937                        return cfn::console_url_stack_detail_with_tab(
2938                            &self.config.region,
2939                            &stack.stack_id,
2940                            &self.cfn_state.detail_tab,
2941                        );
2942                    }
2943                }
2944                cfn::console_url_stacks(&self.config.region)
2945            }
2946            Service::IamUsers => {
2947                if let Some(user_name) = &self.iam_state.current_user {
2948                    let section = match self.iam_state.user_tab {
2949                        UserTab::Permissions => "permissions",
2950                        UserTab::Groups => "groups",
2951                        UserTab::Tags => "tags",
2952                        UserTab::SecurityCredentials => "security_credentials",
2953                        UserTab::LastAccessed => "access_advisor",
2954                    };
2955                    iam::console_url_user_detail(&self.config.region, user_name, section)
2956                } else {
2957                    iam::console_url_users(&self.config.region)
2958                }
2959            }
2960            Service::IamRoles => {
2961                if let Some(policy_name) = &self.iam_state.current_policy {
2962                    if let Some(role_name) = &self.iam_state.current_role {
2963                        return iam::console_url_role_policy(
2964                            &self.config.region,
2965                            role_name,
2966                            policy_name,
2967                        );
2968                    }
2969                }
2970                if let Some(role_name) = &self.iam_state.current_role {
2971                    let section = match self.iam_state.role_tab {
2972                        RoleTab::Permissions => "permissions",
2973                        RoleTab::TrustRelationships => "trust_relationships",
2974                        RoleTab::Tags => "tags",
2975                        RoleTab::LastAccessed => "access_advisor",
2976                        RoleTab::RevokeSessions => "revoke_sessions",
2977                    };
2978                    iam::console_url_role_detail(&self.config.region, role_name, section)
2979                } else {
2980                    iam::console_url_roles(&self.config.region)
2981                }
2982            }
2983            Service::IamUserGroups => iam::console_url_groups(&self.config.region),
2984        }
2985    }
2986
2987    fn calculate_total_bucket_rows(&self) -> usize {
2988        crate::ui::s3::calculate_total_bucket_rows(self)
2989    }
2990
2991    fn calculate_total_object_rows(&self) -> usize {
2992        crate::ui::s3::calculate_total_object_rows(self)
2993    }
2994
2995    fn next_item(&mut self) {
2996        match self.mode {
2997            Mode::FilterInput => {
2998                if self.current_service == Service::CloudFormationStacks {
2999                    use crate::ui::cfn::STATUS_FILTER;
3000                    if self.cfn_state.input_focus == STATUS_FILTER {
3001                        self.cfn_state.status_filter = self.cfn_state.status_filter.next();
3002                    }
3003                }
3004            }
3005            Mode::RegionPicker => {
3006                let filtered = self.get_filtered_regions();
3007                if !filtered.is_empty() {
3008                    self.region_picker_selected =
3009                        (self.region_picker_selected + 1).min(filtered.len() - 1);
3010                }
3011            }
3012            Mode::ProfilePicker => {
3013                let filtered = self.get_filtered_profiles();
3014                if !filtered.is_empty() {
3015                    self.profile_picker_selected =
3016                        (self.profile_picker_selected + 1).min(filtered.len() - 1);
3017                }
3018            }
3019            Mode::SessionPicker => {
3020                let filtered = self.get_filtered_sessions();
3021                if !filtered.is_empty() {
3022                    self.session_picker_selected =
3023                        (self.session_picker_selected + 1).min(filtered.len() - 1);
3024                }
3025            }
3026            Mode::InsightsInput => {
3027                use crate::app::InsightsFocus;
3028                if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3029                    && self.insights_state.insights.show_dropdown
3030                    && !self.insights_state.insights.log_group_matches.is_empty()
3031                {
3032                    let max = self.insights_state.insights.log_group_matches.len() - 1;
3033                    self.insights_state.insights.dropdown_selected =
3034                        (self.insights_state.insights.dropdown_selected + 1).min(max);
3035                }
3036            }
3037            Mode::ColumnSelector => {
3038                let max = if self.current_service == Service::S3Buckets
3039                    && self.s3_state.current_bucket.is_none()
3040                {
3041                    self.all_bucket_columns.len() - 1
3042                } else if self.view_mode == ViewMode::Events {
3043                    self.all_event_columns.len() - 1
3044                } else if self.view_mode == ViewMode::Detail {
3045                    self.all_stream_columns.len() - 1
3046                } else if self.current_service == Service::CloudWatchAlarms {
3047                    // 16 columns + 1 header + 1 empty + 2 view + 1 header + 1 empty + 4 page + 1 header + 1 empty + 1 wrap + 1 header = 30
3048                    29
3049                } else if self.current_service == Service::EcrRepositories {
3050                    if self.ecr_state.current_repository.is_some() {
3051                        // Images: N columns + 1 header + 1 empty + 1 header + 4 page sizes = N + 7
3052                        self.all_ecr_image_columns.len() + 6
3053                    } else {
3054                        // Repositories: just columns
3055                        self.all_ecr_columns.len() - 1
3056                    }
3057                } else if self.current_service == Service::LambdaFunctions {
3058                    // Lambda: N columns + 1 header + 1 empty + 1 header + 4 page sizes = N + 7
3059                    self.lambda_state.all_columns.len() + 6
3060                } else if self.current_service == Service::LambdaApplications {
3061                    // Lambda Applications: N columns + 1 header + 1 empty + 1 header + 3 page sizes = N + 6
3062                    self.all_lambda_application_columns.len() + 5
3063                } else if self.current_service == Service::CloudFormationStacks {
3064                    // CloudFormation: N columns + 1 header + 1 empty + 1 header + 4 page sizes = N + 7
3065                    self.all_cfn_columns.len() + 6
3066                } else if self.current_service == Service::IamUsers {
3067                    if self.iam_state.current_user.is_some() {
3068                        // Policy columns: N columns + 1 header + 1 empty + 1 header + 3 page sizes = N + 6
3069                        self.all_policy_columns.len() + 5
3070                    } else {
3071                        // User columns: N columns + 1 header + 1 empty + 1 header + 3 page sizes = N + 6
3072                        self.all_iam_columns.len() + 5
3073                    }
3074                } else if self.current_service == Service::IamRoles {
3075                    if self.iam_state.current_role.is_some() {
3076                        // Policy columns: N columns + 1 header + 1 empty + 1 header + 3 page sizes = N + 6
3077                        self.all_policy_columns.len() + 5
3078                    } else {
3079                        // Role columns: N columns + 1 header + 1 empty + 1 header + 3 page sizes = N + 6
3080                        self.all_role_columns.len() + 5
3081                    }
3082                } else {
3083                    self.all_columns.len() - 1
3084                };
3085                self.column_selector_index = (self.column_selector_index + 1).min(max);
3086            }
3087            Mode::ServicePicker => {
3088                let filtered = self.filtered_services();
3089                if !filtered.is_empty() {
3090                    self.service_picker.selected =
3091                        (self.service_picker.selected + 1).min(filtered.len() - 1);
3092                }
3093            }
3094            Mode::TabPicker => {
3095                let filtered = self.get_filtered_tabs();
3096                if !filtered.is_empty() {
3097                    self.tab_picker_selected =
3098                        (self.tab_picker_selected + 1).min(filtered.len() - 1);
3099                }
3100            }
3101            Mode::Normal => {
3102                if !self.service_selected {
3103                    let filtered = self.filtered_services();
3104                    if !filtered.is_empty() {
3105                        self.service_picker.selected =
3106                            (self.service_picker.selected + 1).min(filtered.len() - 1);
3107                    }
3108                } else if self.current_service == Service::S3Buckets {
3109                    if self.s3_state.current_bucket.is_some() {
3110                        if self.s3_state.object_tab == S3ObjectTab::Properties {
3111                            // Scroll properties view
3112                            self.s3_state.properties_scroll =
3113                                self.s3_state.properties_scroll.saturating_add(1);
3114                        } else {
3115                            // Calculate total rows including all nested preview items
3116                            let total_rows = self.calculate_total_object_rows();
3117                            let max = total_rows.saturating_sub(1);
3118                            self.s3_state.selected_object =
3119                                (self.s3_state.selected_object + 1).min(max);
3120
3121                            // Adjust scroll offset if selection goes below viewport
3122                            let visible_rows = self.s3_state.object_visible_rows.get();
3123                            if self.s3_state.selected_object
3124                                >= self.s3_state.object_scroll_offset + visible_rows
3125                            {
3126                                self.s3_state.object_scroll_offset =
3127                                    self.s3_state.selected_object - visible_rows + 1;
3128                            }
3129                        }
3130                    } else {
3131                        // Navigate rows in bucket list
3132                        let total_rows = self.calculate_total_bucket_rows();
3133                        if total_rows > 0 {
3134                            self.s3_state.selected_row =
3135                                (self.s3_state.selected_row + 1).min(total_rows - 1);
3136
3137                            // Adjust scroll offset if selection goes below viewport
3138                            let visible_rows = self.s3_state.bucket_visible_rows.get();
3139                            if self.s3_state.selected_row
3140                                >= self.s3_state.bucket_scroll_offset + visible_rows
3141                            {
3142                                self.s3_state.bucket_scroll_offset =
3143                                    self.s3_state.selected_row - visible_rows + 1;
3144                            }
3145                        }
3146                    }
3147                } else if self.view_mode == ViewMode::InsightsResults {
3148                    let max = self
3149                        .insights_state
3150                        .insights
3151                        .query_results
3152                        .len()
3153                        .saturating_sub(1);
3154                    if self.insights_state.insights.results_selected < max {
3155                        self.insights_state.insights.results_selected += 1;
3156                    }
3157                } else if self.view_mode == ViewMode::PolicyView {
3158                    let lines = self.iam_state.policy_document.lines().count();
3159                    let max_scroll = lines.saturating_sub(1);
3160                    self.iam_state.policy_scroll =
3161                        (self.iam_state.policy_scroll + 1).min(max_scroll);
3162                } else if self.view_mode == ViewMode::Events {
3163                    let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3164                    if self.log_groups_state.event_scroll_offset >= max_scroll {
3165                        // At the end, do nothing
3166                    } else {
3167                        self.log_groups_state.event_scroll_offset =
3168                            (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3169                    }
3170                } else if self.current_service == Service::CloudWatchLogGroups {
3171                    if self.view_mode == ViewMode::List {
3172                        let filtered_groups = self.filtered_log_groups();
3173                        self.log_groups_state
3174                            .log_groups
3175                            .next_item(filtered_groups.len());
3176                    } else if self.view_mode == ViewMode::Detail {
3177                        let filtered_streams = self.filtered_log_streams();
3178                        if !filtered_streams.is_empty() {
3179                            let max = filtered_streams.len() - 1;
3180                            if self.log_groups_state.selected_stream >= max {
3181                                // At the end, do nothing
3182                            } else {
3183                                self.log_groups_state.selected_stream =
3184                                    (self.log_groups_state.selected_stream + 1).min(max);
3185                            }
3186                        }
3187                    }
3188                } else if self.current_service == Service::CloudWatchAlarms {
3189                    let filtered_alarms = match self.alarms_state.alarm_tab {
3190                        AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
3191                        AlarmTab::InAlarm => self
3192                            .alarms_state
3193                            .table
3194                            .items
3195                            .iter()
3196                            .filter(|a| a.state.to_uppercase() == "ALARM")
3197                            .count(),
3198                    };
3199                    if filtered_alarms > 0 {
3200                        self.alarms_state.table.next_item(filtered_alarms);
3201                    }
3202                } else if self.current_service == Service::EcrRepositories {
3203                    if self.ecr_state.current_repository.is_some() {
3204                        let filtered_images = self.filtered_ecr_images();
3205                        if !filtered_images.is_empty() {
3206                            self.ecr_state.images.next_item(filtered_images.len());
3207                        }
3208                    } else {
3209                        let filtered_repos = self.filtered_ecr_repositories();
3210                        if !filtered_repos.is_empty() {
3211                            self.ecr_state.repositories.selected =
3212                                (self.ecr_state.repositories.selected + 1)
3213                                    .min(filtered_repos.len() - 1);
3214                            self.ecr_state.repositories.snap_to_page();
3215                        }
3216                    }
3217                } else if self.current_service == Service::LambdaFunctions {
3218                    if self.lambda_state.current_function.is_some()
3219                        && self.lambda_state.detail_tab == LambdaDetailTab::Code
3220                    {
3221                        // Layer table navigation in Code tab
3222                        if let Some(func_name) = &self.lambda_state.current_function {
3223                            if let Some(func) = self
3224                                .lambda_state
3225                                .table
3226                                .items
3227                                .iter()
3228                                .find(|f| f.name == *func_name)
3229                            {
3230                                let max = func.layers.len().saturating_sub(1);
3231                                if !func.layers.is_empty() {
3232                                    self.lambda_state.layer_selected =
3233                                        (self.lambda_state.layer_selected + 1).min(max);
3234                                }
3235                            }
3236                        }
3237                    } else if self.lambda_state.current_function.is_some()
3238                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3239                    {
3240                        // Version table navigation
3241                        let filtered: Vec<_> = self
3242                            .lambda_state
3243                            .version_table
3244                            .items
3245                            .iter()
3246                            .filter(|v| {
3247                                self.lambda_state.version_table.filter.is_empty()
3248                                    || v.version.to_lowercase().contains(
3249                                        &self.lambda_state.version_table.filter.to_lowercase(),
3250                                    )
3251                                    || v.aliases.to_lowercase().contains(
3252                                        &self.lambda_state.version_table.filter.to_lowercase(),
3253                                    )
3254                                    || v.description.to_lowercase().contains(
3255                                        &self.lambda_state.version_table.filter.to_lowercase(),
3256                                    )
3257                            })
3258                            .collect();
3259                        if !filtered.is_empty() {
3260                            self.lambda_state.version_table.selected =
3261                                (self.lambda_state.version_table.selected + 1)
3262                                    .min(filtered.len() - 1);
3263                            self.lambda_state.version_table.snap_to_page();
3264                        }
3265                    } else if self.lambda_state.current_function.is_some()
3266                        && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3267                            || (self.lambda_state.current_version.is_some()
3268                                && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
3269                    {
3270                        // Alias table navigation (both in Aliases tab and Version Configuration)
3271                        let version_filter = self.lambda_state.current_version.clone();
3272                        let filtered: Vec<_> = self
3273                            .lambda_state
3274                            .alias_table
3275                            .items
3276                            .iter()
3277                            .filter(|a| {
3278                                (version_filter.is_none()
3279                                    || a.versions.contains(version_filter.as_ref().unwrap()))
3280                                    && (self.lambda_state.alias_table.filter.is_empty()
3281                                        || a.name.to_lowercase().contains(
3282                                            &self.lambda_state.alias_table.filter.to_lowercase(),
3283                                        )
3284                                        || a.versions.to_lowercase().contains(
3285                                            &self.lambda_state.alias_table.filter.to_lowercase(),
3286                                        )
3287                                        || a.description.to_lowercase().contains(
3288                                            &self.lambda_state.alias_table.filter.to_lowercase(),
3289                                        ))
3290                            })
3291                            .collect();
3292                        if !filtered.is_empty() {
3293                            self.lambda_state.alias_table.selected =
3294                                (self.lambda_state.alias_table.selected + 1)
3295                                    .min(filtered.len() - 1);
3296                            self.lambda_state.alias_table.snap_to_page();
3297                        }
3298                    } else if self.lambda_state.current_function.is_none() {
3299                        let filtered = crate::ui::lambda::filtered_lambda_functions(self);
3300                        if !filtered.is_empty() {
3301                            self.lambda_state.table.next_item(filtered.len());
3302                            self.lambda_state.table.snap_to_page();
3303                        }
3304                    }
3305                } else if self.current_service == Service::LambdaApplications {
3306                    if self.lambda_application_state.current_application.is_some() {
3307                        if self.lambda_application_state.detail_tab
3308                            == crate::ui::lambda::ApplicationDetailTab::Overview
3309                        {
3310                            let len = self.lambda_application_state.resources.items.len();
3311                            if len > 0 {
3312                                self.lambda_application_state.resources.next_item(len);
3313                            }
3314                        } else {
3315                            let len = self.lambda_application_state.deployments.items.len();
3316                            if len > 0 {
3317                                self.lambda_application_state.deployments.next_item(len);
3318                            }
3319                        }
3320                    } else {
3321                        let filtered = crate::ui::lambda::filtered_lambda_applications(self);
3322                        if !filtered.is_empty() {
3323                            self.lambda_application_state.table.selected =
3324                                (self.lambda_application_state.table.selected + 1)
3325                                    .min(filtered.len() - 1);
3326                            self.lambda_application_state.table.snap_to_page();
3327                        }
3328                    }
3329                } else if self.current_service == Service::CloudFormationStacks {
3330                    let filtered = self.filtered_cloudformation_stacks();
3331                    self.cfn_state.table.next_item(filtered.len());
3332                } else if self.current_service == Service::IamUsers {
3333                    if self.iam_state.current_user.is_some() {
3334                        if self.iam_state.user_tab == UserTab::Tags {
3335                            let filtered = crate::ui::iam::filtered_user_tags(self);
3336                            if !filtered.is_empty() {
3337                                self.iam_state.user_tags.next_item(filtered.len());
3338                            }
3339                        } else {
3340                            let filtered = crate::ui::iam::filtered_iam_policies(self);
3341                            if !filtered.is_empty() {
3342                                self.iam_state.policies.next_item(filtered.len());
3343                            }
3344                        }
3345                    } else {
3346                        let filtered = crate::ui::iam::filtered_iam_users(self);
3347                        if !filtered.is_empty() {
3348                            self.iam_state.users.next_item(filtered.len());
3349                        }
3350                    }
3351                } else if self.current_service == Service::IamRoles {
3352                    if self.iam_state.current_role.is_some() {
3353                        if self.iam_state.role_tab == RoleTab::TrustRelationships {
3354                            let lines = self.iam_state.trust_policy_document.lines().count();
3355                            let max_scroll = lines.saturating_sub(1);
3356                            self.iam_state.trust_policy_scroll =
3357                                (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
3358                        } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
3359                            self.iam_state.revoke_sessions_scroll =
3360                                (self.iam_state.revoke_sessions_scroll + 1).min(19);
3361                        } else if self.iam_state.role_tab == RoleTab::Tags {
3362                            let filtered = crate::ui::iam::filtered_tags(self);
3363                            if !filtered.is_empty() {
3364                                self.iam_state.tags.next_item(filtered.len());
3365                            }
3366                        } else if self.iam_state.role_tab == RoleTab::LastAccessed {
3367                            let filtered = crate::ui::iam::filtered_last_accessed(self);
3368                            if !filtered.is_empty() {
3369                                self.iam_state
3370                                    .last_accessed_services
3371                                    .next_item(filtered.len());
3372                            }
3373                        } else {
3374                            let filtered = crate::ui::iam::filtered_iam_policies(self);
3375                            if !filtered.is_empty() {
3376                                self.iam_state.policies.next_item(filtered.len());
3377                            }
3378                        }
3379                    } else {
3380                        let filtered = crate::ui::iam::filtered_iam_roles(self);
3381                        if !filtered.is_empty() {
3382                            self.iam_state.roles.next_item(filtered.len());
3383                        }
3384                    }
3385                } else if self.current_service == Service::IamUserGroups {
3386                    if self.iam_state.current_group.is_some() {
3387                        if self.iam_state.group_tab == GroupTab::Users {
3388                            let filtered: Vec<_> = self
3389                                .iam_state
3390                                .group_users
3391                                .items
3392                                .iter()
3393                                .filter(|u| {
3394                                    if self.iam_state.group_users.filter.is_empty() {
3395                                        true
3396                                    } else {
3397                                        u.user_name.to_lowercase().contains(
3398                                            &self.iam_state.group_users.filter.to_lowercase(),
3399                                        )
3400                                    }
3401                                })
3402                                .collect();
3403                            if !filtered.is_empty() {
3404                                self.iam_state.group_users.next_item(filtered.len());
3405                            }
3406                        } else if self.iam_state.group_tab == GroupTab::Permissions {
3407                            let filtered = crate::ui::iam::filtered_iam_policies(self);
3408                            if !filtered.is_empty() {
3409                                self.iam_state.policies.next_item(filtered.len());
3410                            }
3411                        } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
3412                            let filtered = crate::ui::iam::filtered_last_accessed(self);
3413                            if !filtered.is_empty() {
3414                                self.iam_state
3415                                    .last_accessed_services
3416                                    .next_item(filtered.len());
3417                            }
3418                        }
3419                    } else {
3420                        let filtered: Vec<_> = self
3421                            .iam_state
3422                            .groups
3423                            .items
3424                            .iter()
3425                            .filter(|g| {
3426                                if self.iam_state.groups.filter.is_empty() {
3427                                    true
3428                                } else {
3429                                    g.group_name
3430                                        .to_lowercase()
3431                                        .contains(&self.iam_state.groups.filter.to_lowercase())
3432                                }
3433                            })
3434                            .collect();
3435                        if !filtered.is_empty() {
3436                            self.iam_state.groups.next_item(filtered.len());
3437                        }
3438                    }
3439                }
3440            }
3441            _ => {}
3442        }
3443    }
3444
3445    fn prev_item(&mut self) {
3446        match self.mode {
3447            Mode::FilterInput => {
3448                if self.current_service == Service::CloudFormationStacks {
3449                    use crate::ui::cfn::STATUS_FILTER;
3450                    if self.cfn_state.input_focus == STATUS_FILTER {
3451                        self.cfn_state.status_filter = self.cfn_state.status_filter.prev();
3452                    }
3453                }
3454            }
3455            Mode::RegionPicker => {
3456                self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
3457            }
3458            Mode::ProfilePicker => {
3459                self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
3460            }
3461            Mode::SessionPicker => {
3462                self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
3463            }
3464            Mode::InsightsInput => {
3465                use crate::app::InsightsFocus;
3466                if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3467                    && self.insights_state.insights.show_dropdown
3468                    && !self.insights_state.insights.log_group_matches.is_empty()
3469                {
3470                    self.insights_state.insights.dropdown_selected = self
3471                        .insights_state
3472                        .insights
3473                        .dropdown_selected
3474                        .saturating_sub(1);
3475                }
3476            }
3477            Mode::ColumnSelector => {
3478                self.column_selector_index = self.column_selector_index.saturating_sub(1);
3479            }
3480            Mode::ServicePicker => {
3481                self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3482            }
3483            Mode::TabPicker => {
3484                self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
3485            }
3486            Mode::Normal => {
3487                if !self.service_selected {
3488                    self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3489                } else if self.current_service == Service::S3Buckets {
3490                    if self.s3_state.current_bucket.is_some() {
3491                        if self.s3_state.object_tab == S3ObjectTab::Properties {
3492                            self.s3_state.properties_scroll =
3493                                self.s3_state.properties_scroll.saturating_sub(1);
3494                        } else {
3495                            self.s3_state.selected_object =
3496                                self.s3_state.selected_object.saturating_sub(1);
3497
3498                            // Adjust scroll offset if selection goes above viewport
3499                            if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
3500                                self.s3_state.object_scroll_offset = self.s3_state.selected_object;
3501                            }
3502                        }
3503                    } else {
3504                        self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
3505
3506                        // Adjust scroll offset if selection goes above viewport
3507                        if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
3508                            self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
3509                        }
3510                    }
3511                } else if self.view_mode == ViewMode::InsightsResults {
3512                    if self.insights_state.insights.results_selected > 0 {
3513                        self.insights_state.insights.results_selected -= 1;
3514                    }
3515                } else if self.view_mode == ViewMode::PolicyView {
3516                    self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
3517                } else if self.view_mode == ViewMode::Events {
3518                    if self.log_groups_state.event_scroll_offset == 0 {
3519                        if self.log_groups_state.has_older_events {
3520                            self.log_groups_state.loading = true;
3521                        }
3522                        // Don't move if at position 0
3523                    } else {
3524                        self.log_groups_state.event_scroll_offset =
3525                            self.log_groups_state.event_scroll_offset.saturating_sub(1);
3526                    }
3527                } else if self.current_service == Service::CloudWatchLogGroups {
3528                    if self.view_mode == ViewMode::List {
3529                        self.log_groups_state.log_groups.prev_item();
3530                    } else if self.view_mode == ViewMode::Detail
3531                        && self.log_groups_state.selected_stream > 0
3532                    {
3533                        self.log_groups_state.selected_stream =
3534                            self.log_groups_state.selected_stream.saturating_sub(1);
3535                        self.log_groups_state.expanded_stream = None;
3536                    }
3537                } else if self.current_service == Service::CloudWatchAlarms {
3538                    self.alarms_state.table.prev_item();
3539                } else if self.current_service == Service::EcrRepositories {
3540                    if self.ecr_state.current_repository.is_some() {
3541                        self.ecr_state.images.prev_item();
3542                    } else {
3543                        self.ecr_state.repositories.prev_item();
3544                    }
3545                } else if self.current_service == Service::LambdaFunctions {
3546                    if self.lambda_state.current_function.is_some()
3547                        && self.lambda_state.detail_tab == LambdaDetailTab::Code
3548                    {
3549                        // Layer table navigation in Code tab
3550                        self.lambda_state.layer_selected =
3551                            self.lambda_state.layer_selected.saturating_sub(1);
3552                    } else if self.lambda_state.current_function.is_some()
3553                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3554                    {
3555                        self.lambda_state.version_table.prev_item();
3556                    } else if self.lambda_state.current_function.is_some()
3557                        && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3558                            || (self.lambda_state.current_version.is_some()
3559                                && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
3560                    {
3561                        self.lambda_state.alias_table.prev_item();
3562                    } else if self.lambda_state.current_function.is_none() {
3563                        self.lambda_state.table.prev_item();
3564                    }
3565                } else if self.current_service == Service::LambdaApplications {
3566                    if self.lambda_application_state.current_application.is_some()
3567                        && self.lambda_application_state.detail_tab
3568                            == crate::ui::lambda::ApplicationDetailTab::Overview
3569                    {
3570                        self.lambda_application_state.resources.selected = self
3571                            .lambda_application_state
3572                            .resources
3573                            .selected
3574                            .saturating_sub(1);
3575                    } else if self.lambda_application_state.current_application.is_some()
3576                        && self.lambda_application_state.detail_tab
3577                            == crate::ui::lambda::ApplicationDetailTab::Deployments
3578                    {
3579                        self.lambda_application_state.deployments.selected = self
3580                            .lambda_application_state
3581                            .deployments
3582                            .selected
3583                            .saturating_sub(1);
3584                    } else {
3585                        self.lambda_application_state.table.selected = self
3586                            .lambda_application_state
3587                            .table
3588                            .selected
3589                            .saturating_sub(1);
3590                        self.lambda_application_state.table.snap_to_page();
3591                    }
3592                } else if self.current_service == Service::CloudFormationStacks {
3593                    self.cfn_state.table.prev_item();
3594                } else if self.current_service == Service::IamUsers {
3595                    self.iam_state.users.prev_item();
3596                } else if self.current_service == Service::IamRoles {
3597                    if self.iam_state.current_role.is_some() {
3598                        if self.iam_state.role_tab == RoleTab::TrustRelationships {
3599                            self.iam_state.trust_policy_scroll =
3600                                self.iam_state.trust_policy_scroll.saturating_sub(1);
3601                        } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
3602                            self.iam_state.revoke_sessions_scroll =
3603                                self.iam_state.revoke_sessions_scroll.saturating_sub(1);
3604                        } else if self.iam_state.role_tab == RoleTab::Tags {
3605                            self.iam_state.tags.prev_item();
3606                        } else if self.iam_state.role_tab == RoleTab::LastAccessed {
3607                            self.iam_state.last_accessed_services.prev_item();
3608                        } else {
3609                            self.iam_state.policies.prev_item();
3610                        }
3611                    } else {
3612                        self.iam_state.roles.prev_item();
3613                    }
3614                } else if self.current_service == Service::IamUserGroups {
3615                    if self.iam_state.current_group.is_some() {
3616                        if self.iam_state.group_tab == GroupTab::Users {
3617                            self.iam_state.group_users.prev_item();
3618                        } else if self.iam_state.group_tab == GroupTab::Permissions {
3619                            self.iam_state.policies.prev_item();
3620                        } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
3621                            self.iam_state.last_accessed_services.prev_item();
3622                        }
3623                    } else {
3624                        self.iam_state.groups.prev_item();
3625                    }
3626                }
3627            }
3628            _ => {}
3629        }
3630    }
3631
3632    fn page_down(&mut self) {
3633        if self.mode == Mode::FilterInput && self.current_service == Service::CloudFormationStacks {
3634            use crate::ui::cfn::filtered_cloudformation_stacks;
3635            let page_size = self.cfn_state.table.page_size.value();
3636            let filtered_count = filtered_cloudformation_stacks(self).len();
3637            self.cfn_state.input_focus.handle_page_down(
3638                &mut self.cfn_state.table.selected,
3639                &mut self.cfn_state.table.scroll_offset,
3640                page_size,
3641                filtered_count,
3642            );
3643        } else if self.mode == Mode::FilterInput
3644            && self.current_service == Service::IamRoles
3645            && self.iam_state.current_role.is_none()
3646        {
3647            let page_size = self.iam_state.roles.page_size.value();
3648            let filtered_count = crate::ui::iam::filtered_iam_roles(self).len();
3649            self.iam_state.role_input_focus.handle_page_down(
3650                &mut self.iam_state.roles.selected,
3651                &mut self.iam_state.roles.scroll_offset,
3652                page_size,
3653                filtered_count,
3654            );
3655        } else if self.mode == Mode::FilterInput
3656            && self.current_service == Service::CloudWatchAlarms
3657        {
3658            let page_size = self.alarms_state.table.page_size.value();
3659            let filtered_count = self.alarms_state.table.items.len();
3660            self.alarms_state.input_focus.handle_page_down(
3661                &mut self.alarms_state.table.selected,
3662                &mut self.alarms_state.table.scroll_offset,
3663                page_size,
3664                filtered_count,
3665            );
3666        } else if self.mode == Mode::FilterInput
3667            && self.current_service == Service::CloudWatchLogGroups
3668        {
3669            if self.view_mode == ViewMode::List {
3670                // Log groups list pagination
3671                let filtered = self.filtered_log_groups();
3672                let page_size = self.log_groups_state.log_groups.page_size.value();
3673                let filtered_count = filtered.len();
3674                self.log_groups_state.input_focus.handle_page_down(
3675                    &mut self.log_groups_state.log_groups.selected,
3676                    &mut self.log_groups_state.log_groups.scroll_offset,
3677                    page_size,
3678                    filtered_count,
3679                );
3680            } else {
3681                // Log streams pagination
3682                let filtered = self.filtered_log_streams();
3683                let page_size = 20;
3684                let filtered_count = filtered.len();
3685                self.log_groups_state.input_focus.handle_page_down(
3686                    &mut self.log_groups_state.selected_stream,
3687                    &mut self.log_groups_state.stream_page,
3688                    page_size,
3689                    filtered_count,
3690                );
3691                self.log_groups_state.expanded_stream = None;
3692            }
3693        } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
3694        {
3695            if self.lambda_state.current_function.is_some()
3696                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3697                && self.lambda_state.version_input_focus == InputFocus::Pagination
3698            {
3699                let page_size = self.lambda_state.version_table.page_size.value();
3700                let filtered_count: usize = self
3701                    .lambda_state
3702                    .version_table
3703                    .items
3704                    .iter()
3705                    .filter(|v| {
3706                        self.lambda_state.version_table.filter.is_empty()
3707                            || v.version
3708                                .to_lowercase()
3709                                .contains(&self.lambda_state.version_table.filter.to_lowercase())
3710                            || v.aliases
3711                                .to_lowercase()
3712                                .contains(&self.lambda_state.version_table.filter.to_lowercase())
3713                            || v.description
3714                                .to_lowercase()
3715                                .contains(&self.lambda_state.version_table.filter.to_lowercase())
3716                    })
3717                    .count();
3718                let target = self.lambda_state.version_table.selected + page_size;
3719                self.lambda_state.version_table.selected =
3720                    target.min(filtered_count.saturating_sub(1));
3721            } else if self.lambda_state.current_function.is_some()
3722                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3723                    || (self.lambda_state.current_version.is_some()
3724                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
3725                && self.lambda_state.alias_input_focus == InputFocus::Pagination
3726            {
3727                let page_size = self.lambda_state.alias_table.page_size.value();
3728                let version_filter = self.lambda_state.current_version.clone();
3729                let filtered_count = self
3730                    .lambda_state
3731                    .alias_table
3732                    .items
3733                    .iter()
3734                    .filter(|a| {
3735                        (version_filter.is_none()
3736                            || a.versions.contains(version_filter.as_ref().unwrap()))
3737                            && (self.lambda_state.alias_table.filter.is_empty()
3738                                || a.name
3739                                    .to_lowercase()
3740                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
3741                                || a.versions
3742                                    .to_lowercase()
3743                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
3744                                || a.description
3745                                    .to_lowercase()
3746                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase()))
3747                    })
3748                    .count();
3749                let target = self.lambda_state.alias_table.selected + page_size;
3750                self.lambda_state.alias_table.selected =
3751                    target.min(filtered_count.saturating_sub(1));
3752            } else if self.lambda_state.current_function.is_none() {
3753                let page_size = self.lambda_state.table.page_size.value();
3754                let filtered_count = crate::ui::lambda::filtered_lambda_functions(self).len();
3755                self.lambda_state.input_focus.handle_page_down(
3756                    &mut self.lambda_state.table.selected,
3757                    &mut self.lambda_state.table.scroll_offset,
3758                    page_size,
3759                    filtered_count,
3760                );
3761            }
3762        } else if self.mode == Mode::FilterInput
3763            && self.current_service == Service::EcrRepositories
3764            && self.ecr_state.current_repository.is_none()
3765            && self.ecr_state.input_focus == InputFocus::Filter
3766        {
3767            // When input is focused, allow table scrolling
3768            let filtered = self.filtered_ecr_repositories();
3769            self.ecr_state.repositories.page_down(filtered.len());
3770        } else if self.mode == Mode::FilterInput
3771            && self.current_service == Service::EcrRepositories
3772            && self.ecr_state.current_repository.is_none()
3773        {
3774            let page_size = self.ecr_state.repositories.page_size.value();
3775            let filtered_count = self.filtered_ecr_repositories().len();
3776            self.ecr_state.input_focus.handle_page_down(
3777                &mut self.ecr_state.repositories.selected,
3778                &mut self.ecr_state.repositories.scroll_offset,
3779                page_size,
3780                filtered_count,
3781            );
3782        } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
3783            let page_size = self.iam_state.policies.page_size.value();
3784            let filtered_count = crate::ui::iam::filtered_iam_policies(self).len();
3785            self.iam_state.policy_input_focus.handle_page_down(
3786                &mut self.iam_state.policies.selected,
3787                &mut self.iam_state.policies.scroll_offset,
3788                page_size,
3789                filtered_count,
3790            );
3791        } else if self.view_mode == ViewMode::PolicyView {
3792            let lines = self.iam_state.policy_document.lines().count();
3793            let max_scroll = lines.saturating_sub(1);
3794            self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
3795        } else if self.current_service == Service::IamRoles
3796            && self.iam_state.current_role.is_some()
3797            && self.iam_state.role_tab == RoleTab::TrustRelationships
3798        {
3799            let lines = self.iam_state.trust_policy_document.lines().count();
3800            let max_scroll = lines.saturating_sub(1);
3801            self.iam_state.trust_policy_scroll =
3802                (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
3803        } else if self.current_service == Service::IamRoles
3804            && self.iam_state.current_role.is_some()
3805            && self.iam_state.role_tab == RoleTab::RevokeSessions
3806        {
3807            self.iam_state.revoke_sessions_scroll =
3808                (self.iam_state.revoke_sessions_scroll + 10).min(19);
3809        } else if self.mode == Mode::Normal {
3810            if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
3811            {
3812                let total_rows = self.calculate_total_bucket_rows();
3813                self.s3_state.selected_row = self
3814                    .s3_state
3815                    .selected_row
3816                    .saturating_add(10)
3817                    .min(total_rows.saturating_sub(1));
3818
3819                // Adjust scroll offset if selection goes below viewport
3820                let visible_rows = self.s3_state.bucket_visible_rows.get();
3821                if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
3822                    self.s3_state.bucket_scroll_offset =
3823                        self.s3_state.selected_row - visible_rows + 1;
3824                }
3825            } else if self.current_service == Service::S3Buckets
3826                && self.s3_state.current_bucket.is_some()
3827            {
3828                let total_rows = self.calculate_total_object_rows();
3829                self.s3_state.selected_object = self
3830                    .s3_state
3831                    .selected_object
3832                    .saturating_add(10)
3833                    .min(total_rows.saturating_sub(1));
3834
3835                // Adjust scroll offset if selection goes below viewport
3836                let visible_rows = self.s3_state.object_visible_rows.get();
3837                if self.s3_state.selected_object
3838                    >= self.s3_state.object_scroll_offset + visible_rows
3839                {
3840                    self.s3_state.object_scroll_offset =
3841                        self.s3_state.selected_object - visible_rows + 1;
3842                }
3843            } else if self.current_service == Service::CloudWatchLogGroups
3844                && self.view_mode == ViewMode::List
3845            {
3846                let filtered = self.filtered_log_groups();
3847                self.log_groups_state.log_groups.page_down(filtered.len());
3848            } else if self.current_service == Service::CloudWatchLogGroups
3849                && self.view_mode == ViewMode::Detail
3850            {
3851                let len = self.filtered_log_streams().len();
3852                nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
3853            } else if self.view_mode == ViewMode::Events {
3854                let max = self.log_groups_state.log_events.len();
3855                nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
3856            } else if self.view_mode == ViewMode::InsightsResults {
3857                let max = self.insights_state.insights.query_results.len();
3858                nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
3859            } else if self.current_service == Service::CloudWatchAlarms {
3860                let filtered = match self.alarms_state.alarm_tab {
3861                    AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
3862                    AlarmTab::InAlarm => self
3863                        .alarms_state
3864                        .table
3865                        .items
3866                        .iter()
3867                        .filter(|a| a.state.to_uppercase() == "ALARM")
3868                        .count(),
3869                };
3870                if filtered > 0 {
3871                    self.alarms_state.table.page_down(filtered);
3872                }
3873            } else if self.current_service == Service::EcrRepositories {
3874                if self.ecr_state.current_repository.is_some() {
3875                    let filtered = self.filtered_ecr_images();
3876                    self.ecr_state.images.page_down(filtered.len());
3877                } else {
3878                    let filtered = self.filtered_ecr_repositories();
3879                    self.ecr_state.repositories.page_down(filtered.len());
3880                }
3881            } else if self.current_service == Service::LambdaFunctions {
3882                let len = crate::ui::lambda::filtered_lambda_functions(self).len();
3883                self.lambda_state.table.page_down(len);
3884            } else if self.current_service == Service::LambdaApplications {
3885                let len = crate::ui::lambda::filtered_lambda_applications(self).len();
3886                self.lambda_application_state.table.page_down(len);
3887            } else if self.current_service == Service::CloudFormationStacks {
3888                let filtered = self.filtered_cloudformation_stacks();
3889                self.cfn_state.table.page_down(filtered.len());
3890            } else if self.current_service == Service::IamUsers {
3891                let len = crate::ui::iam::filtered_iam_users(self).len();
3892                nav_page_down(&mut self.iam_state.users.selected, len, 10);
3893            } else if self.current_service == Service::IamRoles {
3894                if self.iam_state.current_role.is_some() {
3895                    let filtered = crate::ui::iam::filtered_iam_policies(self);
3896                    if !filtered.is_empty() {
3897                        self.iam_state.policies.page_down(filtered.len());
3898                    }
3899                } else {
3900                    let filtered = crate::ui::iam::filtered_iam_roles(self);
3901                    self.iam_state.roles.page_down(filtered.len());
3902                }
3903            } else if self.current_service == Service::IamUserGroups {
3904                if self.iam_state.current_group.is_some() {
3905                    if self.iam_state.group_tab == GroupTab::Users {
3906                        let filtered: Vec<_> = self
3907                            .iam_state
3908                            .group_users
3909                            .items
3910                            .iter()
3911                            .filter(|u| {
3912                                if self.iam_state.group_users.filter.is_empty() {
3913                                    true
3914                                } else {
3915                                    u.user_name
3916                                        .to_lowercase()
3917                                        .contains(&self.iam_state.group_users.filter.to_lowercase())
3918                                }
3919                            })
3920                            .collect();
3921                        if !filtered.is_empty() {
3922                            self.iam_state.group_users.page_down(filtered.len());
3923                        }
3924                    } else if self.iam_state.group_tab == GroupTab::Permissions {
3925                        let filtered = crate::ui::iam::filtered_iam_policies(self);
3926                        if !filtered.is_empty() {
3927                            self.iam_state.policies.page_down(filtered.len());
3928                        }
3929                    } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
3930                        let filtered = crate::ui::iam::filtered_last_accessed(self);
3931                        if !filtered.is_empty() {
3932                            self.iam_state
3933                                .last_accessed_services
3934                                .page_down(filtered.len());
3935                        }
3936                    }
3937                } else {
3938                    let filtered: Vec<_> = self
3939                        .iam_state
3940                        .groups
3941                        .items
3942                        .iter()
3943                        .filter(|g| {
3944                            if self.iam_state.groups.filter.is_empty() {
3945                                true
3946                            } else {
3947                                g.group_name
3948                                    .to_lowercase()
3949                                    .contains(&self.iam_state.groups.filter.to_lowercase())
3950                            }
3951                        })
3952                        .collect();
3953                    if !filtered.is_empty() {
3954                        self.iam_state.groups.page_down(filtered.len());
3955                    }
3956                }
3957            }
3958        }
3959    }
3960
3961    fn page_up(&mut self) {
3962        if self.mode == Mode::FilterInput && self.current_service == Service::CloudFormationStacks {
3963            let page_size = self.cfn_state.table.page_size.value();
3964            self.cfn_state.input_focus.handle_page_up(
3965                &mut self.cfn_state.table.selected,
3966                &mut self.cfn_state.table.scroll_offset,
3967                page_size,
3968            );
3969        } else if self.mode == Mode::FilterInput
3970            && self.current_service == Service::IamRoles
3971            && self.iam_state.current_role.is_none()
3972        {
3973            let page_size = self.iam_state.roles.page_size.value();
3974            self.iam_state.role_input_focus.handle_page_up(
3975                &mut self.iam_state.roles.selected,
3976                &mut self.iam_state.roles.scroll_offset,
3977                page_size,
3978            );
3979        } else if self.mode == Mode::FilterInput
3980            && self.current_service == Service::CloudWatchAlarms
3981        {
3982            let page_size = self.alarms_state.table.page_size.value();
3983            self.alarms_state.input_focus.handle_page_up(
3984                &mut self.alarms_state.table.selected,
3985                &mut self.alarms_state.table.scroll_offset,
3986                page_size,
3987            );
3988        } else if self.mode == Mode::FilterInput
3989            && self.current_service == Service::CloudWatchLogGroups
3990        {
3991            if self.view_mode == ViewMode::List {
3992                // Log groups list pagination
3993                let page_size = self.log_groups_state.log_groups.page_size.value();
3994                self.log_groups_state.input_focus.handle_page_up(
3995                    &mut self.log_groups_state.log_groups.selected,
3996                    &mut self.log_groups_state.log_groups.scroll_offset,
3997                    page_size,
3998                );
3999            } else {
4000                // Log streams pagination
4001                let page_size = 20;
4002                self.log_groups_state.input_focus.handle_page_up(
4003                    &mut self.log_groups_state.selected_stream,
4004                    &mut self.log_groups_state.stream_page,
4005                    page_size,
4006                );
4007                self.log_groups_state.expanded_stream = None;
4008            }
4009        } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
4010        {
4011            if self.lambda_state.current_function.is_some()
4012                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4013                && self.lambda_state.version_input_focus == InputFocus::Pagination
4014            {
4015                let page_size = self.lambda_state.version_table.page_size.value();
4016                self.lambda_state.version_table.selected = self
4017                    .lambda_state
4018                    .version_table
4019                    .selected
4020                    .saturating_sub(page_size);
4021            } else if self.lambda_state.current_function.is_some()
4022                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4023                    || (self.lambda_state.current_version.is_some()
4024                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4025                && self.lambda_state.alias_input_focus == InputFocus::Pagination
4026            {
4027                let page_size = self.lambda_state.alias_table.page_size.value();
4028                self.lambda_state.alias_table.selected = self
4029                    .lambda_state
4030                    .alias_table
4031                    .selected
4032                    .saturating_sub(page_size);
4033            } else if self.lambda_state.current_function.is_none() {
4034                let page_size = self.lambda_state.table.page_size.value();
4035                self.lambda_state.input_focus.handle_page_up(
4036                    &mut self.lambda_state.table.selected,
4037                    &mut self.lambda_state.table.scroll_offset,
4038                    page_size,
4039                );
4040            }
4041        } else if self.mode == Mode::FilterInput
4042            && self.current_service == Service::EcrRepositories
4043            && self.ecr_state.current_repository.is_none()
4044            && self.ecr_state.input_focus == InputFocus::Filter
4045        {
4046            // When input is focused, allow table scrolling
4047            self.ecr_state.repositories.page_up();
4048        } else if self.mode == Mode::FilterInput
4049            && self.current_service == Service::EcrRepositories
4050            && self.ecr_state.current_repository.is_none()
4051        {
4052            let page_size = self.ecr_state.repositories.page_size.value();
4053            self.ecr_state.input_focus.handle_page_up(
4054                &mut self.ecr_state.repositories.selected,
4055                &mut self.ecr_state.repositories.scroll_offset,
4056                page_size,
4057            );
4058        } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
4059            let page_size = self.iam_state.policies.page_size.value();
4060            self.iam_state.policy_input_focus.handle_page_up(
4061                &mut self.iam_state.policies.selected,
4062                &mut self.iam_state.policies.scroll_offset,
4063                page_size,
4064            );
4065        } else if self.view_mode == ViewMode::PolicyView {
4066            self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
4067        } else if self.current_service == Service::IamRoles
4068            && self.iam_state.current_role.is_some()
4069            && self.iam_state.role_tab == RoleTab::TrustRelationships
4070        {
4071            self.iam_state.trust_policy_scroll =
4072                self.iam_state.trust_policy_scroll.saturating_sub(10);
4073        } else if self.current_service == Service::IamRoles
4074            && self.iam_state.current_role.is_some()
4075            && self.iam_state.role_tab == RoleTab::RevokeSessions
4076        {
4077            self.iam_state.revoke_sessions_scroll =
4078                self.iam_state.revoke_sessions_scroll.saturating_sub(10);
4079        } else if self.mode == Mode::Normal {
4080            if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
4081            {
4082                self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
4083
4084                // Adjust scroll offset if selection goes above viewport
4085                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
4086                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4087                }
4088            } else if self.current_service == Service::S3Buckets
4089                && self.s3_state.current_bucket.is_some()
4090            {
4091                self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
4092
4093                // Adjust scroll offset if selection goes above viewport
4094                if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
4095                    self.s3_state.object_scroll_offset = self.s3_state.selected_object;
4096                }
4097            } else if self.current_service == Service::CloudWatchLogGroups
4098                && self.view_mode == ViewMode::List
4099            {
4100                self.log_groups_state.log_groups.page_up();
4101            } else if self.current_service == Service::CloudWatchLogGroups
4102                && self.view_mode == ViewMode::Detail
4103            {
4104                self.log_groups_state.selected_stream =
4105                    self.log_groups_state.selected_stream.saturating_sub(10);
4106            } else if self.view_mode == ViewMode::Events {
4107                if self.log_groups_state.event_scroll_offset < 10
4108                    && self.log_groups_state.has_older_events
4109                {
4110                    self.log_groups_state.loading = true;
4111                }
4112                self.log_groups_state.event_scroll_offset =
4113                    self.log_groups_state.event_scroll_offset.saturating_sub(10);
4114            } else if self.view_mode == ViewMode::InsightsResults {
4115                self.insights_state.insights.results_selected = self
4116                    .insights_state
4117                    .insights
4118                    .results_selected
4119                    .saturating_sub(10);
4120            } else if self.current_service == Service::CloudWatchAlarms {
4121                self.alarms_state.table.page_up();
4122            } else if self.current_service == Service::EcrRepositories {
4123                if self.ecr_state.current_repository.is_some() {
4124                    self.ecr_state.images.page_up();
4125                } else {
4126                    self.ecr_state.repositories.page_up();
4127                }
4128            } else if self.current_service == Service::LambdaFunctions {
4129                self.lambda_state.table.page_up();
4130            } else if self.current_service == Service::LambdaApplications {
4131                self.lambda_application_state.table.page_up();
4132            } else if self.current_service == Service::CloudFormationStacks {
4133                self.cfn_state.table.page_up();
4134            } else if self.current_service == Service::IamUsers {
4135                self.iam_state.users.page_up();
4136            } else if self.current_service == Service::IamRoles {
4137                if self.iam_state.current_role.is_some() {
4138                    self.iam_state.policies.page_up();
4139                } else {
4140                    self.iam_state.roles.page_up();
4141                }
4142            }
4143        }
4144    }
4145
4146    fn next_pane(&mut self) {
4147        if self.current_service == Service::S3Buckets {
4148            if self.s3_state.current_bucket.is_some() {
4149                // In objects view - expand prefix and trigger preview load
4150                // Map visual index to actual object (including nested items)
4151                let mut visual_idx = 0;
4152                let mut found_obj: Option<S3Object> = None;
4153
4154                // Helper to recursively check nested items
4155                fn check_nested(
4156                    obj: &S3Object,
4157                    visual_idx: &mut usize,
4158                    target_idx: usize,
4159                    expanded_prefixes: &std::collections::HashSet<String>,
4160                    prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
4161                    found_obj: &mut Option<S3Object>,
4162                ) {
4163                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4164                        if let Some(preview) = prefix_preview.get(&obj.key) {
4165                            for nested_obj in preview {
4166                                if *visual_idx == target_idx {
4167                                    *found_obj = Some(nested_obj.clone());
4168                                    return;
4169                                }
4170                                *visual_idx += 1;
4171
4172                                // Recursively check deeper levels
4173                                check_nested(
4174                                    nested_obj,
4175                                    visual_idx,
4176                                    target_idx,
4177                                    expanded_prefixes,
4178                                    prefix_preview,
4179                                    found_obj,
4180                                );
4181                                if found_obj.is_some() {
4182                                    return;
4183                                }
4184                            }
4185                        } else {
4186                            // Loading row
4187                            *visual_idx += 1;
4188                        }
4189                    }
4190                }
4191
4192                for obj in &self.s3_state.objects {
4193                    if visual_idx == self.s3_state.selected_object {
4194                        found_obj = Some(obj.clone());
4195                        break;
4196                    }
4197                    visual_idx += 1;
4198
4199                    // Check nested items recursively
4200                    check_nested(
4201                        obj,
4202                        &mut visual_idx,
4203                        self.s3_state.selected_object,
4204                        &self.s3_state.expanded_prefixes,
4205                        &self.s3_state.prefix_preview,
4206                        &mut found_obj,
4207                    );
4208                    if found_obj.is_some() {
4209                        break;
4210                    }
4211                }
4212
4213                if let Some(obj) = found_obj {
4214                    if obj.is_prefix {
4215                        if !self.s3_state.expanded_prefixes.contains(&obj.key) {
4216                            self.s3_state.expanded_prefixes.insert(obj.key.clone());
4217                            // Trigger preview load if not already cached
4218                            if !self.s3_state.prefix_preview.contains_key(&obj.key) {
4219                                self.s3_state.buckets.loading = true;
4220                            }
4221                        }
4222                        // Move to first child if expanded and has children
4223                        if self.s3_state.expanded_prefixes.contains(&obj.key) {
4224                            if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
4225                                if !preview.is_empty() {
4226                                    self.s3_state.selected_object += 1;
4227                                }
4228                            }
4229                        }
4230                    }
4231                }
4232            } else {
4233                // In bucket list - find which bucket/prefix the selected row corresponds to
4234                let mut row_idx = 0;
4235                let mut found = false;
4236                for bucket in &self.s3_state.buckets.items {
4237                    if row_idx == self.s3_state.selected_row {
4238                        // Selected row is a bucket - expand and move to first child
4239                        if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
4240                            self.s3_state.expanded_prefixes.insert(bucket.name.clone());
4241                            if !self.s3_state.bucket_preview.contains_key(&bucket.name)
4242                                && !self.s3_state.bucket_errors.contains_key(&bucket.name)
4243                            {
4244                                self.s3_state.buckets.loading = true;
4245                            }
4246                        }
4247                        // Move to first child if expanded and has children
4248                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
4249                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
4250                                if !preview.is_empty() {
4251                                    self.s3_state.selected_row = row_idx + 1;
4252                                }
4253                            }
4254                        }
4255                        break;
4256                    }
4257                    row_idx += 1;
4258
4259                    // Skip error rows - they're not selectable
4260                    if self.s3_state.bucket_errors.contains_key(&bucket.name)
4261                        && self.s3_state.expanded_prefixes.contains(&bucket.name)
4262                    {
4263                        continue;
4264                    }
4265
4266                    if self.s3_state.expanded_prefixes.contains(&bucket.name) {
4267                        if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
4268                            // Recursive function to check nested items at any depth
4269                            #[allow(clippy::too_many_arguments)]
4270                            fn check_nested_expansion(
4271                                objects: &[crate::s3::Object],
4272                                row_idx: &mut usize,
4273                                target_row: usize,
4274                                expanded_prefixes: &mut std::collections::HashSet<String>,
4275                                prefix_preview: &std::collections::HashMap<
4276                                    String,
4277                                    Vec<crate::s3::Object>,
4278                                >,
4279                                found: &mut bool,
4280                                loading: &mut bool,
4281                                selected_row: &mut usize,
4282                            ) {
4283                                for obj in objects {
4284                                    if *row_idx == target_row {
4285                                        // Selected this item - expand and move to first child
4286                                        if obj.is_prefix {
4287                                            if !expanded_prefixes.contains(&obj.key) {
4288                                                expanded_prefixes.insert(obj.key.clone());
4289                                                if !prefix_preview.contains_key(&obj.key) {
4290                                                    *loading = true;
4291                                                }
4292                                            }
4293                                            // Move to first child if expanded and has children
4294                                            if expanded_prefixes.contains(&obj.key) {
4295                                                if let Some(preview) = prefix_preview.get(&obj.key)
4296                                                {
4297                                                    if !preview.is_empty() {
4298                                                        *selected_row = *row_idx + 1;
4299                                                    }
4300                                                }
4301                                            }
4302                                        }
4303                                        *found = true;
4304                                        return;
4305                                    }
4306                                    *row_idx += 1;
4307
4308                                    // Recursively check nested items if expanded
4309                                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4310                                        if let Some(nested) = prefix_preview.get(&obj.key) {
4311                                            check_nested_expansion(
4312                                                nested,
4313                                                row_idx,
4314                                                target_row,
4315                                                expanded_prefixes,
4316                                                prefix_preview,
4317                                                found,
4318                                                loading,
4319                                                selected_row,
4320                                            );
4321                                            if *found {
4322                                                return;
4323                                            }
4324                                        } else {
4325                                            *row_idx += 1; // Loading row
4326                                        }
4327                                    }
4328                                }
4329                            }
4330
4331                            check_nested_expansion(
4332                                preview,
4333                                &mut row_idx,
4334                                self.s3_state.selected_row,
4335                                &mut self.s3_state.expanded_prefixes,
4336                                &self.s3_state.prefix_preview,
4337                                &mut found,
4338                                &mut self.s3_state.buckets.loading,
4339                                &mut self.s3_state.selected_row,
4340                            );
4341                            if found || row_idx > self.s3_state.selected_row {
4342                                break;
4343                            }
4344                        } else {
4345                            row_idx += 1;
4346                            if row_idx > self.s3_state.selected_row {
4347                                break;
4348                            }
4349                        }
4350                    }
4351                    if found {
4352                        break;
4353                    }
4354                }
4355            }
4356        } else if self.view_mode == ViewMode::InsightsResults {
4357            // Right arrow scrolls horizontally by 1 column
4358            let max_cols = self
4359                .insights_state
4360                .insights
4361                .query_results
4362                .first()
4363                .map(|r| r.len())
4364                .unwrap_or(0);
4365            if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
4366                self.insights_state.insights.results_horizontal_scroll += 1;
4367            }
4368        } else if self.current_service == Service::CloudWatchLogGroups
4369            && self.view_mode == ViewMode::List
4370        {
4371            // Expand selected log group
4372            if self.log_groups_state.log_groups.expanded_item
4373                != Some(self.log_groups_state.log_groups.selected)
4374            {
4375                self.log_groups_state.log_groups.expanded_item =
4376                    Some(self.log_groups_state.log_groups.selected);
4377            }
4378        } else if self.current_service == Service::CloudWatchLogGroups
4379            && self.view_mode == ViewMode::Detail
4380        {
4381            // Expand selected log stream
4382            if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
4383            {
4384                self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
4385            }
4386        } else if self.view_mode == ViewMode::Events {
4387            // Only scroll if there are hidden columns
4388            // Expand selected event
4389            if self.log_groups_state.expanded_event
4390                != Some(self.log_groups_state.event_scroll_offset)
4391            {
4392                self.log_groups_state.expanded_event =
4393                    Some(self.log_groups_state.event_scroll_offset);
4394            }
4395        } else if self.current_service == Service::CloudWatchAlarms {
4396            // Expand selected alarm
4397            if !self.alarms_state.table.is_expanded() {
4398                self.alarms_state.table.toggle_expand();
4399            }
4400        } else if self.current_service == Service::EcrRepositories {
4401            if self.ecr_state.current_repository.is_some() {
4402                // In images view - expand selected image
4403                self.ecr_state.images.toggle_expand();
4404            } else {
4405                // In repositories view - expand selected repository
4406                self.ecr_state.repositories.toggle_expand();
4407            }
4408        } else if self.current_service == Service::LambdaFunctions {
4409            if self.lambda_state.current_function.is_some()
4410                && self.lambda_state.detail_tab == LambdaDetailTab::Code
4411            {
4412                // Expand selected layer
4413                if self.lambda_state.layer_expanded != Some(self.lambda_state.layer_selected) {
4414                    self.lambda_state.layer_expanded = Some(self.lambda_state.layer_selected);
4415                } else {
4416                    self.lambda_state.layer_expanded = None;
4417                }
4418            } else if self.lambda_state.current_function.is_some()
4419                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4420            {
4421                // Expand selected version
4422                self.lambda_state.version_table.toggle_expand();
4423            } else if self.lambda_state.current_function.is_some()
4424                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4425                    || (self.lambda_state.current_version.is_some()
4426                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4427            {
4428                // Expand selected alias
4429                self.lambda_state.alias_table.toggle_expand();
4430            } else if self.lambda_state.current_function.is_none() {
4431                // Expand selected function
4432                self.lambda_state.table.toggle_expand();
4433            }
4434        } else if self.current_service == Service::LambdaApplications {
4435            if self.lambda_application_state.current_application.is_some() {
4436                // In detail view - expand resource or deployment
4437                if self.lambda_application_state.detail_tab
4438                    == crate::ui::lambda::ApplicationDetailTab::Overview
4439                {
4440                    self.lambda_application_state.resources.toggle_expand();
4441                } else {
4442                    self.lambda_application_state.deployments.toggle_expand();
4443                }
4444            } else {
4445                // Expand selected application in list
4446                if self.lambda_application_state.table.expanded_item
4447                    != Some(self.lambda_application_state.table.selected)
4448                {
4449                    self.lambda_application_state.table.expanded_item =
4450                        Some(self.lambda_application_state.table.selected);
4451                }
4452            }
4453        } else if self.current_service == Service::CloudFormationStacks
4454            && self.cfn_state.current_stack.is_none()
4455        {
4456            self.cfn_state.table.toggle_expand();
4457        } else if self.current_service == Service::IamUsers {
4458            if self.iam_state.current_user.is_some() {
4459                if self.iam_state.user_tab == UserTab::Tags {
4460                    if self.iam_state.user_tags.expanded_item
4461                        != Some(self.iam_state.user_tags.selected)
4462                    {
4463                        self.iam_state.user_tags.expanded_item =
4464                            Some(self.iam_state.user_tags.selected);
4465                    }
4466                } else if self.iam_state.policies.expanded_item
4467                    != Some(self.iam_state.policies.selected)
4468                {
4469                    self.iam_state.policies.toggle_expand();
4470                }
4471            } else if !self.iam_state.users.is_expanded() {
4472                self.iam_state.users.toggle_expand();
4473            }
4474        } else if self.current_service == Service::IamRoles {
4475            if self.iam_state.current_role.is_some() {
4476                // Handle expansion based on current tab
4477                if self.iam_state.role_tab == RoleTab::Tags {
4478                    if !self.iam_state.tags.is_expanded() {
4479                        self.iam_state.tags.expand();
4480                    }
4481                } else if self.iam_state.role_tab == RoleTab::LastAccessed {
4482                    if !self.iam_state.last_accessed_services.is_expanded() {
4483                        self.iam_state.last_accessed_services.expand();
4484                    }
4485                } else if !self.iam_state.policies.is_expanded() {
4486                    self.iam_state.policies.expand();
4487                }
4488            } else if !self.iam_state.roles.is_expanded() {
4489                self.iam_state.roles.expand();
4490            }
4491        } else if self.current_service == Service::IamUserGroups {
4492            if self.iam_state.current_group.is_some() {
4493                if self.iam_state.group_tab == GroupTab::Users {
4494                    if !self.iam_state.group_users.is_expanded() {
4495                        self.iam_state.group_users.expand();
4496                    }
4497                } else if self.iam_state.group_tab == GroupTab::Permissions {
4498                    if !self.iam_state.policies.is_expanded() {
4499                        self.iam_state.policies.expand();
4500                    }
4501                } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
4502                    && !self.iam_state.last_accessed_services.is_expanded()
4503                {
4504                    self.iam_state.last_accessed_services.expand();
4505                }
4506            } else if !self.iam_state.groups.is_expanded() {
4507                self.iam_state.groups.expand();
4508            }
4509        }
4510    }
4511
4512    fn go_to_page(&mut self, page: usize) {
4513        if page == 0 {
4514            return;
4515        }
4516
4517        match self.current_service {
4518            Service::CloudWatchAlarms => {
4519                let alarm_page_size = self.alarms_state.table.page_size.value();
4520                let target = (page - 1) * alarm_page_size;
4521                let filtered_count = match self.alarms_state.alarm_tab {
4522                    AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4523                    AlarmTab::InAlarm => self
4524                        .alarms_state
4525                        .table
4526                        .items
4527                        .iter()
4528                        .filter(|a| a.state.to_uppercase() == "ALARM")
4529                        .count(),
4530                };
4531                let max_offset = filtered_count.saturating_sub(alarm_page_size);
4532                self.alarms_state.table.scroll_offset = target.min(max_offset);
4533                self.alarms_state.table.selected = self
4534                    .alarms_state
4535                    .table
4536                    .scroll_offset
4537                    .min(filtered_count.saturating_sub(1));
4538            }
4539            Service::CloudWatchLogGroups => match self.view_mode {
4540                ViewMode::Events => {
4541                    let page_size = 20;
4542                    let target = (page - 1) * page_size;
4543                    let max = self.log_groups_state.log_events.len().saturating_sub(1);
4544                    self.log_groups_state.event_scroll_offset = target.min(max);
4545                }
4546                ViewMode::Detail => {
4547                    let page_size = 20;
4548                    let target = (page - 1) * page_size;
4549                    let max = self.log_groups_state.log_streams.len().saturating_sub(1);
4550                    self.log_groups_state.selected_stream = target.min(max);
4551                }
4552                ViewMode::List => {
4553                    let total = self.log_groups_state.log_groups.items.len();
4554                    self.log_groups_state.log_groups.goto_page(page, total);
4555                }
4556                _ => {}
4557            },
4558            Service::EcrRepositories => {
4559                if self.ecr_state.current_repository.is_some() {
4560                    let filtered_count = self
4561                        .ecr_state
4562                        .images
4563                        .filtered(|img| {
4564                            self.ecr_state.images.filter.is_empty()
4565                                || img
4566                                    .tag
4567                                    .to_lowercase()
4568                                    .contains(&self.ecr_state.images.filter.to_lowercase())
4569                                || img
4570                                    .digest
4571                                    .to_lowercase()
4572                                    .contains(&self.ecr_state.images.filter.to_lowercase())
4573                        })
4574                        .len();
4575                    self.ecr_state.images.goto_page(page, filtered_count);
4576                } else {
4577                    let filtered_count = self
4578                        .ecr_state
4579                        .repositories
4580                        .filtered(|r| {
4581                            self.ecr_state.repositories.filter.is_empty()
4582                                || r.name
4583                                    .to_lowercase()
4584                                    .contains(&self.ecr_state.repositories.filter.to_lowercase())
4585                        })
4586                        .len();
4587                    self.ecr_state.repositories.goto_page(page, filtered_count);
4588                }
4589            }
4590            Service::S3Buckets => {
4591                if self.s3_state.current_bucket.is_some() {
4592                    let page_size = 50; // S3 objects use fixed page size
4593                    let target = (page - 1) * page_size;
4594                    let total_rows = self.calculate_total_object_rows();
4595                    let max = total_rows.saturating_sub(1);
4596                    self.s3_state.selected_object = target.min(max);
4597                } else {
4598                    let page_size = 50; // S3 buckets use fixed page size
4599                    let target = (page - 1) * page_size;
4600                    let total_rows = self.calculate_total_bucket_rows();
4601                    let max = total_rows.saturating_sub(1);
4602                    self.s3_state.selected_row = target.min(max);
4603                }
4604            }
4605            Service::LambdaFunctions => {
4606                if self.lambda_state.current_function.is_some()
4607                    && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4608                {
4609                    let filtered_count = self
4610                        .lambda_state
4611                        .version_table
4612                        .filtered(|v| {
4613                            self.lambda_state.version_table.filter.is_empty()
4614                                || v.version.to_lowercase().contains(
4615                                    &self.lambda_state.version_table.filter.to_lowercase(),
4616                                )
4617                                || v.aliases.to_lowercase().contains(
4618                                    &self.lambda_state.version_table.filter.to_lowercase(),
4619                                )
4620                                || v.description.to_lowercase().contains(
4621                                    &self.lambda_state.version_table.filter.to_lowercase(),
4622                                )
4623                        })
4624                        .len();
4625                    self.lambda_state
4626                        .version_table
4627                        .goto_page(page, filtered_count);
4628                } else {
4629                    let filtered_count = crate::ui::lambda::filtered_lambda_functions(self).len();
4630                    self.lambda_state.table.goto_page(page, filtered_count);
4631                }
4632            }
4633            Service::LambdaApplications => {
4634                let filtered_count = crate::ui::lambda::filtered_lambda_applications(self).len();
4635                self.lambda_application_state
4636                    .table
4637                    .goto_page(page, filtered_count);
4638            }
4639            Service::CloudFormationStacks => {
4640                let filtered_count = self.filtered_cloudformation_stacks().len();
4641                self.cfn_state.table.goto_page(page, filtered_count);
4642            }
4643            Service::IamUsers => {
4644                let filtered_count = crate::ui::iam::filtered_iam_users(self).len();
4645                self.iam_state.users.goto_page(page, filtered_count);
4646            }
4647            Service::IamRoles => {
4648                let filtered_count = crate::ui::iam::filtered_iam_roles(self).len();
4649                self.iam_state.roles.goto_page(page, filtered_count);
4650            }
4651            _ => {}
4652        }
4653    }
4654
4655    fn prev_pane(&mut self) {
4656        if self.current_service == Service::S3Buckets {
4657            if self.s3_state.current_bucket.is_some() {
4658                // In objects view - collapse prefix or jump to parent
4659                // Map visual index to actual object (including nested items)
4660                let mut visual_idx = 0;
4661                let mut found_obj: Option<S3Object> = None;
4662                let mut parent_idx: Option<usize> = None;
4663
4664                // Helper to recursively find object and its parent
4665                #[allow(clippy::too_many_arguments)]
4666                fn find_with_parent(
4667                    objects: &[S3Object],
4668                    visual_idx: &mut usize,
4669                    target_idx: usize,
4670                    expanded_prefixes: &std::collections::HashSet<String>,
4671                    prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
4672                    found_obj: &mut Option<S3Object>,
4673                    parent_idx: &mut Option<usize>,
4674                    current_parent: Option<usize>,
4675                ) {
4676                    for obj in objects {
4677                        if *visual_idx == target_idx {
4678                            *found_obj = Some(obj.clone());
4679                            *parent_idx = current_parent;
4680                            return;
4681                        }
4682                        let obj_idx = *visual_idx;
4683                        *visual_idx += 1;
4684
4685                        // Check nested items if expanded
4686                        if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4687                            if let Some(preview) = prefix_preview.get(&obj.key) {
4688                                find_with_parent(
4689                                    preview,
4690                                    visual_idx,
4691                                    target_idx,
4692                                    expanded_prefixes,
4693                                    prefix_preview,
4694                                    found_obj,
4695                                    parent_idx,
4696                                    Some(obj_idx),
4697                                );
4698                                if found_obj.is_some() {
4699                                    return;
4700                                }
4701                            }
4702                        }
4703                    }
4704                }
4705
4706                find_with_parent(
4707                    &self.s3_state.objects,
4708                    &mut visual_idx,
4709                    self.s3_state.selected_object,
4710                    &self.s3_state.expanded_prefixes,
4711                    &self.s3_state.prefix_preview,
4712                    &mut found_obj,
4713                    &mut parent_idx,
4714                    None,
4715                );
4716
4717                if let Some(obj) = found_obj {
4718                    if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
4719                        // Expanded: collapse it
4720                        self.s3_state.expanded_prefixes.remove(&obj.key);
4721                    } else if let Some(parent) = parent_idx {
4722                        // Already collapsed or not a prefix: jump to parent
4723                        self.s3_state.selected_object = parent;
4724                    }
4725                }
4726
4727                // Adjust scroll offset to keep selection visible
4728                let visible_rows = self.s3_state.object_visible_rows.get();
4729                if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
4730                    self.s3_state.object_scroll_offset = self.s3_state.selected_object;
4731                } else if self.s3_state.selected_object
4732                    >= self.s3_state.object_scroll_offset + visible_rows
4733                {
4734                    self.s3_state.object_scroll_offset = self
4735                        .s3_state
4736                        .selected_object
4737                        .saturating_sub(visible_rows - 1);
4738                }
4739            } else {
4740                // In bucket list - find which bucket/prefix the selected row corresponds to
4741                let mut row_idx = 0;
4742                for bucket in &self.s3_state.buckets.items {
4743                    if row_idx == self.s3_state.selected_row {
4744                        // Selected row is a bucket - collapse it
4745                        self.s3_state.expanded_prefixes.remove(&bucket.name);
4746                        break;
4747                    }
4748                    row_idx += 1;
4749                    if self.s3_state.expanded_prefixes.contains(&bucket.name) {
4750                        if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
4751                            // Recursive function to check nested items at any depth
4752                            #[allow(clippy::too_many_arguments)]
4753                            fn check_nested_collapse(
4754                                objects: &[crate::s3::Object],
4755                                row_idx: &mut usize,
4756                                target_row: usize,
4757                                expanded_prefixes: &mut std::collections::HashSet<String>,
4758                                prefix_preview: &std::collections::HashMap<
4759                                    String,
4760                                    Vec<crate::s3::Object>,
4761                                >,
4762                                found: &mut bool,
4763                                selected_row: &mut usize,
4764                                parent_row: usize,
4765                            ) {
4766                                for obj in objects {
4767                                    let current_row = *row_idx;
4768                                    if *row_idx == target_row {
4769                                        // Selected this item - collapse or jump to parent
4770                                        if obj.is_prefix {
4771                                            if expanded_prefixes.contains(&obj.key) {
4772                                                // Expanded: collapse it
4773                                                expanded_prefixes.remove(&obj.key);
4774                                            } else {
4775                                                // Already collapsed: jump to parent
4776                                                *selected_row = parent_row;
4777                                            }
4778                                        } else {
4779                                            // Not a prefix: jump to parent
4780                                            *selected_row = parent_row;
4781                                        }
4782                                        *found = true;
4783                                        return;
4784                                    }
4785                                    *row_idx += 1;
4786
4787                                    // Recursively check nested items if expanded
4788                                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
4789                                        if let Some(nested) = prefix_preview.get(&obj.key) {
4790                                            check_nested_collapse(
4791                                                nested,
4792                                                row_idx,
4793                                                target_row,
4794                                                expanded_prefixes,
4795                                                prefix_preview,
4796                                                found,
4797                                                selected_row,
4798                                                current_row,
4799                                            );
4800                                            if *found {
4801                                                return;
4802                                            }
4803                                        } else {
4804                                            *row_idx += 1; // Loading row
4805                                        }
4806                                    }
4807                                }
4808                            }
4809
4810                            let mut found = false;
4811                            let parent_row = row_idx - 1; // Parent is the bucket
4812                            check_nested_collapse(
4813                                preview,
4814                                &mut row_idx,
4815                                self.s3_state.selected_row,
4816                                &mut self.s3_state.expanded_prefixes,
4817                                &self.s3_state.prefix_preview,
4818                                &mut found,
4819                                &mut self.s3_state.selected_row,
4820                                parent_row,
4821                            );
4822                            if found {
4823                                // Adjust scroll offset to keep selection visible
4824                                let visible_rows = self.s3_state.bucket_visible_rows.get();
4825                                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
4826                                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4827                                } else if self.s3_state.selected_row
4828                                    >= self.s3_state.bucket_scroll_offset + visible_rows
4829                                {
4830                                    self.s3_state.bucket_scroll_offset =
4831                                        self.s3_state.selected_row.saturating_sub(visible_rows - 1);
4832                                }
4833                                return;
4834                            }
4835                        } else {
4836                            row_idx += 1;
4837                        }
4838                    }
4839                }
4840
4841                // Adjust scroll offset to keep selection visible after collapse
4842                let visible_rows = self.s3_state.bucket_visible_rows.get();
4843                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
4844                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
4845                } else if self.s3_state.selected_row
4846                    >= self.s3_state.bucket_scroll_offset + visible_rows
4847                {
4848                    self.s3_state.bucket_scroll_offset =
4849                        self.s3_state.selected_row.saturating_sub(visible_rows - 1);
4850                }
4851            }
4852        } else if self.view_mode == ViewMode::InsightsResults {
4853            // Left arrow scrolls horizontally by 1 column
4854            self.insights_state.insights.results_horizontal_scroll = self
4855                .insights_state
4856                .insights
4857                .results_horizontal_scroll
4858                .saturating_sub(1);
4859        } else if self.current_service == Service::CloudWatchLogGroups
4860            && self.view_mode == ViewMode::List
4861        {
4862            // Collapse expanded log group
4863            if self.log_groups_state.log_groups.has_expanded_item() {
4864                self.log_groups_state.log_groups.collapse();
4865            }
4866        } else if self.current_service == Service::CloudWatchLogGroups
4867            && self.view_mode == ViewMode::Detail
4868        {
4869            // Collapse expanded log stream
4870            if self.log_groups_state.expanded_stream.is_some() {
4871                self.log_groups_state.expanded_stream = None;
4872            }
4873        } else if self.view_mode == ViewMode::Events {
4874            // Collapse expanded event
4875            if self.log_groups_state.expanded_event.is_some() {
4876                self.log_groups_state.expanded_event = None;
4877            }
4878        } else if self.current_service == Service::CloudWatchAlarms {
4879            // Collapse expanded alarm
4880            self.alarms_state.table.collapse();
4881        } else if self.current_service == Service::EcrRepositories {
4882            if self.ecr_state.current_repository.is_some() {
4883                // In images view - collapse expanded image
4884                self.ecr_state.images.collapse();
4885            } else {
4886                // In repositories view - collapse expanded repository
4887                self.ecr_state.repositories.collapse();
4888            }
4889        } else if self.current_service == Service::LambdaFunctions {
4890            if self.lambda_state.current_function.is_some()
4891                && self.lambda_state.detail_tab == LambdaDetailTab::Code
4892            {
4893                // Collapse selected layer
4894                self.lambda_state.layer_expanded = None;
4895            } else if self.lambda_state.current_function.is_some()
4896                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
4897            {
4898                // Collapse selected version
4899                self.lambda_state.version_table.collapse();
4900            } else if self.lambda_state.current_function.is_some()
4901                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
4902                    || (self.lambda_state.current_version.is_some()
4903                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
4904            {
4905                // Collapse selected alias
4906                self.lambda_state.alias_table.collapse();
4907            } else if self.lambda_state.current_function.is_none() {
4908                // Collapse expanded function
4909                self.lambda_state.table.collapse();
4910            }
4911        } else if self.current_service == Service::LambdaApplications {
4912            if self.lambda_application_state.current_application.is_some() {
4913                // In detail view - collapse resource or deployment
4914                if self.lambda_application_state.detail_tab
4915                    == crate::ui::lambda::ApplicationDetailTab::Overview
4916                {
4917                    self.lambda_application_state.resources.collapse();
4918                } else {
4919                    self.lambda_application_state.deployments.collapse();
4920                }
4921            } else {
4922                // Collapse expanded application in list
4923                if self.lambda_application_state.table.has_expanded_item() {
4924                    self.lambda_application_state.table.collapse();
4925                }
4926            }
4927        } else if self.current_service == Service::CloudFormationStacks
4928            && self.cfn_state.current_stack.is_none()
4929        {
4930            self.cfn_state.table.collapse();
4931        } else if self.current_service == Service::IamUsers {
4932            if self.iam_state.users.has_expanded_item() {
4933                self.iam_state.users.collapse();
4934            }
4935        } else if self.current_service == Service::IamRoles {
4936            if self.view_mode == ViewMode::PolicyView {
4937                // Go back from policy view to role detail
4938                self.view_mode = ViewMode::Detail;
4939                self.iam_state.current_policy = None;
4940                self.iam_state.policy_document.clear();
4941                self.iam_state.policy_scroll = 0;
4942            } else if self.iam_state.current_role.is_some() {
4943                if self.iam_state.role_tab == RoleTab::Tags
4944                    && self.iam_state.tags.has_expanded_item()
4945                {
4946                    self.iam_state.tags.collapse();
4947                } else if self.iam_state.role_tab == RoleTab::LastAccessed
4948                    && self
4949                        .iam_state
4950                        .last_accessed_services
4951                        .expanded_item
4952                        .is_some()
4953                {
4954                    self.iam_state.last_accessed_services.collapse();
4955                } else if self.iam_state.policies.has_expanded_item() {
4956                    self.iam_state.policies.collapse();
4957                }
4958            } else if self.iam_state.roles.has_expanded_item() {
4959                self.iam_state.roles.collapse();
4960            }
4961        } else if self.current_service == Service::IamUserGroups {
4962            if self.iam_state.current_group.is_some() {
4963                if self.iam_state.group_tab == GroupTab::Users
4964                    && self.iam_state.group_users.has_expanded_item()
4965                {
4966                    self.iam_state.group_users.collapse();
4967                } else if self.iam_state.group_tab == GroupTab::Permissions
4968                    && self.iam_state.policies.has_expanded_item()
4969                {
4970                    self.iam_state.policies.collapse();
4971                } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
4972                    && self
4973                        .iam_state
4974                        .last_accessed_services
4975                        .expanded_item
4976                        .is_some()
4977                {
4978                    self.iam_state.last_accessed_services.collapse();
4979                }
4980            } else if self.iam_state.groups.has_expanded_item() {
4981                self.iam_state.groups.collapse();
4982            }
4983        }
4984    }
4985
4986    fn select_item(&mut self) {
4987        if self.mode == Mode::RegionPicker {
4988            let filtered = self.get_filtered_regions();
4989            if let Some(region) = filtered.get(self.region_picker_selected) {
4990                // Save current session before changing region
4991                if !self.tabs.is_empty() {
4992                    let mut session = crate::session::Session::new(
4993                        self.profile.clone(),
4994                        self.region.clone(),
4995                        self.config.account_id.clone(),
4996                        self.config.role_arn.clone(),
4997                    );
4998
4999                    for tab in &self.tabs {
5000                        session.tabs.push(crate::session::SessionTab {
5001                            service: format!("{:?}", tab.service),
5002                            title: tab.title.clone(),
5003                            breadcrumb: tab.breadcrumb.clone(),
5004                            filter: None,
5005                            selected_item: None,
5006                        });
5007                    }
5008
5009                    let _ = session.save();
5010                }
5011
5012                self.region = region.code.to_string();
5013                self.config.region = region.code.to_string();
5014
5015                // Close all tabs - region change invalidates all data
5016                self.tabs.clear();
5017                self.current_tab = 0;
5018                self.service_selected = false;
5019
5020                self.mode = Mode::Normal;
5021            }
5022        } else if self.mode == Mode::ProfilePicker {
5023            let filtered = self.get_filtered_profiles();
5024            if let Some(profile) = filtered.get(self.profile_picker_selected) {
5025                let profile_name = profile.name.clone();
5026                let profile_region = profile.region.clone();
5027
5028                self.profile = profile_name.clone();
5029                std::env::set_var("AWS_PROFILE", &profile_name);
5030
5031                // Use profile's region if available
5032                if let Some(region) = profile_region {
5033                    self.region = region;
5034                }
5035
5036                self.mode = Mode::Normal;
5037                // Note: Changing profile requires reconnecting to AWS
5038            }
5039        } else if self.mode == Mode::ServicePicker {
5040            let filtered = self.filtered_services();
5041            if let Some(&service) = filtered.get(self.service_picker.selected) {
5042                let new_service = match service {
5043                    "CloudWatch > Log Groups" => Service::CloudWatchLogGroups,
5044                    "CloudWatch > Logs Insights" => Service::CloudWatchInsights,
5045                    "CloudWatch > Alarms" => Service::CloudWatchAlarms,
5046                    "CloudFormation > Stacks" => Service::CloudFormationStacks,
5047                    "ECR > Repositories" => Service::EcrRepositories,
5048                    "IAM > Users" => Service::IamUsers,
5049                    "IAM > Roles" => Service::IamRoles,
5050                    "IAM > User Groups" => Service::IamUserGroups,
5051                    "Lambda > Functions" => Service::LambdaFunctions,
5052                    "Lambda > Applications" => Service::LambdaApplications,
5053                    "S3 > Buckets" => Service::S3Buckets,
5054                    _ => return,
5055                };
5056
5057                // Create new tab
5058                self.tabs.push(Tab {
5059                    service: new_service,
5060                    title: service.to_string(),
5061                    breadcrumb: service.to_string(),
5062                });
5063                self.current_tab = self.tabs.len() - 1;
5064                self.current_service = new_service;
5065                self.view_mode = ViewMode::List;
5066                self.service_selected = true;
5067                self.mode = Mode::Normal;
5068            }
5069        } else if self.mode == Mode::TabPicker {
5070            let filtered = self.get_filtered_tabs();
5071            if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
5072                self.current_tab = idx;
5073                self.current_service = self.tabs[idx].service;
5074                self.mode = Mode::Normal;
5075                self.tab_filter.clear();
5076            }
5077        } else if self.mode == Mode::SessionPicker {
5078            let filtered = self.get_filtered_sessions();
5079            if let Some(&session) = filtered.get(self.session_picker_selected) {
5080                let session = session.clone();
5081
5082                // Load the selected session
5083                self.current_session = Some(session.clone());
5084                self.profile = session.profile.clone();
5085                self.region = session.region.clone();
5086                self.config.region = session.region.clone();
5087                self.config.account_id = session.account_id.clone();
5088                self.config.role_arn = session.role_arn.clone();
5089
5090                // Restore tabs
5091                self.tabs = session
5092                    .tabs
5093                    .iter()
5094                    .map(|st| Tab {
5095                        service: match st.service.as_str() {
5096                            "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
5097                            "CloudWatchInsights" => Service::CloudWatchInsights,
5098                            "CloudWatchAlarms" => Service::CloudWatchAlarms,
5099                            "S3Buckets" => Service::S3Buckets,
5100                            _ => Service::CloudWatchLogGroups,
5101                        },
5102                        title: st.title.clone(),
5103                        breadcrumb: st.breadcrumb.clone(),
5104                    })
5105                    .collect();
5106
5107                if !self.tabs.is_empty() {
5108                    self.current_tab = 0;
5109                    self.current_service = self.tabs[0].service;
5110                    self.service_selected = true;
5111                }
5112
5113                self.mode = Mode::Normal;
5114            }
5115        } else if self.mode == Mode::InsightsInput {
5116            // In InsightsInput mode, behavior depends on focus
5117            use crate::app::InsightsFocus;
5118            match self.insights_state.insights.insights_focus {
5119                InsightsFocus::Query => {
5120                    // Add newline to query
5121                    self.insights_state.insights.query_text.push('\n');
5122                    self.insights_state.insights.query_cursor_line += 1;
5123                    self.insights_state.insights.query_cursor_col = 0;
5124                }
5125                InsightsFocus::LogGroupSearch => {
5126                    // Toggle dropdown
5127                    self.insights_state.insights.show_dropdown =
5128                        !self.insights_state.insights.show_dropdown;
5129                }
5130                _ => {}
5131            }
5132        } else if self.mode == Mode::Normal {
5133            // If no service selected, select from service picker
5134            if !self.service_selected {
5135                let filtered = self.filtered_services();
5136                if let Some(&service) = filtered.get(self.service_picker.selected) {
5137                    match service {
5138                        "CloudWatch > Log Groups" => {
5139                            self.current_service = Service::CloudWatchLogGroups;
5140                            self.view_mode = ViewMode::List;
5141                            self.service_selected = true;
5142                        }
5143                        "CloudWatch > Logs Insights" => {
5144                            self.current_service = Service::CloudWatchInsights;
5145                            self.view_mode = ViewMode::InsightsResults;
5146                            self.service_selected = true;
5147                        }
5148                        "CloudWatch > Alarms" => {
5149                            self.current_service = Service::CloudWatchAlarms;
5150                            self.view_mode = ViewMode::List;
5151                            self.service_selected = true;
5152                        }
5153                        "S3 > Buckets" => {
5154                            self.current_service = Service::S3Buckets;
5155                            self.view_mode = ViewMode::List;
5156                            self.service_selected = true;
5157                        }
5158                        "ECR > Repositories" => {
5159                            self.current_service = Service::EcrRepositories;
5160                            self.view_mode = ViewMode::List;
5161                            self.service_selected = true;
5162                        }
5163                        "Lambda > Functions" => {
5164                            self.current_service = Service::LambdaFunctions;
5165                            self.view_mode = ViewMode::List;
5166                            self.service_selected = true;
5167                        }
5168                        "Lambda > Applications" => {
5169                            self.current_service = Service::LambdaApplications;
5170                            self.view_mode = ViewMode::List;
5171                            self.service_selected = true;
5172                        }
5173                        _ => {}
5174                    }
5175                }
5176                return;
5177            }
5178
5179            // Select in content area
5180            if self.view_mode == ViewMode::InsightsResults {
5181                // Toggle expand for selected result
5182                if self.insights_state.insights.expanded_result
5183                    == Some(self.insights_state.insights.results_selected)
5184                {
5185                    self.insights_state.insights.expanded_result = None;
5186                } else {
5187                    self.insights_state.insights.expanded_result =
5188                        Some(self.insights_state.insights.results_selected);
5189                }
5190            } else if self.current_service == Service::S3Buckets {
5191                if self.s3_state.current_bucket.is_none() {
5192                    // Find which bucket/prefix the selected row corresponds to
5193                    let mut row_idx = 0;
5194                    for bucket in &self.s3_state.buckets.items {
5195                        if row_idx == self.s3_state.selected_row {
5196                            // Selected a bucket - drill into it
5197                            self.s3_state.current_bucket = Some(bucket.name.clone());
5198                            self.s3_state.prefix_stack.clear();
5199                            self.s3_state.buckets.loading = true;
5200                            return;
5201                        }
5202                        row_idx += 1;
5203
5204                        // Skip error rows - they're not selectable
5205                        if self.s3_state.bucket_errors.contains_key(&bucket.name)
5206                            && self.s3_state.expanded_prefixes.contains(&bucket.name)
5207                        {
5208                            continue;
5209                        }
5210
5211                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
5212                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
5213                                for obj in preview {
5214                                    if row_idx == self.s3_state.selected_row {
5215                                        // Selected a prefix - drill into bucket with this prefix
5216                                        if obj.is_prefix {
5217                                            self.s3_state.current_bucket =
5218                                                Some(bucket.name.clone());
5219                                            self.s3_state.prefix_stack = vec![obj.key.clone()];
5220                                            self.s3_state.buckets.loading = true;
5221                                        }
5222                                        return;
5223                                    }
5224                                    row_idx += 1;
5225
5226                                    // Check nested preview rows
5227                                    if obj.is_prefix
5228                                        && self.s3_state.expanded_prefixes.contains(&obj.key)
5229                                    {
5230                                        if let Some(nested) =
5231                                            self.s3_state.prefix_preview.get(&obj.key)
5232                                        {
5233                                            for nested_obj in nested {
5234                                                if row_idx == self.s3_state.selected_row {
5235                                                    // Selected a nested prefix - drill into bucket with this prefix
5236                                                    if nested_obj.is_prefix {
5237                                                        self.s3_state.current_bucket =
5238                                                            Some(bucket.name.clone());
5239                                                        // Build proper prefix stack: parent, then child
5240                                                        self.s3_state.prefix_stack = vec![
5241                                                            obj.key.clone(),
5242                                                            nested_obj.key.clone(),
5243                                                        ];
5244                                                        self.s3_state.buckets.loading = true;
5245                                                    }
5246                                                    return;
5247                                                }
5248                                                row_idx += 1;
5249                                            }
5250                                        } else {
5251                                            row_idx += 1;
5252                                        }
5253                                    }
5254                                }
5255                            } else {
5256                                row_idx += 1;
5257                            }
5258                        }
5259                    }
5260                } else {
5261                    // In objects view - map visual index to actual object (including nested items)
5262                    let mut visual_idx = 0;
5263                    let mut found_obj: Option<S3Object> = None;
5264
5265                    // Helper to recursively check nested items
5266                    fn check_nested_select(
5267                        obj: &S3Object,
5268                        visual_idx: &mut usize,
5269                        target_idx: usize,
5270                        expanded_prefixes: &std::collections::HashSet<String>,
5271                        prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
5272                        found_obj: &mut Option<S3Object>,
5273                    ) {
5274                        if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
5275                            if let Some(preview) = prefix_preview.get(&obj.key) {
5276                                for nested_obj in preview {
5277                                    if *visual_idx == target_idx {
5278                                        *found_obj = Some(nested_obj.clone());
5279                                        return;
5280                                    }
5281                                    *visual_idx += 1;
5282
5283                                    // Recursively check deeper levels
5284                                    check_nested_select(
5285                                        nested_obj,
5286                                        visual_idx,
5287                                        target_idx,
5288                                        expanded_prefixes,
5289                                        prefix_preview,
5290                                        found_obj,
5291                                    );
5292                                    if found_obj.is_some() {
5293                                        return;
5294                                    }
5295                                }
5296                            } else {
5297                                // Loading row
5298                                *visual_idx += 1;
5299                            }
5300                        }
5301                    }
5302
5303                    for obj in &self.s3_state.objects {
5304                        if visual_idx == self.s3_state.selected_object {
5305                            found_obj = Some(obj.clone());
5306                            break;
5307                        }
5308                        visual_idx += 1;
5309
5310                        // Check nested items recursively
5311                        check_nested_select(
5312                            obj,
5313                            &mut visual_idx,
5314                            self.s3_state.selected_object,
5315                            &self.s3_state.expanded_prefixes,
5316                            &self.s3_state.prefix_preview,
5317                            &mut found_obj,
5318                        );
5319                        if found_obj.is_some() {
5320                            break;
5321                        }
5322                    }
5323
5324                    if let Some(obj) = found_obj {
5325                        if obj.is_prefix {
5326                            // Drill into prefix
5327                            self.s3_state.prefix_stack.push(obj.key.clone());
5328                            self.s3_state.buckets.loading = true;
5329                        }
5330                    }
5331                }
5332            } else if self.current_service == Service::CloudFormationStacks {
5333                if self.cfn_state.current_stack.is_none() {
5334                    // Drill into stack detail view
5335                    let filtered_stacks = self.filtered_cloudformation_stacks();
5336                    if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
5337                        self.cfn_state.current_stack = Some(stack.name.clone());
5338                        self.update_current_tab_breadcrumb();
5339                    }
5340                }
5341            } else if self.current_service == Service::EcrRepositories {
5342                if self.ecr_state.current_repository.is_none() {
5343                    // In repositories view - drill into selected repository
5344                    let filtered_repos = self.filtered_ecr_repositories();
5345                    if let Some(repo) = self.ecr_state.repositories.get_selected(&filtered_repos) {
5346                        let repo_name = repo.name.clone();
5347                        let repo_uri = repo.uri.clone();
5348                        self.ecr_state.current_repository = Some(repo_name);
5349                        self.ecr_state.current_repository_uri = Some(repo_uri);
5350                        self.ecr_state.images.reset();
5351                        self.ecr_state.repositories.loading = true;
5352                    }
5353                }
5354            } else if self.current_service == Service::IamUsers {
5355                if self.iam_state.current_user.is_some() {
5356                    // Open policy view (but not on Tags tab)
5357                    if self.iam_state.user_tab != UserTab::Tags {
5358                        let filtered = crate::ui::iam::filtered_iam_policies(self);
5359                        if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
5360                            self.iam_state.current_policy = Some(policy.policy_name.clone());
5361                            self.iam_state.policy_scroll = 0;
5362                            self.view_mode = ViewMode::PolicyView;
5363                            self.iam_state.policies.loading = true;
5364                            self.update_current_tab_breadcrumb();
5365                        }
5366                    }
5367                } else if self.iam_state.current_user.is_none() {
5368                    let filtered_users = crate::ui::iam::filtered_iam_users(self);
5369                    if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
5370                        self.iam_state.current_user = Some(user.user_name.clone());
5371                        self.iam_state.user_tab = UserTab::Permissions;
5372                        self.iam_state.policies.reset();
5373                        self.update_current_tab_breadcrumb();
5374                    }
5375                }
5376            } else if self.current_service == Service::IamRoles {
5377                if self.iam_state.current_role.is_some() {
5378                    // Open policy view (but not on Tags tab)
5379                    if self.iam_state.role_tab != RoleTab::Tags {
5380                        let filtered = crate::ui::iam::filtered_iam_policies(self);
5381                        if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
5382                            self.iam_state.current_policy = Some(policy.policy_name.clone());
5383                            self.iam_state.policy_scroll = 0;
5384                            self.view_mode = ViewMode::PolicyView;
5385                            self.iam_state.policies.loading = true;
5386                            self.update_current_tab_breadcrumb();
5387                        }
5388                    }
5389                } else if self.iam_state.current_role.is_none() {
5390                    let filtered_roles = crate::ui::iam::filtered_iam_roles(self);
5391                    if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
5392                        self.iam_state.current_role = Some(role.role_name.clone());
5393                        self.iam_state.role_tab = RoleTab::Permissions;
5394                        self.iam_state.policies.reset();
5395                        self.update_current_tab_breadcrumb();
5396                    }
5397                }
5398            } else if self.current_service == Service::IamUserGroups {
5399                if self.iam_state.current_group.is_none() {
5400                    let filtered_groups: Vec<_> = self
5401                        .iam_state
5402                        .groups
5403                        .items
5404                        .iter()
5405                        .filter(|g| {
5406                            if self.iam_state.groups.filter.is_empty() {
5407                                true
5408                            } else {
5409                                g.group_name
5410                                    .to_lowercase()
5411                                    .contains(&self.iam_state.groups.filter.to_lowercase())
5412                            }
5413                        })
5414                        .collect();
5415                    if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
5416                        self.iam_state.current_group = Some(group.group_name.clone());
5417                        self.update_current_tab_breadcrumb();
5418                    }
5419                }
5420            } else if self.current_service == Service::LambdaFunctions {
5421                if self.lambda_state.current_function.is_some()
5422                    && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5423                {
5424                    // In Normal mode, select version to open detail view
5425                    // In other modes (FilterInput), toggle expansion
5426                    if self.mode == Mode::Normal {
5427                        let page_size = self.lambda_state.version_table.page_size.value();
5428                        let filtered: Vec<_> = self
5429                            .lambda_state
5430                            .version_table
5431                            .items
5432                            .iter()
5433                            .filter(|v| {
5434                                self.lambda_state.version_table.filter.is_empty()
5435                                    || v.version.to_lowercase().contains(
5436                                        &self.lambda_state.version_table.filter.to_lowercase(),
5437                                    )
5438                                    || v.aliases.to_lowercase().contains(
5439                                        &self.lambda_state.version_table.filter.to_lowercase(),
5440                                    )
5441                            })
5442                            .collect();
5443                        let current_page = self.lambda_state.version_table.selected / page_size;
5444                        let start_idx = current_page * page_size;
5445                        let end_idx = (start_idx + page_size).min(filtered.len());
5446                        let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
5447                        let page_index = self.lambda_state.version_table.selected % page_size;
5448                        if let Some(version) = paginated.get(page_index) {
5449                            self.lambda_state.current_version = Some(version.version.clone());
5450                            self.lambda_state.detail_tab = LambdaDetailTab::Code;
5451                        }
5452                    } else {
5453                        // Toggle expansion
5454                        if self.lambda_state.version_table.expanded_item
5455                            == Some(self.lambda_state.version_table.selected)
5456                        {
5457                            self.lambda_state.version_table.collapse();
5458                        } else {
5459                            self.lambda_state.version_table.expanded_item =
5460                                Some(self.lambda_state.version_table.selected);
5461                        }
5462                    }
5463                } else if self.lambda_state.current_function.is_some()
5464                    && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5465                {
5466                    // Select alias to view detail (no tab change - alias view has no tabs)
5467                    let filtered: Vec<_> = self
5468                        .lambda_state
5469                        .alias_table
5470                        .items
5471                        .iter()
5472                        .filter(|a| {
5473                            self.lambda_state.alias_table.filter.is_empty()
5474                                || a.name
5475                                    .to_lowercase()
5476                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
5477                                || a.versions
5478                                    .to_lowercase()
5479                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
5480                        })
5481                        .collect();
5482                    if let Some(alias) = self.lambda_state.alias_table.get_selected(&filtered) {
5483                        self.lambda_state.current_alias = Some(alias.name.clone());
5484                    }
5485                } else if self.lambda_state.current_function.is_none() {
5486                    let filtered_functions = crate::ui::lambda::filtered_lambda_functions(self);
5487                    if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
5488                        self.lambda_state.current_function = Some(func.name.clone());
5489                        self.lambda_state.detail_tab = LambdaDetailTab::Code;
5490                        self.update_current_tab_breadcrumb();
5491                    }
5492                }
5493            } else if self.current_service == Service::LambdaApplications {
5494                let filtered = crate::ui::lambda::filtered_lambda_applications(self);
5495                if let Some(app) = self.lambda_application_state.table.get_selected(&filtered) {
5496                    let app_name = app.name.clone();
5497                    self.lambda_application_state.current_application = Some(app_name.clone());
5498                    self.lambda_application_state.detail_tab =
5499                        crate::ui::lambda::ApplicationDetailTab::Overview;
5500
5501                    // Load mock resources
5502                    use crate::lambda::Resource;
5503                    self.lambda_application_state.resources.items = vec![
5504                        Resource {
5505                            logical_id: "ApiGatewayRestApi".to_string(),
5506                            physical_id: "abc123xyz".to_string(),
5507                            resource_type: "AWS::ApiGateway::RestApi".to_string(),
5508                            last_modified: "2025-01-10 14:30:00 (UTC)".to_string(),
5509                        },
5510                        Resource {
5511                            logical_id: "LambdaFunction".to_string(),
5512                            physical_id: format!("{}-function", app_name),
5513                            resource_type: "AWS::Lambda::Function".to_string(),
5514                            last_modified: "2025-01-10 14:25:00 (UTC)".to_string(),
5515                        },
5516                        Resource {
5517                            logical_id: "DynamoDBTable".to_string(),
5518                            physical_id: format!("{}-table", app_name),
5519                            resource_type: "AWS::DynamoDB::Table".to_string(),
5520                            last_modified: "2025-01-09 10:15:00 (UTC)".to_string(),
5521                        },
5522                    ];
5523
5524                    // Load mock deployments
5525                    use crate::lambda::Deployment;
5526                    self.lambda_application_state.deployments.items = vec![
5527                        Deployment {
5528                            deployment_id: "d-ABC123XYZ".to_string(),
5529                            resource_type: "AWS::Serverless::Application".to_string(),
5530                            last_updated: "2025-01-10 14:30:00 (UTC)".to_string(),
5531                            status: "Succeeded".to_string(),
5532                        },
5533                        Deployment {
5534                            deployment_id: "d-DEF456UVW".to_string(),
5535                            resource_type: "AWS::Serverless::Application".to_string(),
5536                            last_updated: "2025-01-09 10:15:00 (UTC)".to_string(),
5537                            status: "Succeeded".to_string(),
5538                        },
5539                    ];
5540
5541                    self.update_current_tab_breadcrumb();
5542                }
5543            } else if self.current_service == Service::CloudWatchLogGroups {
5544                if self.view_mode == ViewMode::List {
5545                    // Map filtered selection to actual group index
5546                    let filtered_groups = self.filtered_log_groups();
5547                    if let Some(selected_group) =
5548                        filtered_groups.get(self.log_groups_state.log_groups.selected)
5549                    {
5550                        if let Some(actual_idx) = self
5551                            .log_groups_state
5552                            .log_groups
5553                            .items
5554                            .iter()
5555                            .position(|g| g.name == selected_group.name)
5556                        {
5557                            self.log_groups_state.log_groups.selected = actual_idx;
5558                        }
5559                    }
5560                    self.view_mode = ViewMode::Detail;
5561                    self.log_groups_state.log_streams.clear();
5562                    self.log_groups_state.selected_stream = 0;
5563                    self.log_groups_state.loading = true;
5564                    self.update_current_tab_breadcrumb();
5565                } else if self.view_mode == ViewMode::Detail {
5566                    // Map filtered stream selection to actual stream index
5567                    let filtered_streams = self.filtered_log_streams();
5568                    if let Some(selected_stream) =
5569                        filtered_streams.get(self.log_groups_state.selected_stream)
5570                    {
5571                        if let Some(actual_idx) = self
5572                            .log_groups_state
5573                            .log_streams
5574                            .iter()
5575                            .position(|s| s.name == selected_stream.name)
5576                        {
5577                            self.log_groups_state.selected_stream = actual_idx;
5578                        }
5579                    }
5580                    self.view_mode = ViewMode::Events;
5581                    self.update_current_tab_breadcrumb();
5582                    self.log_groups_state.log_events.clear();
5583                    self.log_groups_state.event_scroll_offset = 0;
5584                    self.log_groups_state.next_backward_token = None;
5585                    self.log_groups_state.loading = true;
5586                } else if self.view_mode == ViewMode::Events {
5587                    // Toggle expand for selected event
5588                    if self.log_groups_state.expanded_event
5589                        == Some(self.log_groups_state.event_scroll_offset)
5590                    {
5591                        self.log_groups_state.expanded_event = None;
5592                    } else {
5593                        self.log_groups_state.expanded_event =
5594                            Some(self.log_groups_state.event_scroll_offset);
5595                    }
5596                }
5597            } else if self.current_service == Service::CloudWatchAlarms {
5598                // Toggle expand for selected alarm
5599                self.alarms_state.table.toggle_expand();
5600            } else if self.current_service == Service::CloudWatchInsights {
5601                // In Normal mode, Enter always executes query
5602                if !self.insights_state.insights.selected_log_groups.is_empty() {
5603                    self.log_groups_state.loading = true;
5604                    self.insights_state.insights.query_completed = true;
5605                }
5606            }
5607        }
5608    }
5609
5610    pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
5611        self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
5612        Ok(())
5613    }
5614
5615    pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
5616        let alarms = self.alarms_client.list_alarms().await?;
5617        self.alarms_state.table.items = alarms
5618            .into_iter()
5619            .map(
5620                |(
5621                    name,
5622                    state,
5623                    state_updated,
5624                    description,
5625                    metric_name,
5626                    namespace,
5627                    statistic,
5628                    period,
5629                    comparison,
5630                    threshold,
5631                    actions_enabled,
5632                    state_reason,
5633                    resource,
5634                    dimensions,
5635                    expression,
5636                    alarm_type,
5637                    cross_account,
5638                )| Alarm {
5639                    name,
5640                    state,
5641                    state_updated_timestamp: state_updated,
5642                    description,
5643                    metric_name,
5644                    namespace,
5645                    statistic,
5646                    period,
5647                    comparison_operator: comparison,
5648                    threshold,
5649                    actions_enabled,
5650                    state_reason,
5651                    resource,
5652                    dimensions,
5653                    expression,
5654                    alarm_type,
5655                    cross_account,
5656                },
5657            )
5658            .collect();
5659        Ok(())
5660    }
5661
5662    pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
5663        if let Some(bucket_name) = &self.s3_state.current_bucket {
5664            // Get or fetch bucket region
5665            let bucket_region = if let Some(bucket) = self
5666                .s3_state
5667                .buckets
5668                .items
5669                .iter_mut()
5670                .find(|b| &b.name == bucket_name)
5671            {
5672                if bucket.region.is_empty() {
5673                    // Fetch the actual region
5674                    let region = self.s3_client.get_bucket_location(bucket_name).await?;
5675                    bucket.region = region.clone();
5676                    region
5677                } else {
5678                    bucket.region.clone()
5679                }
5680            } else {
5681                self.config.region.clone()
5682            };
5683
5684            let prefix = self
5685                .s3_state
5686                .prefix_stack
5687                .last()
5688                .cloned()
5689                .unwrap_or_default();
5690            let objects = self
5691                .s3_client
5692                .list_objects(bucket_name, &bucket_region, &prefix)
5693                .await?;
5694            self.s3_state.objects = objects
5695                .into_iter()
5696                .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
5697                    key,
5698                    size,
5699                    last_modified: modified,
5700                    is_prefix,
5701                    storage_class,
5702                })
5703                .collect();
5704            self.s3_state.selected_object = 0;
5705        }
5706        Ok(())
5707    }
5708
5709    pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
5710        let bucket_region = self
5711            .s3_state
5712            .buckets
5713            .items
5714            .iter()
5715            .find(|b| b.name == bucket_name)
5716            .and_then(|b| {
5717                if b.region.is_empty() {
5718                    None
5719                } else {
5720                    Some(b.region.as_str())
5721                }
5722            })
5723            .unwrap_or(self.config.region.as_str());
5724        let objects = self
5725            .s3_client
5726            .list_objects(&bucket_name, bucket_region, "")
5727            .await?;
5728        let preview: Vec<S3Object> = objects
5729            .into_iter()
5730            .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
5731                key,
5732                size,
5733                last_modified: modified,
5734                is_prefix,
5735                storage_class,
5736            })
5737            .collect();
5738        self.s3_state.bucket_preview.insert(bucket_name, preview);
5739        Ok(())
5740    }
5741
5742    pub async fn load_prefix_preview(
5743        &mut self,
5744        bucket_name: String,
5745        prefix: String,
5746    ) -> anyhow::Result<()> {
5747        let bucket_region = self
5748            .s3_state
5749            .buckets
5750            .items
5751            .iter()
5752            .find(|b| b.name == bucket_name)
5753            .and_then(|b| {
5754                if b.region.is_empty() {
5755                    None
5756                } else {
5757                    Some(b.region.as_str())
5758                }
5759            })
5760            .unwrap_or(self.config.region.as_str());
5761        let objects = self
5762            .s3_client
5763            .list_objects(&bucket_name, bucket_region, &prefix)
5764            .await?;
5765        let preview: Vec<S3Object> = objects
5766            .into_iter()
5767            .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
5768                key,
5769                size,
5770                last_modified: modified,
5771                is_prefix,
5772                storage_class,
5773            })
5774            .collect();
5775        self.s3_state.prefix_preview.insert(prefix, preview);
5776        Ok(())
5777    }
5778
5779    pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
5780        let repos = match self.ecr_state.tab {
5781            EcrTab::Private => self.ecr_client.list_private_repositories().await?,
5782            EcrTab::Public => self.ecr_client.list_public_repositories().await?,
5783        };
5784
5785        self.ecr_state.repositories.items = repos
5786            .into_iter()
5787            .map(|r| EcrRepository {
5788                name: r.name,
5789                uri: r.uri,
5790                created_at: r.created_at,
5791                tag_immutability: r.tag_immutability,
5792                encryption_type: r.encryption_type,
5793            })
5794            .collect();
5795
5796        self.ecr_state
5797            .repositories
5798            .items
5799            .sort_by(|a, b| a.name.cmp(&b.name));
5800        Ok(())
5801    }
5802
5803    pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
5804        if let Some(repo_name) = &self.ecr_state.current_repository {
5805            if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
5806                let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
5807
5808                self.ecr_state.images.items = images
5809                    .into_iter()
5810                    .map(|i| EcrImage {
5811                        tag: i.tag,
5812                        artifact_type: i.artifact_type,
5813                        pushed_at: i.pushed_at,
5814                        size_bytes: i.size_bytes,
5815                        uri: i.uri,
5816                        digest: i.digest,
5817                        last_pull_time: i.last_pull_time,
5818                    })
5819                    .collect();
5820
5821                self.ecr_state
5822                    .images
5823                    .items
5824                    .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
5825            }
5826        }
5827        Ok(())
5828    }
5829
5830    pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
5831        let stacks = self
5832            .cloudformation_client
5833            .list_stacks(self.cfn_state.view_nested)
5834            .await?;
5835
5836        let mut stacks: Vec<CfnStack> = stacks
5837            .into_iter()
5838            .map(|s| CfnStack {
5839                name: s.name,
5840                stack_id: s.stack_id,
5841                status: s.status,
5842                created_time: s.created_time,
5843                updated_time: s.updated_time,
5844                deleted_time: s.deleted_time,
5845                drift_status: s.drift_status,
5846                last_drift_check_time: s.last_drift_check_time,
5847                status_reason: s.status_reason,
5848                description: s.description,
5849                detailed_status: String::new(),
5850                root_stack: String::new(),
5851                parent_stack: String::new(),
5852                termination_protection: false,
5853                iam_role: String::new(),
5854                tags: Vec::new(),
5855                stack_policy: String::new(),
5856                rollback_monitoring_time: String::new(),
5857                rollback_alarms: Vec::new(),
5858                notification_arns: Vec::new(),
5859            })
5860            .collect();
5861
5862        // Sort by created_time DESC
5863        stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
5864
5865        self.cfn_state.table.items = stacks;
5866
5867        Ok(())
5868    }
5869
5870    pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
5871        // Load attached (managed) policies
5872        let attached_policies = self
5873            .iam_client
5874            .list_attached_role_policies(role_name)
5875            .await
5876            .map_err(|e| anyhow::anyhow!(e))?;
5877
5878        let mut policies: Vec<crate::iam::Policy> = attached_policies
5879            .into_iter()
5880            .map(|p| crate::iam::Policy {
5881                policy_name: p.policy_name().unwrap_or("").to_string(),
5882                policy_type: "Managed".to_string(),
5883                attached_via: "Direct".to_string(),
5884                attached_entities: "-".to_string(),
5885                description: "-".to_string(),
5886                creation_time: "-".to_string(),
5887                edited_time: "-".to_string(),
5888                policy_arn: p.policy_arn().map(|s| s.to_string()),
5889            })
5890            .collect();
5891
5892        // Load inline policies
5893        let inline_policy_names = self
5894            .iam_client
5895            .list_role_policies(role_name)
5896            .await
5897            .map_err(|e| anyhow::anyhow!(e))?;
5898
5899        for policy_name in inline_policy_names {
5900            policies.push(crate::iam::Policy {
5901                policy_name,
5902                policy_type: "Inline".to_string(),
5903                attached_via: "Direct".to_string(),
5904                attached_entities: "-".to_string(),
5905                description: "-".to_string(),
5906                creation_time: "-".to_string(),
5907                edited_time: "-".to_string(),
5908                policy_arn: None,
5909            });
5910        }
5911
5912        self.iam_state.policies.items = policies;
5913
5914        Ok(())
5915    }
5916
5917    pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
5918        let attached_policies = self
5919            .iam_client
5920            .list_attached_group_policies(group_name)
5921            .await
5922            .map_err(|e| anyhow::anyhow!(e))?;
5923
5924        let mut policies: Vec<crate::iam::Policy> = attached_policies
5925            .into_iter()
5926            .map(|p| crate::iam::Policy {
5927                policy_name: p.policy_name().unwrap_or("").to_string(),
5928                policy_type: "AWS managed".to_string(),
5929                attached_via: "Direct".to_string(),
5930                attached_entities: "-".to_string(),
5931                description: "-".to_string(),
5932                creation_time: "-".to_string(),
5933                edited_time: "-".to_string(),
5934                policy_arn: p.policy_arn().map(|s| s.to_string()),
5935            })
5936            .collect();
5937
5938        let inline_policy_names = self
5939            .iam_client
5940            .list_group_policies(group_name)
5941            .await
5942            .map_err(|e| anyhow::anyhow!(e))?;
5943
5944        for policy_name in inline_policy_names {
5945            policies.push(crate::iam::Policy {
5946                policy_name,
5947                policy_type: "Inline".to_string(),
5948                attached_via: "Direct".to_string(),
5949                attached_entities: "-".to_string(),
5950                description: "-".to_string(),
5951                creation_time: "-".to_string(),
5952                edited_time: "-".to_string(),
5953                policy_arn: None,
5954            });
5955        }
5956
5957        self.iam_state.policies.items = policies;
5958
5959        Ok(())
5960    }
5961
5962    pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
5963        let users = self
5964            .iam_client
5965            .get_group_users(group_name)
5966            .await
5967            .map_err(|e| anyhow::anyhow!(e))?;
5968
5969        let group_users: Vec<crate::iam::GroupUser> = users
5970            .into_iter()
5971            .map(|u| {
5972                let creation_time = {
5973                    let dt = u.create_date();
5974                    let timestamp = dt.secs();
5975                    let datetime =
5976                        chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
5977                    datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
5978                };
5979
5980                crate::iam::GroupUser {
5981                    user_name: u.user_name().to_string(),
5982                    groups: String::new(),
5983                    last_activity: String::new(),
5984                    creation_time,
5985                }
5986            })
5987            .collect();
5988
5989        self.iam_state.group_users.items = group_users;
5990
5991        Ok(())
5992    }
5993
5994    pub async fn load_policy_document(
5995        &mut self,
5996        role_name: &str,
5997        policy_name: &str,
5998    ) -> anyhow::Result<()> {
5999        // Find the policy to get its ARN and type
6000        let policy = self
6001            .iam_state
6002            .policies
6003            .items
6004            .iter()
6005            .find(|p| p.policy_name == policy_name)
6006            .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
6007
6008        let document = if let Some(policy_arn) = &policy.policy_arn {
6009            // Managed policy - use get_policy_version
6010            self.iam_client
6011                .get_policy_version(policy_arn)
6012                .await
6013                .map_err(|e| anyhow::anyhow!(e))?
6014        } else {
6015            // Inline policy - use get_role_policy
6016            self.iam_client
6017                .get_role_policy(role_name, policy_name)
6018                .await
6019                .map_err(|e| anyhow::anyhow!(e))?
6020        };
6021
6022        self.iam_state.policy_document = document;
6023
6024        Ok(())
6025    }
6026
6027    pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
6028        let document = self
6029            .iam_client
6030            .get_role(role_name)
6031            .await
6032            .map_err(|e| anyhow::anyhow!(e))?;
6033
6034        self.iam_state.trust_policy_document = document;
6035
6036        Ok(())
6037    }
6038
6039    pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
6040        // TODO: Implement real AWS API call to get service last accessed details
6041        self.iam_state.last_accessed_services.items = vec![];
6042        self.iam_state.last_accessed_services.selected = 0;
6043
6044        Ok(())
6045    }
6046
6047    pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
6048        let tags = self
6049            .iam_client
6050            .list_role_tags(role_name)
6051            .await
6052            .map_err(|e| anyhow::anyhow!(e))?;
6053        self.iam_state.tags.items = tags
6054            .into_iter()
6055            .map(|(k, v)| crate::iam::RoleTag { key: k, value: v })
6056            .collect();
6057        self.iam_state.tags.reset();
6058        Ok(())
6059    }
6060
6061    pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
6062        let tags = self
6063            .iam_client
6064            .list_user_tags(user_name)
6065            .await
6066            .map_err(|e| anyhow::anyhow!(e))?;
6067        self.iam_state.user_tags.items = tags
6068            .into_iter()
6069            .map(|(k, v)| crate::iam::UserTag { key: k, value: v })
6070            .collect();
6071        self.iam_state.user_tags.reset();
6072        Ok(())
6073    }
6074
6075    pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
6076        if let Some(group) = self
6077            .log_groups_state
6078            .log_groups
6079            .items
6080            .get(self.log_groups_state.log_groups.selected)
6081        {
6082            self.log_groups_state.log_streams =
6083                self.cloudwatch_client.list_log_streams(&group.name).await?;
6084            self.log_groups_state.selected_stream = 0;
6085        }
6086        Ok(())
6087    }
6088
6089    pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
6090        if let Some(group) = self
6091            .log_groups_state
6092            .log_groups
6093            .items
6094            .get(self.log_groups_state.log_groups.selected)
6095        {
6096            if let Some(stream) = self
6097                .log_groups_state
6098                .log_streams
6099                .get(self.log_groups_state.selected_stream)
6100            {
6101                // Calculate time range from relative date picker
6102                let (start_time, end_time) =
6103                    if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
6104                        let now = chrono::Utc::now().timestamp_millis();
6105                        let duration_ms = match self.log_groups_state.relative_unit {
6106                            crate::app::TimeUnit::Minutes => amount * 60 * 1000,
6107                            crate::app::TimeUnit::Hours => amount * 60 * 60 * 1000,
6108                            crate::app::TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
6109                            crate::app::TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
6110                        };
6111                        (Some(now - duration_ms), Some(now))
6112                    } else {
6113                        (None, None)
6114                    };
6115
6116                let (mut events, has_more, token) = self
6117                    .cloudwatch_client
6118                    .get_log_events(
6119                        &group.name,
6120                        &stream.name,
6121                        self.log_groups_state.next_backward_token.clone(),
6122                        start_time,
6123                        end_time,
6124                    )
6125                    .await?;
6126
6127                if self.log_groups_state.next_backward_token.is_some() {
6128                    // Prepend older events - keep selection at top
6129                    events.append(&mut self.log_groups_state.log_events);
6130                    self.log_groups_state.event_scroll_offset = 0;
6131                } else {
6132                    // Initial load - start at first event
6133                    self.log_groups_state.event_scroll_offset = 0;
6134                }
6135
6136                self.log_groups_state.log_events = events;
6137                self.log_groups_state.has_older_events =
6138                    has_more && self.log_groups_state.log_events.len() >= 25;
6139                self.log_groups_state.next_backward_token = token;
6140                self.log_groups_state.selected_event = 0;
6141            }
6142        }
6143        Ok(())
6144    }
6145
6146    pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
6147        if self.insights_state.insights.selected_log_groups.is_empty() {
6148            return Err(anyhow::anyhow!(
6149                "No log groups selected. Please select at least one log group."
6150            ));
6151        }
6152
6153        let now = chrono::Utc::now().timestamp_millis();
6154        let amount = self
6155            .insights_state
6156            .insights
6157            .insights_relative_amount
6158            .parse::<i64>()
6159            .unwrap_or(1);
6160        let duration_ms = match self.insights_state.insights.insights_relative_unit {
6161            TimeUnit::Minutes => amount * 60 * 1000,
6162            TimeUnit::Hours => amount * 60 * 60 * 1000,
6163            TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
6164            TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
6165        };
6166        let start_time = now - duration_ms;
6167
6168        let query_id = self
6169            .cloudwatch_client
6170            .start_query(
6171                self.insights_state.insights.selected_log_groups.clone(),
6172                self.insights_state.insights.query_text.trim().to_string(),
6173                start_time,
6174                now,
6175            )
6176            .await?;
6177
6178        // Poll for results
6179        for _ in 0..60 {
6180            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
6181            let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
6182
6183            if status == "Complete" {
6184                self.insights_state.insights.query_results = results;
6185                self.insights_state.insights.query_completed = true;
6186                self.insights_state.insights.results_selected = 0;
6187                self.insights_state.insights.expanded_result = None;
6188                self.view_mode = ViewMode::InsightsResults;
6189                return Ok(());
6190            } else if status == "Failed" || status == "Cancelled" {
6191                return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
6192            }
6193        }
6194
6195        Err(anyhow::anyhow!("Query timeout"))
6196    }
6197}
6198
6199impl CloudWatchInsightsState {
6200    fn new() -> Self {
6201        Self {
6202            insights: InsightsState::default(),
6203            loading: false,
6204        }
6205    }
6206}
6207
6208impl CloudWatchAlarmsState {
6209    fn new() -> Self {
6210        Self {
6211            table: crate::table::TableState::new(),
6212            alarm_tab: AlarmTab::AllAlarms,
6213            view_as: AlarmViewMode::Table,
6214            wrap_lines: false,
6215            sort_column: "Last state update".to_string(),
6216            sort_direction: SortDirection::Asc,
6217            input_focus: InputFocus::Filter,
6218        }
6219    }
6220}
6221
6222impl ServicePickerState {
6223    fn new() -> Self {
6224        Self {
6225            filter: String::new(),
6226            selected: 0,
6227            services: vec![
6228                "CloudWatch > Log Groups",
6229                "CloudWatch > Logs Insights",
6230                "CloudWatch > Alarms",
6231                "CloudFormation > Stacks",
6232                "ECR > Repositories",
6233                "IAM > Users",
6234                "IAM > Roles",
6235                "IAM > User Groups",
6236                "Lambda > Functions",
6237                "Lambda > Applications",
6238                "S3 > Buckets",
6239            ],
6240        }
6241    }
6242}
6243
6244#[cfg(test)]
6245mod test_helpers {
6246    use super::*;
6247
6248    // Test helper functions to reduce boilerplate
6249    #[allow(dead_code)]
6250    pub fn test_app() -> App {
6251        App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
6252    }
6253
6254    #[allow(dead_code)]
6255    pub fn test_app_no_region() -> App {
6256        App::new_without_client("test".to_string(), None)
6257    }
6258
6259    #[allow(dead_code)]
6260    pub fn test_tab(service: Service) -> Tab {
6261        Tab {
6262            service,
6263            title: service.name().to_string(),
6264            breadcrumb: service.name().to_string(),
6265        }
6266    }
6267
6268    #[allow(dead_code)]
6269    pub fn test_iam_role(name: &str) -> crate::iam::IamRole {
6270        crate::iam::IamRole {
6271            role_name: name.to_string(),
6272            path: "/".to_string(),
6273            description: format!("Test role {}", name),
6274            trusted_entities: "AWS Service: ec2.amazonaws.com".to_string(),
6275            creation_time: "2024-01-01 00:00:00".to_string(),
6276            arn: format!("arn:aws:iam::123456789012:role/{}", name),
6277            max_session_duration: "3600 seconds".to_string(),
6278            last_activity: "-".to_string(),
6279        }
6280    }
6281}
6282
6283#[cfg(test)]
6284mod tests {
6285    use super::*;
6286    use crate::keymap::Action;
6287    use test_helpers::*;
6288
6289    #[test]
6290    fn test_next_tab_cycles_forward() {
6291        let mut app = test_app();
6292        app.tabs = vec![
6293            Tab {
6294                service: Service::CloudWatchLogGroups,
6295                title: "CloudWatch > Log Groups".to_string(),
6296                breadcrumb: "CloudWatch > Log Groups".to_string(),
6297            },
6298            Tab {
6299                service: Service::CloudWatchInsights,
6300                title: "CloudWatch > Logs Insights".to_string(),
6301                breadcrumb: "CloudWatch > Logs Insights".to_string(),
6302            },
6303            Tab {
6304                service: Service::CloudWatchAlarms,
6305                title: "CloudWatch > Alarms".to_string(),
6306                breadcrumb: "CloudWatch > Alarms".to_string(),
6307            },
6308        ];
6309        app.current_tab = 0;
6310
6311        app.handle_action(Action::NextTab);
6312        assert_eq!(app.current_tab, 1);
6313        assert_eq!(app.current_service, Service::CloudWatchInsights);
6314
6315        app.handle_action(Action::NextTab);
6316        assert_eq!(app.current_tab, 2);
6317        assert_eq!(app.current_service, Service::CloudWatchAlarms);
6318
6319        // Should wrap around
6320        app.handle_action(Action::NextTab);
6321        assert_eq!(app.current_tab, 0);
6322        assert_eq!(app.current_service, Service::CloudWatchLogGroups);
6323    }
6324
6325    #[test]
6326    fn test_prev_tab_cycles_backward() {
6327        let mut app = test_app();
6328        app.tabs = vec![
6329            Tab {
6330                service: Service::CloudWatchLogGroups,
6331                title: "CloudWatch > Log Groups".to_string(),
6332                breadcrumb: "CloudWatch > Log Groups".to_string(),
6333            },
6334            Tab {
6335                service: Service::CloudWatchInsights,
6336                title: "CloudWatch > Logs Insights".to_string(),
6337                breadcrumb: "CloudWatch > Logs Insights".to_string(),
6338            },
6339            Tab {
6340                service: Service::CloudWatchAlarms,
6341                title: "CloudWatch > Alarms".to_string(),
6342                breadcrumb: "CloudWatch > Alarms".to_string(),
6343            },
6344        ];
6345        app.current_tab = 2;
6346
6347        app.handle_action(Action::PrevTab);
6348        assert_eq!(app.current_tab, 1);
6349        assert_eq!(app.current_service, Service::CloudWatchInsights);
6350
6351        app.handle_action(Action::PrevTab);
6352        assert_eq!(app.current_tab, 0);
6353        assert_eq!(app.current_service, Service::CloudWatchLogGroups);
6354
6355        // Should wrap around
6356        app.handle_action(Action::PrevTab);
6357        assert_eq!(app.current_tab, 2);
6358        assert_eq!(app.current_service, Service::CloudWatchAlarms);
6359    }
6360
6361    #[test]
6362    fn test_close_tab_removes_current() {
6363        let mut app = test_app();
6364        app.tabs = vec![
6365            Tab {
6366                service: Service::CloudWatchLogGroups,
6367                title: "CloudWatch > Log Groups".to_string(),
6368                breadcrumb: "CloudWatch > Log Groups".to_string(),
6369            },
6370            Tab {
6371                service: Service::CloudWatchInsights,
6372                title: "CloudWatch > Logs Insights".to_string(),
6373                breadcrumb: "CloudWatch > Logs Insights".to_string(),
6374            },
6375            Tab {
6376                service: Service::CloudWatchAlarms,
6377                title: "CloudWatch > Alarms".to_string(),
6378                breadcrumb: "CloudWatch > Alarms".to_string(),
6379            },
6380        ];
6381        app.current_tab = 1;
6382        app.service_selected = true;
6383
6384        app.handle_action(Action::CloseTab);
6385        assert_eq!(app.tabs.len(), 2);
6386        assert_eq!(app.current_tab, 1);
6387        assert_eq!(app.current_service, Service::CloudWatchAlarms);
6388    }
6389
6390    #[test]
6391    fn test_close_last_tab_exits_service() {
6392        let mut app = test_app();
6393        app.tabs = vec![Tab {
6394            service: Service::CloudWatchLogGroups,
6395            title: "CloudWatch > Log Groups".to_string(),
6396            breadcrumb: "CloudWatch > Log Groups".to_string(),
6397        }];
6398        app.current_tab = 0;
6399        app.service_selected = true;
6400
6401        app.handle_action(Action::CloseTab);
6402        assert_eq!(app.tabs.len(), 0);
6403        assert!(!app.service_selected);
6404        assert_eq!(app.current_tab, 0);
6405    }
6406
6407    #[test]
6408    fn test_close_service_removes_current_tab() {
6409        let mut app = test_app();
6410        app.tabs = vec![
6411            Tab {
6412                service: Service::CloudWatchLogGroups,
6413                title: "CloudWatch > Log Groups".to_string(),
6414                breadcrumb: "CloudWatch > Log Groups".to_string(),
6415            },
6416            Tab {
6417                service: Service::CloudWatchInsights,
6418                title: "CloudWatch > Logs Insights".to_string(),
6419                breadcrumb: "CloudWatch > Logs Insights".to_string(),
6420            },
6421            Tab {
6422                service: Service::CloudWatchAlarms,
6423                title: "CloudWatch > Alarms".to_string(),
6424                breadcrumb: "CloudWatch > Alarms".to_string(),
6425            },
6426        ];
6427        app.current_tab = 1;
6428        app.service_selected = true;
6429
6430        app.handle_action(Action::CloseService);
6431
6432        // Tab should be removed
6433        assert_eq!(app.tabs.len(), 2);
6434        // Should switch to next tab (Alarms at index 1)
6435        assert_eq!(app.current_tab, 1);
6436        assert_eq!(app.current_service, Service::CloudWatchAlarms);
6437        // Should stay in service mode, NOT show service picker
6438        assert!(app.service_selected);
6439        assert_eq!(app.mode, Mode::Normal);
6440    }
6441
6442    #[test]
6443    fn test_close_service_last_tab_shows_picker() {
6444        let mut app = test_app();
6445        app.tabs = vec![Tab {
6446            service: Service::CloudWatchLogGroups,
6447            title: "CloudWatch > Log Groups".to_string(),
6448            breadcrumb: "CloudWatch > Log Groups".to_string(),
6449        }];
6450        app.current_tab = 0;
6451        app.service_selected = true;
6452
6453        app.handle_action(Action::CloseService);
6454
6455        // Tab should be removed
6456        assert_eq!(app.tabs.len(), 0);
6457        // Should show service picker
6458        assert!(!app.service_selected);
6459        assert_eq!(app.mode, Mode::ServicePicker);
6460    }
6461
6462    #[test]
6463    fn test_open_tab_picker_with_tabs() {
6464        let mut app = test_app();
6465        app.tabs = vec![
6466            Tab {
6467                service: Service::CloudWatchLogGroups,
6468                title: "CloudWatch > Log Groups".to_string(),
6469                breadcrumb: "CloudWatch > Log Groups".to_string(),
6470            },
6471            Tab {
6472                service: Service::CloudWatchInsights,
6473                title: "CloudWatch > Logs Insights".to_string(),
6474                breadcrumb: "CloudWatch > Logs Insights".to_string(),
6475            },
6476        ];
6477        app.current_tab = 1;
6478
6479        app.handle_action(Action::OpenTabPicker);
6480        assert_eq!(app.mode, Mode::TabPicker);
6481        assert_eq!(app.tab_picker_selected, 1);
6482    }
6483
6484    #[test]
6485    fn test_open_tab_picker_without_tabs() {
6486        let mut app = test_app();
6487        app.tabs = vec![];
6488
6489        app.handle_action(Action::OpenTabPicker);
6490        assert_eq!(app.mode, Mode::Normal);
6491    }
6492
6493    #[test]
6494    fn test_pending_key_state() {
6495        let mut app = test_app();
6496        assert_eq!(app.pending_key, None);
6497
6498        app.pending_key = Some('g');
6499        assert_eq!(app.pending_key, Some('g'));
6500    }
6501
6502    #[test]
6503    fn test_tab_breadcrumb_updates() {
6504        let mut app = test_app();
6505        app.tabs = vec![Tab {
6506            service: Service::CloudWatchLogGroups,
6507            title: "CloudWatch > Log Groups".to_string(),
6508            breadcrumb: "CloudWatch > Log groups".to_string(),
6509        }];
6510        app.current_tab = 0;
6511        app.service_selected = true;
6512        app.current_service = Service::CloudWatchLogGroups;
6513
6514        // Initial breadcrumb
6515        assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
6516
6517        // Add a log group and update breadcrumb
6518        app.log_groups_state
6519            .log_groups
6520            .items
6521            .push(rusticity_core::LogGroup {
6522                name: "/aws/lambda/test".to_string(),
6523                creation_time: None,
6524                stored_bytes: Some(1024),
6525                retention_days: None,
6526                log_class: None,
6527                arn: None,
6528            });
6529        app.log_groups_state.log_groups.reset();
6530        app.view_mode = ViewMode::Detail;
6531        app.update_current_tab_breadcrumb();
6532
6533        // Breadcrumb should now include log group
6534        assert_eq!(
6535            app.tabs[0].breadcrumb,
6536            "CloudWatch > Log groups > /aws/lambda/test"
6537        );
6538    }
6539
6540    #[test]
6541    fn test_s3_bucket_column_selector_navigation() {
6542        let mut app = test_app();
6543        app.current_service = Service::S3Buckets;
6544        app.mode = Mode::ColumnSelector;
6545        app.column_selector_index = 0;
6546
6547        // Should navigate through 3 S3 bucket columns (0, 1, 2)
6548        app.handle_action(Action::NextItem);
6549        assert_eq!(app.column_selector_index, 1);
6550
6551        app.handle_action(Action::NextItem);
6552        assert_eq!(app.column_selector_index, 2);
6553
6554        // Should not go beyond max (2)
6555        app.handle_action(Action::NextItem);
6556        assert_eq!(app.column_selector_index, 2);
6557
6558        // Navigate back
6559        app.handle_action(Action::PrevItem);
6560        assert_eq!(app.column_selector_index, 1);
6561
6562        app.handle_action(Action::PrevItem);
6563        assert_eq!(app.column_selector_index, 0);
6564
6565        // Should not go below 0
6566        app.handle_action(Action::PrevItem);
6567        assert_eq!(app.column_selector_index, 0);
6568    }
6569
6570    #[test]
6571    fn test_cloudwatch_alarms_state_initialized() {
6572        let app = test_app();
6573
6574        // Alarms state should be initialized
6575        assert_eq!(app.alarms_state.table.items.len(), 0);
6576        assert_eq!(app.alarms_state.table.selected, 0);
6577        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
6578        assert!(!app.alarms_state.table.loading);
6579        assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
6580        assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
6581    }
6582
6583    #[test]
6584    fn test_cloudwatch_alarms_service_selection() {
6585        let mut app = test_app();
6586
6587        // Switch to alarms service
6588        app.current_service = Service::CloudWatchAlarms;
6589        app.service_selected = true;
6590
6591        assert_eq!(app.current_service, Service::CloudWatchAlarms);
6592        assert!(app.service_selected);
6593    }
6594
6595    #[test]
6596    fn test_cloudwatch_alarms_column_preferences() {
6597        let app = test_app();
6598
6599        // Should have alarm columns defined
6600        assert!(!app.all_alarm_columns.is_empty());
6601        assert!(!app.visible_alarm_columns.is_empty());
6602
6603        // Default visible columns
6604        assert!(app.visible_alarm_columns.contains(&AlarmColumn::Name));
6605        assert!(app.visible_alarm_columns.contains(&AlarmColumn::State));
6606    }
6607
6608    #[test]
6609    fn test_s3_bucket_navigation_without_expansion() {
6610        let mut app = test_app();
6611        app.current_service = Service::S3Buckets;
6612        app.service_selected = true;
6613        app.mode = Mode::Normal;
6614
6615        // Add 3 buckets
6616        app.s3_state.buckets.items = vec![
6617            S3Bucket {
6618                name: "bucket1".to_string(),
6619                region: "us-east-1".to_string(),
6620                creation_date: "2024-01-01T00:00:00Z".to_string(),
6621            },
6622            S3Bucket {
6623                name: "bucket2".to_string(),
6624                region: "us-east-1".to_string(),
6625                creation_date: "2024-01-02T00:00:00Z".to_string(),
6626            },
6627            S3Bucket {
6628                name: "bucket3".to_string(),
6629                region: "us-east-1".to_string(),
6630                creation_date: "2024-01-03T00:00:00Z".to_string(),
6631            },
6632        ];
6633        app.s3_state.selected_row = 0;
6634
6635        // Navigate down
6636        app.handle_action(Action::NextItem);
6637        assert_eq!(app.s3_state.selected_row, 1);
6638
6639        app.handle_action(Action::NextItem);
6640        assert_eq!(app.s3_state.selected_row, 2);
6641
6642        // Should not go beyond last bucket
6643        app.handle_action(Action::NextItem);
6644        assert_eq!(app.s3_state.selected_row, 2);
6645
6646        // Navigate up
6647        app.handle_action(Action::PrevItem);
6648        assert_eq!(app.s3_state.selected_row, 1);
6649
6650        app.handle_action(Action::PrevItem);
6651        assert_eq!(app.s3_state.selected_row, 0);
6652
6653        // Should not go below 0
6654        app.handle_action(Action::PrevItem);
6655        assert_eq!(app.s3_state.selected_row, 0);
6656    }
6657
6658    #[test]
6659    fn test_s3_bucket_navigation_with_expansion() {
6660        let mut app = test_app();
6661        app.current_service = Service::S3Buckets;
6662        app.service_selected = true;
6663        app.mode = Mode::Normal;
6664
6665        // Add 2 buckets
6666        app.s3_state.buckets.items = vec![
6667            S3Bucket {
6668                name: "bucket1".to_string(),
6669                region: "us-east-1".to_string(),
6670                creation_date: "2024-01-01T00:00:00Z".to_string(),
6671            },
6672            S3Bucket {
6673                name: "bucket2".to_string(),
6674                region: "us-east-1".to_string(),
6675                creation_date: "2024-01-02T00:00:00Z".to_string(),
6676            },
6677        ];
6678
6679        // Expand bucket1 with 2 objects
6680        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6681        app.s3_state.bucket_preview.insert(
6682            "bucket1".to_string(),
6683            vec![
6684                S3Object {
6685                    key: "file1.txt".to_string(),
6686                    size: 100,
6687                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6688                    is_prefix: false,
6689                    storage_class: "STANDARD".to_string(),
6690                },
6691                S3Object {
6692                    key: "folder/".to_string(),
6693                    size: 0,
6694                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6695                    is_prefix: true,
6696                    storage_class: String::new(),
6697                },
6698            ],
6699        );
6700
6701        app.s3_state.selected_row = 0;
6702
6703        // Total rows: bucket1 (row 0) + file1.txt (row 1) + folder/ (row 2) + bucket2 (row 3) = 4 rows
6704        // Navigate through all rows
6705        app.handle_action(Action::NextItem);
6706        assert_eq!(app.s3_state.selected_row, 1); // file1.txt
6707
6708        app.handle_action(Action::NextItem);
6709        assert_eq!(app.s3_state.selected_row, 2); // folder/
6710
6711        app.handle_action(Action::NextItem);
6712        assert_eq!(app.s3_state.selected_row, 3); // bucket2
6713
6714        // Should not go beyond last row
6715        app.handle_action(Action::NextItem);
6716        assert_eq!(app.s3_state.selected_row, 3);
6717    }
6718
6719    #[test]
6720    fn test_s3_bucket_navigation_with_nested_expansion() {
6721        let mut app = test_app();
6722        app.current_service = Service::S3Buckets;
6723        app.service_selected = true;
6724        app.mode = Mode::Normal;
6725
6726        // Add 1 bucket
6727        app.s3_state.buckets.items = vec![S3Bucket {
6728            name: "bucket1".to_string(),
6729            region: "us-east-1".to_string(),
6730            creation_date: "2024-01-01T00:00:00Z".to_string(),
6731        }];
6732
6733        // Expand bucket1 with a folder
6734        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6735        app.s3_state.bucket_preview.insert(
6736            "bucket1".to_string(),
6737            vec![S3Object {
6738                key: "folder/".to_string(),
6739                size: 0,
6740                last_modified: "2024-01-01T00:00:00Z".to_string(),
6741                is_prefix: true,
6742                storage_class: String::new(),
6743            }],
6744        );
6745
6746        // Expand the folder with 2 nested objects
6747        app.s3_state.expanded_prefixes.insert("folder/".to_string());
6748        app.s3_state.prefix_preview.insert(
6749            "folder/".to_string(),
6750            vec![
6751                S3Object {
6752                    key: "folder/file1.txt".to_string(),
6753                    size: 100,
6754                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6755                    is_prefix: false,
6756                    storage_class: "STANDARD".to_string(),
6757                },
6758                S3Object {
6759                    key: "folder/file2.txt".to_string(),
6760                    size: 200,
6761                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6762                    is_prefix: false,
6763                    storage_class: "STANDARD".to_string(),
6764                },
6765            ],
6766        );
6767
6768        app.s3_state.selected_row = 0;
6769
6770        // Total rows: bucket1 (0) + folder/ (1) + file1.txt (2) + file2.txt (3) = 4 rows
6771        app.handle_action(Action::NextItem);
6772        assert_eq!(app.s3_state.selected_row, 1); // folder/
6773
6774        app.handle_action(Action::NextItem);
6775        assert_eq!(app.s3_state.selected_row, 2); // folder/file1.txt
6776
6777        app.handle_action(Action::NextItem);
6778        assert_eq!(app.s3_state.selected_row, 3); // folder/file2.txt
6779
6780        // Should not go beyond last row
6781        app.handle_action(Action::NextItem);
6782        assert_eq!(app.s3_state.selected_row, 3);
6783    }
6784
6785    #[test]
6786    fn test_calculate_total_bucket_rows() {
6787        let mut app = test_app();
6788
6789        // No buckets
6790        assert_eq!(app.calculate_total_bucket_rows(), 0);
6791
6792        // 2 buckets, no expansion
6793        app.s3_state.buckets.items = vec![
6794            S3Bucket {
6795                name: "bucket1".to_string(),
6796                region: "us-east-1".to_string(),
6797                creation_date: "2024-01-01T00:00:00Z".to_string(),
6798            },
6799            S3Bucket {
6800                name: "bucket2".to_string(),
6801                region: "us-east-1".to_string(),
6802                creation_date: "2024-01-02T00:00:00Z".to_string(),
6803            },
6804        ];
6805        assert_eq!(app.calculate_total_bucket_rows(), 2);
6806
6807        // Expand bucket1 with 3 objects
6808        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6809        app.s3_state.bucket_preview.insert(
6810            "bucket1".to_string(),
6811            vec![
6812                S3Object {
6813                    key: "file1.txt".to_string(),
6814                    size: 100,
6815                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6816                    is_prefix: false,
6817                    storage_class: "STANDARD".to_string(),
6818                },
6819                S3Object {
6820                    key: "file2.txt".to_string(),
6821                    size: 200,
6822                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6823                    is_prefix: false,
6824                    storage_class: "STANDARD".to_string(),
6825                },
6826                S3Object {
6827                    key: "folder/".to_string(),
6828                    size: 0,
6829                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6830                    is_prefix: true,
6831                    storage_class: String::new(),
6832                },
6833            ],
6834        );
6835        assert_eq!(app.calculate_total_bucket_rows(), 5); // 2 buckets + 3 objects
6836
6837        // Expand folder/ with 2 nested objects
6838        app.s3_state.expanded_prefixes.insert("folder/".to_string());
6839        app.s3_state.prefix_preview.insert(
6840            "folder/".to_string(),
6841            vec![
6842                S3Object {
6843                    key: "folder/nested1.txt".to_string(),
6844                    size: 50,
6845                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6846                    is_prefix: false,
6847                    storage_class: "STANDARD".to_string(),
6848                },
6849                S3Object {
6850                    key: "folder/nested2.txt".to_string(),
6851                    size: 75,
6852                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6853                    is_prefix: false,
6854                    storage_class: "STANDARD".to_string(),
6855                },
6856            ],
6857        );
6858        assert_eq!(app.calculate_total_bucket_rows(), 7); // 2 buckets + 3 objects + 2 nested
6859    }
6860
6861    #[test]
6862    fn test_calculate_total_object_rows() {
6863        let mut app = test_app();
6864        app.s3_state.current_bucket = Some("test-bucket".to_string());
6865
6866        // No objects
6867        assert_eq!(app.calculate_total_object_rows(), 0);
6868
6869        // 2 objects, no expansion
6870        app.s3_state.objects = vec![
6871            S3Object {
6872                key: "file1.txt".to_string(),
6873                size: 100,
6874                last_modified: "2024-01-01T00:00:00Z".to_string(),
6875                is_prefix: false,
6876                storage_class: "STANDARD".to_string(),
6877            },
6878            S3Object {
6879                key: "folder/".to_string(),
6880                size: 0,
6881                last_modified: "2024-01-01T00:00:00Z".to_string(),
6882                is_prefix: true,
6883                storage_class: String::new(),
6884            },
6885        ];
6886        assert_eq!(app.calculate_total_object_rows(), 2);
6887
6888        // Expand folder/ with 2 items
6889        app.s3_state.expanded_prefixes.insert("folder/".to_string());
6890        app.s3_state.prefix_preview.insert(
6891            "folder/".to_string(),
6892            vec![
6893                S3Object {
6894                    key: "folder/file2.txt".to_string(),
6895                    size: 200,
6896                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6897                    is_prefix: false,
6898                    storage_class: "STANDARD".to_string(),
6899                },
6900                S3Object {
6901                    key: "folder/subfolder/".to_string(),
6902                    size: 0,
6903                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6904                    is_prefix: true,
6905                    storage_class: String::new(),
6906                },
6907            ],
6908        );
6909        assert_eq!(app.calculate_total_object_rows(), 4); // 2 + 2 nested
6910
6911        // Expand subfolder/ with 1 item (3rd level)
6912        app.s3_state
6913            .expanded_prefixes
6914            .insert("folder/subfolder/".to_string());
6915        app.s3_state.prefix_preview.insert(
6916            "folder/subfolder/".to_string(),
6917            vec![S3Object {
6918                key: "folder/subfolder/deep.txt".to_string(),
6919                size: 50,
6920                last_modified: "2024-01-01T00:00:00Z".to_string(),
6921                is_prefix: false,
6922                storage_class: "STANDARD".to_string(),
6923            }],
6924        );
6925        assert_eq!(app.calculate_total_object_rows(), 5); // 2 + 2 nested + 1 deep
6926    }
6927
6928    #[test]
6929    fn test_s3_object_navigation_with_deep_nesting() {
6930        let mut app = test_app();
6931        app.current_service = Service::S3Buckets;
6932        app.service_selected = true;
6933        app.mode = Mode::Normal;
6934        app.s3_state.current_bucket = Some("test-bucket".to_string());
6935
6936        // Add folder structure: folder1/ -> folder2/ -> file.txt
6937        app.s3_state.objects = vec![S3Object {
6938            key: "folder1/".to_string(),
6939            size: 0,
6940            last_modified: "2024-01-01T00:00:00Z".to_string(),
6941            is_prefix: true,
6942            storage_class: String::new(),
6943        }];
6944
6945        // Expand folder1/
6946        app.s3_state
6947            .expanded_prefixes
6948            .insert("folder1/".to_string());
6949        app.s3_state.prefix_preview.insert(
6950            "folder1/".to_string(),
6951            vec![S3Object {
6952                key: "folder1/folder2/".to_string(),
6953                size: 0,
6954                last_modified: "2024-01-01T00:00:00Z".to_string(),
6955                is_prefix: true,
6956                storage_class: String::new(),
6957            }],
6958        );
6959
6960        // Expand folder2/
6961        app.s3_state
6962            .expanded_prefixes
6963            .insert("folder1/folder2/".to_string());
6964        app.s3_state.prefix_preview.insert(
6965            "folder1/folder2/".to_string(),
6966            vec![S3Object {
6967                key: "folder1/folder2/file.txt".to_string(),
6968                size: 100,
6969                last_modified: "2024-01-01T00:00:00Z".to_string(),
6970                is_prefix: false,
6971                storage_class: "STANDARD".to_string(),
6972            }],
6973        );
6974
6975        app.s3_state.selected_object = 0;
6976
6977        // Total: folder1/ (0) + folder2/ (1) + file.txt (2) = 3 rows
6978        app.handle_action(Action::NextItem);
6979        assert_eq!(app.s3_state.selected_object, 1); // folder2/
6980
6981        app.handle_action(Action::NextItem);
6982        assert_eq!(app.s3_state.selected_object, 2); // file.txt
6983
6984        // Should not go beyond
6985        app.handle_action(Action::NextItem);
6986        assert_eq!(app.s3_state.selected_object, 2);
6987    }
6988
6989    #[test]
6990    fn test_s3_expand_nested_folder_in_objects_view() {
6991        let mut app = test_app();
6992        app.current_service = Service::S3Buckets;
6993        app.service_selected = true;
6994        app.mode = Mode::Normal;
6995        app.s3_state.current_bucket = Some("test-bucket".to_string());
6996
6997        // Add parent folder
6998        app.s3_state.objects = vec![S3Object {
6999            key: "parent/".to_string(),
7000            size: 0,
7001            last_modified: "2024-01-01T00:00:00Z".to_string(),
7002            is_prefix: true,
7003            storage_class: String::new(),
7004        }];
7005
7006        // Expand parent
7007        app.s3_state.expanded_prefixes.insert("parent/".to_string());
7008        app.s3_state.prefix_preview.insert(
7009            "parent/".to_string(),
7010            vec![S3Object {
7011                key: "parent/child/".to_string(),
7012                size: 0,
7013                last_modified: "2024-01-01T00:00:00Z".to_string(),
7014                is_prefix: true,
7015                storage_class: String::new(),
7016            }],
7017        );
7018
7019        // Select the nested folder (index 1)
7020        app.s3_state.selected_object = 1;
7021
7022        // Expand it (simulate pressing Enter/Right)
7023        app.handle_action(Action::NextPane);
7024
7025        // Should be expanded now
7026        assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
7027        assert!(app.s3_state.buckets.loading); // Should trigger load
7028    }
7029
7030    #[test]
7031    fn test_s3_drill_into_nested_folder() {
7032        let mut app = test_app();
7033        app.current_service = Service::S3Buckets;
7034        app.service_selected = true;
7035        app.mode = Mode::Normal;
7036        app.s3_state.current_bucket = Some("test-bucket".to_string());
7037
7038        // Add parent folder
7039        app.s3_state.objects = vec![S3Object {
7040            key: "parent/".to_string(),
7041            size: 0,
7042            last_modified: "2024-01-01T00:00:00Z".to_string(),
7043            is_prefix: true,
7044            storage_class: String::new(),
7045        }];
7046
7047        // Expand parent
7048        app.s3_state.expanded_prefixes.insert("parent/".to_string());
7049        app.s3_state.prefix_preview.insert(
7050            "parent/".to_string(),
7051            vec![S3Object {
7052                key: "parent/child/".to_string(),
7053                size: 0,
7054                last_modified: "2024-01-01T00:00:00Z".to_string(),
7055                is_prefix: true,
7056                storage_class: String::new(),
7057            }],
7058        );
7059
7060        // Select the nested folder (index 1)
7061        app.s3_state.selected_object = 1;
7062
7063        // Drill into it (simulate pressing Enter)
7064        app.handle_action(Action::Select);
7065
7066        // Should navigate into the folder
7067        assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
7068        assert!(app.s3_state.buckets.loading); // Should trigger load
7069    }
7070
7071    #[test]
7072    fn test_s3_esc_pops_navigation_stack() {
7073        let mut app = test_app();
7074        app.current_service = Service::S3Buckets;
7075        app.s3_state.current_bucket = Some("test-bucket".to_string());
7076        app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
7077
7078        // Press Esc - should pop from stack
7079        app.handle_action(Action::GoBack);
7080        assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
7081        assert!(app.s3_state.buckets.loading);
7082
7083        // Press Esc again - should pop to bucket root
7084        app.s3_state.buckets.loading = false;
7085        app.handle_action(Action::GoBack);
7086        assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
7087        assert!(app.s3_state.buckets.loading);
7088
7089        // Press Esc again - should exit bucket
7090        app.s3_state.buckets.loading = false;
7091        app.handle_action(Action::GoBack);
7092        assert_eq!(app.s3_state.current_bucket, None);
7093    }
7094
7095    #[test]
7096    fn test_s3_esc_from_bucket_root_exits() {
7097        let mut app = test_app();
7098        app.current_service = Service::S3Buckets;
7099        app.s3_state.current_bucket = Some("test-bucket".to_string());
7100        app.s3_state.prefix_stack = vec![];
7101
7102        // Press Esc from bucket root - should exit bucket
7103        app.handle_action(Action::GoBack);
7104        assert_eq!(app.s3_state.current_bucket, None);
7105        assert_eq!(app.s3_state.objects.len(), 0);
7106    }
7107
7108    #[test]
7109    fn test_s3_drill_into_nested_prefix_from_bucket_list() {
7110        let mut app = test_app();
7111        app.current_service = Service::S3Buckets;
7112        app.service_selected = true;
7113        app.mode = Mode::Normal;
7114
7115        // Setup bucket with nested preview
7116        app.s3_state.buckets.items = vec![S3Bucket {
7117            name: "test-bucket".to_string(),
7118            region: "us-east-1".to_string(),
7119            creation_date: "2024-01-01".to_string(),
7120        }];
7121
7122        // Expand bucket to show first-level prefix
7123        app.s3_state
7124            .expanded_prefixes
7125            .insert("test-bucket".to_string());
7126        app.s3_state.bucket_preview.insert(
7127            "test-bucket".to_string(),
7128            vec![S3Object {
7129                key: "parent/".to_string(),
7130                size: 0,
7131                last_modified: "2024-01-01".to_string(),
7132                is_prefix: true,
7133                storage_class: String::new(),
7134            }],
7135        );
7136
7137        // Expand parent to show nested prefix
7138        app.s3_state.expanded_prefixes.insert("parent/".to_string());
7139        app.s3_state.prefix_preview.insert(
7140            "parent/".to_string(),
7141            vec![S3Object {
7142                key: "parent/child/".to_string(),
7143                size: 0,
7144                last_modified: "2024-01-01".to_string(),
7145                is_prefix: true,
7146                storage_class: String::new(),
7147            }],
7148        );
7149
7150        // Select nested prefix (row 2: bucket, parent, nested)
7151        app.s3_state.selected_row = 2;
7152
7153        // Drill into nested prefix
7154        app.handle_action(Action::Select);
7155
7156        // Should have both parent and child in stack
7157        assert_eq!(
7158            app.s3_state.prefix_stack,
7159            vec!["parent/".to_string(), "parent/child/".to_string()]
7160        );
7161        assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
7162        assert!(app.s3_state.buckets.loading);
7163
7164        // Now press Esc - should go back to parent
7165        app.s3_state.buckets.loading = false;
7166        app.handle_action(Action::GoBack);
7167        assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
7168        assert!(app.s3_state.buckets.loading);
7169
7170        // Press Esc again - should go to bucket root
7171        app.s3_state.buckets.loading = false;
7172        app.handle_action(Action::GoBack);
7173        assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
7174        assert!(app.s3_state.buckets.loading);
7175
7176        // Press Esc again - should exit bucket
7177        app.s3_state.buckets.loading = false;
7178        app.handle_action(Action::GoBack);
7179        assert_eq!(app.s3_state.current_bucket, None);
7180    }
7181
7182    #[test]
7183    fn test_region_picker_fuzzy_filter() {
7184        let mut app = test_app();
7185        app.region_latencies.insert("us-east-1".to_string(), 10);
7186        app.region_filter = "vir".to_string();
7187        let filtered = app.get_filtered_regions();
7188        assert!(filtered.iter().any(|r| r.code == "us-east-1"));
7189    }
7190
7191    #[test]
7192    fn test_profile_picker_loads_profiles() {
7193        let profiles = App::load_aws_profiles();
7194        // Should at least have default profile or be empty if no config
7195        assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
7196    }
7197
7198    #[test]
7199    fn test_profile_with_region_uses_it() {
7200        let mut app = test_app_no_region();
7201        app.available_profiles = vec![AwsProfile {
7202            name: "test-profile".to_string(),
7203            region: Some("eu-west-1".to_string()),
7204            account: Some("123456789".to_string()),
7205            role_arn: None,
7206            source_profile: None,
7207        }];
7208        app.profile_picker_selected = 0;
7209        app.mode = Mode::ProfilePicker;
7210
7211        // Simulate selecting the profile
7212        let filtered = app.get_filtered_profiles();
7213        if let Some(profile) = filtered.first() {
7214            let profile_name = profile.name.clone();
7215            let profile_region = profile.region.clone();
7216
7217            app.profile = profile_name;
7218            if let Some(region) = profile_region {
7219                app.region = region;
7220            }
7221        }
7222
7223        assert_eq!(app.profile, "test-profile");
7224        assert_eq!(app.region, "eu-west-1");
7225    }
7226
7227    #[test]
7228    fn test_profile_without_region_keeps_unknown() {
7229        let mut app = test_app_no_region();
7230        let initial_region = app.region.clone();
7231
7232        app.available_profiles = vec![AwsProfile {
7233            name: "test-profile".to_string(),
7234            region: None,
7235            account: None,
7236            role_arn: None,
7237            source_profile: None,
7238        }];
7239        app.profile_picker_selected = 0;
7240        app.mode = Mode::ProfilePicker;
7241
7242        let filtered = app.get_filtered_profiles();
7243        if let Some(profile) = filtered.first() {
7244            let profile_name = profile.name.clone();
7245            let profile_region = profile.region.clone();
7246
7247            app.profile = profile_name;
7248            if let Some(region) = profile_region {
7249                app.region = region;
7250            }
7251        }
7252
7253        assert_eq!(app.profile, "test-profile");
7254        assert_eq!(app.region, initial_region); // Should keep initial region
7255    }
7256
7257    #[test]
7258    fn test_region_selection_closes_all_tabs() {
7259        let mut app = test_app();
7260
7261        // Add some tabs
7262        app.tabs.push(Tab {
7263            service: Service::CloudWatchLogGroups,
7264            title: "CloudWatch".to_string(),
7265            breadcrumb: "CloudWatch".to_string(),
7266        });
7267        app.tabs.push(Tab {
7268            service: Service::S3Buckets,
7269            title: "S3".to_string(),
7270            breadcrumb: "S3".to_string(),
7271        });
7272        app.service_selected = true;
7273        app.current_tab = 1;
7274
7275        // Add latency for region
7276        app.region_latencies.insert("eu-west-1".to_string(), 50);
7277
7278        // Simulate selecting a different region
7279        app.mode = Mode::RegionPicker;
7280        app.region_picker_selected = 0;
7281
7282        let filtered = app.get_filtered_regions();
7283        if let Some(region) = filtered.first() {
7284            app.region = region.code.to_string();
7285            app.tabs.clear();
7286            app.current_tab = 0;
7287            app.service_selected = false;
7288            app.mode = Mode::Normal;
7289        }
7290
7291        assert_eq!(app.tabs.len(), 0);
7292        assert_eq!(app.current_tab, 0);
7293        assert!(!app.service_selected);
7294        assert_eq!(app.region, "eu-west-1");
7295    }
7296
7297    #[test]
7298    fn test_region_picker_can_be_closed_without_selection() {
7299        let mut app = test_app();
7300        let initial_region = app.region.clone();
7301
7302        app.mode = Mode::RegionPicker;
7303
7304        // Close without selecting (Esc)
7305        app.mode = Mode::Normal;
7306
7307        // Region should not change
7308        assert_eq!(app.region, initial_region);
7309    }
7310
7311    #[test]
7312    fn test_session_filter_works() {
7313        let mut app = test_app();
7314
7315        app.sessions = vec![
7316            crate::session::Session {
7317                id: "1".to_string(),
7318                timestamp: "2024-01-01".to_string(),
7319                profile: "prod-profile".to_string(),
7320                region: "us-east-1".to_string(),
7321                account_id: "123456789".to_string(),
7322                role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
7323                tabs: vec![],
7324            },
7325            crate::session::Session {
7326                id: "2".to_string(),
7327                timestamp: "2024-01-02".to_string(),
7328                profile: "dev-profile".to_string(),
7329                region: "eu-west-1".to_string(),
7330                account_id: "987654321".to_string(),
7331                role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
7332                tabs: vec![],
7333            },
7334        ];
7335
7336        // Filter by profile
7337        app.session_filter = "prod".to_string();
7338        let filtered = app.get_filtered_sessions();
7339        assert_eq!(filtered.len(), 1);
7340        assert_eq!(filtered[0].profile, "prod-profile");
7341
7342        // Filter by region
7343        app.session_filter = "eu".to_string();
7344        let filtered = app.get_filtered_sessions();
7345        assert_eq!(filtered.len(), 1);
7346        assert_eq!(filtered[0].region, "eu-west-1");
7347
7348        // No filter
7349        app.session_filter.clear();
7350        let filtered = app.get_filtered_sessions();
7351        assert_eq!(filtered.len(), 2);
7352    }
7353
7354    #[test]
7355    fn test_profile_picker_shows_account() {
7356        let mut app = test_app_no_region();
7357        app.available_profiles = vec![AwsProfile {
7358            name: "test-profile".to_string(),
7359            region: Some("us-east-1".to_string()),
7360            account: Some("123456789".to_string()),
7361            role_arn: None,
7362            source_profile: None,
7363        }];
7364
7365        let filtered = app.get_filtered_profiles();
7366        assert_eq!(filtered.len(), 1);
7367        assert_eq!(filtered[0].account, Some("123456789".to_string()));
7368    }
7369
7370    #[test]
7371    fn test_profile_without_account() {
7372        let mut app = test_app_no_region();
7373        app.available_profiles = vec![AwsProfile {
7374            name: "test-profile".to_string(),
7375            region: Some("us-east-1".to_string()),
7376            account: None,
7377            role_arn: None,
7378            source_profile: None,
7379        }];
7380
7381        let filtered = app.get_filtered_profiles();
7382        assert_eq!(filtered.len(), 1);
7383        assert_eq!(filtered[0].account, None);
7384    }
7385
7386    #[test]
7387    fn test_profile_with_all_fields() {
7388        let mut app = test_app_no_region();
7389        app.available_profiles = vec![AwsProfile {
7390            name: "prod-profile".to_string(),
7391            region: Some("us-west-2".to_string()),
7392            account: Some("123456789".to_string()),
7393            role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
7394            source_profile: Some("base-profile".to_string()),
7395        }];
7396
7397        let filtered = app.get_filtered_profiles();
7398        assert_eq!(filtered.len(), 1);
7399        assert_eq!(filtered[0].name, "prod-profile");
7400        assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
7401        assert_eq!(filtered[0].account, Some("123456789".to_string()));
7402        assert_eq!(
7403            filtered[0].role_arn,
7404            Some("arn:aws:iam::123456789:role/AdminRole".to_string())
7405        );
7406        assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
7407    }
7408
7409    #[test]
7410    fn test_profile_filter_by_source_profile() {
7411        let mut app = test_app_no_region();
7412        app.available_profiles = vec![
7413            AwsProfile {
7414                name: "profile1".to_string(),
7415                region: None,
7416                account: None,
7417                role_arn: None,
7418                source_profile: Some("base".to_string()),
7419            },
7420            AwsProfile {
7421                name: "profile2".to_string(),
7422                region: None,
7423                account: None,
7424                role_arn: None,
7425                source_profile: Some("other".to_string()),
7426            },
7427        ];
7428
7429        app.profile_filter = "base".to_string();
7430        let filtered = app.get_filtered_profiles();
7431        assert_eq!(filtered.len(), 1);
7432        assert_eq!(filtered[0].name, "profile1");
7433    }
7434
7435    #[test]
7436    fn test_profile_filter_by_role() {
7437        let mut app = test_app_no_region();
7438        app.available_profiles = vec![
7439            AwsProfile {
7440                name: "admin-profile".to_string(),
7441                region: None,
7442                account: None,
7443                role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
7444                source_profile: None,
7445            },
7446            AwsProfile {
7447                name: "dev-profile".to_string(),
7448                region: None,
7449                account: None,
7450                role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
7451                source_profile: None,
7452            },
7453        ];
7454
7455        app.profile_filter = "Admin".to_string();
7456        let filtered = app.get_filtered_profiles();
7457        assert_eq!(filtered.len(), 1);
7458        assert_eq!(filtered[0].name, "admin-profile");
7459    }
7460
7461    #[test]
7462    fn test_profiles_sorted_by_name() {
7463        let mut app = test_app_no_region();
7464        app.available_profiles = vec![
7465            AwsProfile {
7466                name: "zebra-profile".to_string(),
7467                region: None,
7468                account: None,
7469                role_arn: None,
7470                source_profile: None,
7471            },
7472            AwsProfile {
7473                name: "alpha-profile".to_string(),
7474                region: None,
7475                account: None,
7476                role_arn: None,
7477                source_profile: None,
7478            },
7479            AwsProfile {
7480                name: "beta-profile".to_string(),
7481                region: None,
7482                account: None,
7483                role_arn: None,
7484                source_profile: None,
7485            },
7486        ];
7487
7488        let filtered = app.get_filtered_profiles();
7489        assert_eq!(filtered.len(), 3);
7490        assert_eq!(filtered[0].name, "alpha-profile");
7491        assert_eq!(filtered[1].name, "beta-profile");
7492        assert_eq!(filtered[2].name, "zebra-profile");
7493    }
7494
7495    #[test]
7496    fn test_profile_with_role_arn() {
7497        let mut app = test_app_no_region();
7498        app.available_profiles = vec![AwsProfile {
7499            name: "role-profile".to_string(),
7500            region: Some("us-east-1".to_string()),
7501            account: Some("123456789".to_string()),
7502            role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
7503            source_profile: None,
7504        }];
7505
7506        let filtered = app.get_filtered_profiles();
7507        assert_eq!(filtered.len(), 1);
7508        assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
7509    }
7510
7511    #[test]
7512    fn test_profile_with_user_arn() {
7513        let mut app = test_app_no_region();
7514        app.available_profiles = vec![AwsProfile {
7515            name: "user-profile".to_string(),
7516            region: Some("us-east-1".to_string()),
7517            account: Some("123456789".to_string()),
7518            role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
7519            source_profile: None,
7520        }];
7521
7522        let filtered = app.get_filtered_profiles();
7523        assert_eq!(filtered.len(), 1);
7524        assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
7525    }
7526
7527    #[test]
7528    fn test_filtered_profiles_also_sorted() {
7529        let mut app = test_app_no_region();
7530        app.available_profiles = vec![
7531            AwsProfile {
7532                name: "prod-zebra".to_string(),
7533                region: Some("us-east-1".to_string()),
7534                account: None,
7535                role_arn: None,
7536                source_profile: None,
7537            },
7538            AwsProfile {
7539                name: "prod-alpha".to_string(),
7540                region: Some("us-east-1".to_string()),
7541                account: None,
7542                role_arn: None,
7543                source_profile: None,
7544            },
7545            AwsProfile {
7546                name: "dev-profile".to_string(),
7547                region: Some("us-west-2".to_string()),
7548                account: None,
7549                role_arn: None,
7550                source_profile: None,
7551            },
7552        ];
7553
7554        app.profile_filter = "prod".to_string();
7555        let filtered = app.get_filtered_profiles();
7556        assert_eq!(filtered.len(), 2);
7557        assert_eq!(filtered[0].name, "prod-alpha");
7558        assert_eq!(filtered[1].name, "prod-zebra");
7559    }
7560
7561    #[test]
7562    fn test_profile_picker_has_all_columns() {
7563        let mut app = test_app_no_region();
7564        app.available_profiles = vec![AwsProfile {
7565            name: "test".to_string(),
7566            region: Some("us-east-1".to_string()),
7567            account: Some("123456789".to_string()),
7568            role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
7569            source_profile: Some("base".to_string()),
7570        }];
7571
7572        let filtered = app.get_filtered_profiles();
7573        assert_eq!(filtered.len(), 1);
7574        assert!(filtered[0].name == "test");
7575        assert!(filtered[0].region.is_some());
7576        assert!(filtered[0].account.is_some());
7577        assert!(filtered[0].role_arn.is_some());
7578        assert!(filtered[0].source_profile.is_some());
7579    }
7580
7581    #[test]
7582    fn test_session_picker_shows_tab_count() {
7583        let mut app = test_app_no_region();
7584        app.sessions = vec![crate::session::Session {
7585            id: "1".to_string(),
7586            timestamp: "2024-01-01".to_string(),
7587            profile: "test".to_string(),
7588            region: "us-east-1".to_string(),
7589            account_id: "123".to_string(),
7590            role_arn: String::new(),
7591            tabs: vec![
7592                crate::session::SessionTab {
7593                    service: "CloudWatch".to_string(),
7594                    title: "Logs".to_string(),
7595                    breadcrumb: String::new(),
7596                    filter: None,
7597                    selected_item: None,
7598                },
7599                crate::session::SessionTab {
7600                    service: "S3".to_string(),
7601                    title: "Buckets".to_string(),
7602                    breadcrumb: String::new(),
7603                    filter: None,
7604                    selected_item: None,
7605                },
7606            ],
7607        }];
7608
7609        let filtered = app.get_filtered_sessions();
7610        assert_eq!(filtered.len(), 1);
7611        assert_eq!(filtered[0].tabs.len(), 2);
7612    }
7613
7614    #[test]
7615    fn test_start_background_data_fetch_loads_profiles() {
7616        let mut app = test_app_no_region();
7617        assert!(app.available_profiles.is_empty());
7618
7619        // Load profiles synchronously
7620        app.available_profiles = App::load_aws_profiles();
7621
7622        // Profiles should be loaded
7623        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7624    }
7625
7626    #[test]
7627    fn test_refresh_in_profile_picker() {
7628        let mut app = test_app_no_region();
7629        app.mode = Mode::ProfilePicker;
7630        app.available_profiles = vec![AwsProfile {
7631            name: "test".to_string(),
7632            region: None,
7633            account: None,
7634            role_arn: None,
7635            source_profile: None,
7636        }];
7637
7638        app.handle_action(Action::Refresh);
7639
7640        // Should set loading state
7641        assert!(app.log_groups_state.loading);
7642        assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
7643    }
7644
7645    #[test]
7646    fn test_refresh_sets_loading_for_profile_picker() {
7647        let mut app = test_app_no_region();
7648        app.mode = Mode::ProfilePicker;
7649
7650        assert!(!app.log_groups_state.loading);
7651
7652        app.handle_action(Action::Refresh);
7653
7654        assert!(app.log_groups_state.loading);
7655    }
7656
7657    #[test]
7658    fn test_profiles_loaded_on_demand() {
7659        let mut app = test_app_no_region();
7660
7661        // Profiles not loaded by default
7662        assert!(app.available_profiles.is_empty());
7663
7664        // Load on demand
7665        app.available_profiles = App::load_aws_profiles();
7666
7667        // Now loaded
7668        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7669    }
7670
7671    #[test]
7672    fn test_profile_accounts_not_fetched_automatically() {
7673        let mut app = test_app_no_region();
7674        app.available_profiles = App::load_aws_profiles();
7675
7676        // Accounts should not be populated automatically
7677        for profile in &app.available_profiles {
7678            // Account may or may not be set depending on what's in config
7679            // But we're not fetching them automatically
7680            assert!(profile.account.is_none() || profile.account.is_some());
7681        }
7682    }
7683
7684    #[test]
7685    fn test_ctrl_r_triggers_account_fetch() {
7686        let mut app = test_app_no_region();
7687        app.mode = Mode::ProfilePicker;
7688        app.available_profiles = vec![AwsProfile {
7689            name: "test".to_string(),
7690            region: Some("us-east-1".to_string()),
7691            account: None,
7692            role_arn: None,
7693            source_profile: None,
7694        }];
7695
7696        // Before refresh, account is None
7697        assert!(app.available_profiles[0].account.is_none());
7698
7699        // Trigger refresh
7700        app.handle_action(Action::Refresh);
7701
7702        // Should set loading state (actual fetch happens in main.rs event loop)
7703        assert!(app.log_groups_state.loading);
7704    }
7705
7706    #[test]
7707    fn test_refresh_in_region_picker() {
7708        let mut app = test_app_no_region();
7709        app.mode = Mode::RegionPicker;
7710
7711        let initial_latencies = app.region_latencies.len();
7712        app.handle_action(Action::Refresh);
7713
7714        // Latencies should be cleared and remeasured
7715        assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
7716    }
7717
7718    #[test]
7719    fn test_refresh_in_session_picker() {
7720        let mut app = test_app_no_region();
7721        app.mode = Mode::SessionPicker;
7722        app.sessions = vec![];
7723
7724        app.handle_action(Action::Refresh);
7725
7726        // Sessions should be reloaded (may be empty if no saved sessions)
7727        assert!(app.sessions.is_empty() || !app.sessions.is_empty());
7728    }
7729
7730    #[test]
7731    fn test_session_picker_selection() {
7732        let mut app = test_app();
7733
7734        app.sessions = vec![crate::session::Session {
7735            id: "1".to_string(),
7736            timestamp: "2024-01-01".to_string(),
7737            profile: "prod-profile".to_string(),
7738            region: "us-west-2".to_string(),
7739            account_id: "123456789".to_string(),
7740            role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
7741            tabs: vec![crate::session::SessionTab {
7742                service: "CloudWatchLogGroups".to_string(),
7743                title: "Log Groups".to_string(),
7744                breadcrumb: "CloudWatch > Log Groups".to_string(),
7745                filter: Some("test".to_string()),
7746                selected_item: None,
7747            }],
7748        }];
7749
7750        app.mode = Mode::SessionPicker;
7751        app.session_picker_selected = 0;
7752
7753        // Simulate selecting the session
7754        app.handle_action(Action::Select);
7755
7756        assert_eq!(app.mode, Mode::Normal);
7757        assert_eq!(app.profile, "prod-profile");
7758        assert_eq!(app.region, "us-west-2");
7759        assert_eq!(app.config.account_id, "123456789");
7760        assert_eq!(app.tabs.len(), 1);
7761        assert_eq!(app.tabs[0].title, "Log Groups");
7762    }
7763
7764    #[test]
7765    fn test_save_session_creates_session() {
7766        let mut app =
7767            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7768        app.config.account_id = "123456789".to_string();
7769        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7770
7771        app.tabs.push(Tab {
7772            service: Service::CloudWatchLogGroups,
7773            title: "Log Groups".to_string(),
7774            breadcrumb: "CloudWatch > Log Groups".to_string(),
7775        });
7776
7777        app.save_current_session();
7778
7779        assert!(app.current_session.is_some());
7780        let session = app.current_session.clone().unwrap();
7781        assert_eq!(session.profile, "test-profile");
7782        assert_eq!(session.region, "us-east-1");
7783        assert_eq!(session.account_id, "123456789");
7784        assert_eq!(session.tabs.len(), 1);
7785
7786        // Cleanup
7787        let _ = session.delete();
7788    }
7789
7790    #[test]
7791    fn test_save_session_updates_existing() {
7792        let mut app =
7793            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7794        app.config.account_id = "123456789".to_string();
7795        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7796
7797        app.current_session = Some(crate::session::Session {
7798            id: "existing".to_string(),
7799            timestamp: "2024-01-01".to_string(),
7800            profile: "test-profile".to_string(),
7801            region: "us-east-1".to_string(),
7802            account_id: "123456789".to_string(),
7803            role_arn: "arn:aws:iam::123456789:role/test".to_string(),
7804            tabs: vec![],
7805        });
7806
7807        app.tabs.push(Tab {
7808            service: Service::CloudWatchLogGroups,
7809            title: "Log Groups".to_string(),
7810            breadcrumb: "CloudWatch > Log Groups".to_string(),
7811        });
7812
7813        app.save_current_session();
7814
7815        let session = app.current_session.clone().unwrap();
7816        assert_eq!(session.id, "existing");
7817        assert_eq!(session.tabs.len(), 1);
7818
7819        // Cleanup
7820        let _ = session.delete();
7821    }
7822
7823    #[test]
7824    fn test_save_session_skips_empty_tabs() {
7825        let mut app =
7826            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7827        app.config.account_id = "123456789".to_string();
7828
7829        app.save_current_session();
7830
7831        assert!(app.current_session.is_none());
7832    }
7833
7834    #[test]
7835    fn test_save_session_deletes_when_tabs_closed() {
7836        let mut app =
7837            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7838        app.config.account_id = "123456789".to_string();
7839        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7840
7841        // Create a session with tabs
7842        app.current_session = Some(crate::session::Session {
7843            id: "test_delete".to_string(),
7844            timestamp: "2024-01-01 10:00:00 UTC".to_string(),
7845            profile: "test-profile".to_string(),
7846            region: "us-east-1".to_string(),
7847            account_id: "123456789".to_string(),
7848            role_arn: "arn:aws:iam::123456789:role/test".to_string(),
7849            tabs: vec![],
7850        });
7851
7852        // Save with no tabs should delete session
7853        app.save_current_session();
7854
7855        assert!(app.current_session.is_none());
7856    }
7857
7858    #[test]
7859    fn test_closing_all_tabs_deletes_session() {
7860        let mut app =
7861            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7862        app.config.account_id = "123456789".to_string();
7863        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7864
7865        // Add a tab
7866        app.tabs.push(Tab {
7867            service: Service::CloudWatchLogGroups,
7868            title: "Log Groups".to_string(),
7869            breadcrumb: "CloudWatch > Log Groups".to_string(),
7870        });
7871
7872        // Create session
7873        app.save_current_session();
7874        assert!(app.current_session.is_some());
7875        let session_id = app.current_session.as_ref().unwrap().id.clone();
7876
7877        // Close all tabs
7878        app.tabs.clear();
7879
7880        // Save should delete session
7881        app.save_current_session();
7882        assert!(app.current_session.is_none());
7883
7884        // Cleanup - ensure session file is deleted
7885        let _ = crate::session::Session::load(&session_id).map(|s| s.delete());
7886    }
7887
7888    #[test]
7889    fn test_credential_error_opens_profile_picker() {
7890        // Simulate what main.rs does on credential error
7891        let mut app = App::new_without_client("default".to_string(), None);
7892        let error_str = "Unable to load credentials from any source";
7893
7894        if error_str.contains("credentials") {
7895            app.available_profiles = App::load_aws_profiles();
7896            app.mode = Mode::ProfilePicker;
7897        }
7898
7899        assert_eq!(app.mode, Mode::ProfilePicker);
7900        // Should have loaded profiles
7901        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7902    }
7903
7904    #[test]
7905    fn test_non_credential_error_shows_error_modal() {
7906        let mut app = App::new_without_client("default".to_string(), None);
7907        let error_str = "Network timeout";
7908
7909        if !error_str.contains("credentials") {
7910            app.error_message = Some(error_str.to_string());
7911            app.mode = Mode::ErrorModal;
7912        }
7913
7914        assert_eq!(app.mode, Mode::ErrorModal);
7915        assert!(app.error_message.is_some());
7916    }
7917
7918    #[tokio::test]
7919    async fn test_profile_selection_loads_credentials() {
7920        // Set a valid AWS profile if available
7921        std::env::set_var("AWS_PROFILE", "default");
7922
7923        // Try to create app with profile
7924        let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
7925
7926        if let Ok(app) = result {
7927            // If credentials are available, verify they're loaded
7928            assert!(!app.config.account_id.is_empty());
7929            assert!(!app.config.role_arn.is_empty());
7930            assert_eq!(app.profile, "default");
7931            assert_eq!(app.config.region, "us-east-1");
7932        }
7933        // If no credentials, test passes (can't test without real AWS creds)
7934    }
7935
7936    #[test]
7937    fn test_new_app_shows_service_picker_with_no_tabs() {
7938        let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
7939
7940        // Should start with no service selected
7941        assert!(!app.service_selected);
7942        // Should be in ServicePicker mode (service picker)
7943        assert_eq!(app.mode, Mode::ServicePicker);
7944        // Should have no tabs
7945        assert!(app.tabs.is_empty());
7946    }
7947
7948    #[tokio::test]
7949    async fn test_aws_profile_env_var_read_before_config_load() {
7950        // This test verifies the bug: AWS_PROFILE should be read and used
7951        std::env::set_var("AWS_PROFILE", "test-profile");
7952
7953        // Simulate what happens in App::new
7954        let profile_name = None
7955            .or_else(|| std::env::var("AWS_PROFILE").ok())
7956            .unwrap_or_else(|| "default".to_string());
7957
7958        // Should have read test-profile from env
7959        assert_eq!(profile_name, "test-profile");
7960
7961        // Now set it (redundant but that's what the code does)
7962        std::env::set_var("AWS_PROFILE", &profile_name);
7963
7964        // Verify it's still set
7965        assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
7966
7967        std::env::remove_var("AWS_PROFILE");
7968    }
7969
7970    #[test]
7971    fn test_next_preferences_cloudformation() {
7972        let mut app = test_app();
7973        app.current_service = Service::CloudFormationStacks;
7974        app.mode = Mode::ColumnSelector;
7975        app.column_selector_index = 0;
7976
7977        // Should jump to PageSize section
7978        let page_size_idx = app.all_cfn_columns.len() + 2;
7979        app.handle_action(Action::NextPreferences);
7980        assert_eq!(app.column_selector_index, page_size_idx);
7981
7982        // Should wrap back to Columns
7983        app.handle_action(Action::NextPreferences);
7984        assert_eq!(app.column_selector_index, 0);
7985    }
7986
7987    #[test]
7988    fn test_next_preferences_lambda_functions() {
7989        let mut app = test_app();
7990        app.current_service = Service::LambdaFunctions;
7991        app.mode = Mode::ColumnSelector;
7992        app.column_selector_index = 0;
7993
7994        let page_size_idx = app.lambda_state.all_columns.len() + 2;
7995        app.handle_action(Action::NextPreferences);
7996        assert_eq!(app.column_selector_index, page_size_idx);
7997
7998        app.handle_action(Action::NextPreferences);
7999        assert_eq!(app.column_selector_index, 0);
8000    }
8001
8002    #[test]
8003    fn test_next_preferences_lambda_applications() {
8004        let mut app = test_app();
8005        app.current_service = Service::LambdaApplications;
8006        app.mode = Mode::ColumnSelector;
8007        app.column_selector_index = 0;
8008
8009        let page_size_idx = app.all_lambda_application_columns.len() + 2;
8010        app.handle_action(Action::NextPreferences);
8011        assert_eq!(app.column_selector_index, page_size_idx);
8012
8013        app.handle_action(Action::NextPreferences);
8014        assert_eq!(app.column_selector_index, 0);
8015    }
8016
8017    #[test]
8018    fn test_next_preferences_ecr_images() {
8019        let mut app = test_app();
8020        app.current_service = Service::EcrRepositories;
8021        app.ecr_state.current_repository = Some("test-repo".to_string());
8022        app.mode = Mode::ColumnSelector;
8023        app.column_selector_index = 0;
8024
8025        let page_size_idx = app.all_ecr_image_columns.len() + 2;
8026        app.handle_action(Action::NextPreferences);
8027        assert_eq!(app.column_selector_index, page_size_idx);
8028
8029        app.handle_action(Action::NextPreferences);
8030        assert_eq!(app.column_selector_index, 0);
8031    }
8032
8033    #[test]
8034    fn test_cloudformation_next_item() {
8035        let mut app = test_app();
8036        app.current_service = Service::CloudFormationStacks;
8037        app.service_selected = true;
8038        app.mode = Mode::Normal;
8039        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8040        app.cfn_state.table.items = vec![
8041            CfnStack {
8042                name: "stack1".to_string(),
8043                stack_id: "id1".to_string(),
8044                status: "CREATE_COMPLETE".to_string(),
8045                created_time: "2024-01-01".to_string(),
8046                updated_time: String::new(),
8047                deleted_time: String::new(),
8048                drift_status: String::new(),
8049                last_drift_check_time: String::new(),
8050                status_reason: String::new(),
8051                description: String::new(),
8052                detailed_status: String::new(),
8053                root_stack: String::new(),
8054                parent_stack: String::new(),
8055                termination_protection: false,
8056                iam_role: String::new(),
8057                tags: Vec::new(),
8058                stack_policy: String::new(),
8059                rollback_monitoring_time: String::new(),
8060                rollback_alarms: Vec::new(),
8061                notification_arns: Vec::new(),
8062            },
8063            CfnStack {
8064                name: "stack2".to_string(),
8065                stack_id: "id2".to_string(),
8066                status: "UPDATE_COMPLETE".to_string(),
8067                created_time: "2024-01-02".to_string(),
8068                updated_time: String::new(),
8069                deleted_time: String::new(),
8070                drift_status: String::new(),
8071                last_drift_check_time: String::new(),
8072                status_reason: String::new(),
8073                description: String::new(),
8074                detailed_status: String::new(),
8075                root_stack: String::new(),
8076                parent_stack: String::new(),
8077                termination_protection: false,
8078                iam_role: String::new(),
8079                tags: Vec::new(),
8080                stack_policy: String::new(),
8081                rollback_monitoring_time: String::new(),
8082                rollback_alarms: Vec::new(),
8083                notification_arns: Vec::new(),
8084            },
8085        ];
8086        app.cfn_state.table.reset();
8087
8088        app.handle_action(Action::NextItem);
8089        assert_eq!(app.cfn_state.table.selected, 1);
8090
8091        app.handle_action(Action::NextItem);
8092        assert_eq!(app.cfn_state.table.selected, 1); // At max
8093    }
8094
8095    #[test]
8096    fn test_cloudformation_prev_item() {
8097        let mut app = test_app();
8098        app.current_service = Service::CloudFormationStacks;
8099        app.service_selected = true;
8100        app.mode = Mode::Normal;
8101        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8102        app.cfn_state.table.items = vec![
8103            CfnStack {
8104                name: "stack1".to_string(),
8105                stack_id: "id1".to_string(),
8106                status: "CREATE_COMPLETE".to_string(),
8107                created_time: "2024-01-01".to_string(),
8108                updated_time: String::new(),
8109                deleted_time: String::new(),
8110                drift_status: String::new(),
8111                last_drift_check_time: String::new(),
8112                status_reason: String::new(),
8113                description: String::new(),
8114                detailed_status: String::new(),
8115                root_stack: String::new(),
8116                parent_stack: String::new(),
8117                termination_protection: false,
8118                iam_role: String::new(),
8119                tags: Vec::new(),
8120                stack_policy: String::new(),
8121                rollback_monitoring_time: String::new(),
8122                rollback_alarms: Vec::new(),
8123                notification_arns: Vec::new(),
8124            },
8125            CfnStack {
8126                name: "stack2".to_string(),
8127                stack_id: "id2".to_string(),
8128                status: "UPDATE_COMPLETE".to_string(),
8129                created_time: "2024-01-02".to_string(),
8130                updated_time: String::new(),
8131                deleted_time: String::new(),
8132                drift_status: String::new(),
8133                last_drift_check_time: String::new(),
8134                status_reason: String::new(),
8135                description: String::new(),
8136                detailed_status: String::new(),
8137                root_stack: String::new(),
8138                parent_stack: String::new(),
8139                termination_protection: false,
8140                iam_role: String::new(),
8141                tags: Vec::new(),
8142                stack_policy: String::new(),
8143                rollback_monitoring_time: String::new(),
8144                rollback_alarms: Vec::new(),
8145                notification_arns: Vec::new(),
8146            },
8147        ];
8148        app.cfn_state.table.selected = 1;
8149
8150        app.handle_action(Action::PrevItem);
8151        assert_eq!(app.cfn_state.table.selected, 0);
8152
8153        app.handle_action(Action::PrevItem);
8154        assert_eq!(app.cfn_state.table.selected, 0); // At min
8155    }
8156
8157    #[test]
8158    fn test_cloudformation_page_down() {
8159        let mut app = test_app();
8160        app.current_service = Service::CloudFormationStacks;
8161        app.service_selected = true;
8162        app.mode = Mode::Normal;
8163        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8164
8165        // Create 20 stacks
8166        for i in 0..20 {
8167            app.cfn_state.table.items.push(CfnStack {
8168                name: format!("stack{}", i),
8169                stack_id: format!("id{}", i),
8170                status: "CREATE_COMPLETE".to_string(),
8171                created_time: format!("2024-01-{:02}", i + 1),
8172                updated_time: String::new(),
8173                deleted_time: String::new(),
8174                drift_status: String::new(),
8175                last_drift_check_time: String::new(),
8176                status_reason: String::new(),
8177                description: String::new(),
8178                detailed_status: String::new(),
8179                root_stack: String::new(),
8180                parent_stack: String::new(),
8181                termination_protection: false,
8182                iam_role: String::new(),
8183                tags: Vec::new(),
8184                stack_policy: String::new(),
8185                rollback_monitoring_time: String::new(),
8186                rollback_alarms: Vec::new(),
8187                notification_arns: Vec::new(),
8188            });
8189        }
8190        app.cfn_state.table.reset();
8191
8192        app.handle_action(Action::PageDown);
8193        assert_eq!(app.cfn_state.table.selected, 10);
8194
8195        app.handle_action(Action::PageDown);
8196        assert_eq!(app.cfn_state.table.selected, 19); // Clamped to max
8197    }
8198
8199    #[test]
8200    fn test_cloudformation_page_up() {
8201        let mut app = test_app();
8202        app.current_service = Service::CloudFormationStacks;
8203        app.service_selected = true;
8204        app.mode = Mode::Normal;
8205        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8206
8207        // Create 20 stacks
8208        for i in 0..20 {
8209            app.cfn_state.table.items.push(CfnStack {
8210                name: format!("stack{}", i),
8211                stack_id: format!("id{}", i),
8212                status: "CREATE_COMPLETE".to_string(),
8213                created_time: format!("2024-01-{:02}", i + 1),
8214                updated_time: String::new(),
8215                deleted_time: String::new(),
8216                drift_status: String::new(),
8217                last_drift_check_time: String::new(),
8218                status_reason: String::new(),
8219                description: String::new(),
8220                detailed_status: String::new(),
8221                root_stack: String::new(),
8222                parent_stack: String::new(),
8223                termination_protection: false,
8224                iam_role: String::new(),
8225                tags: Vec::new(),
8226                stack_policy: String::new(),
8227                rollback_monitoring_time: String::new(),
8228                rollback_alarms: Vec::new(),
8229                notification_arns: Vec::new(),
8230            });
8231        }
8232        app.cfn_state.table.selected = 15;
8233
8234        app.handle_action(Action::PageUp);
8235        assert_eq!(app.cfn_state.table.selected, 5);
8236
8237        app.handle_action(Action::PageUp);
8238        assert_eq!(app.cfn_state.table.selected, 0); // Clamped to min
8239    }
8240
8241    #[test]
8242    fn test_cloudformation_filter_input() {
8243        let mut app = test_app();
8244        app.current_service = Service::CloudFormationStacks;
8245        app.service_selected = true;
8246        app.mode = Mode::Normal;
8247
8248        app.handle_action(Action::StartFilter);
8249        assert_eq!(app.mode, Mode::FilterInput);
8250
8251        // Directly set filter (character input is handled in event loop, not actions)
8252        app.cfn_state.table.filter = "test".to_string();
8253        assert_eq!(app.cfn_state.table.filter, "test");
8254    }
8255
8256    #[test]
8257    fn test_cloudformation_filter_applies() {
8258        let mut app = test_app();
8259        app.current_service = Service::CloudFormationStacks;
8260        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8261        app.cfn_state.table.items = vec![
8262            CfnStack {
8263                name: "prod-stack".to_string(),
8264                stack_id: "id1".to_string(),
8265                status: "CREATE_COMPLETE".to_string(),
8266                created_time: "2024-01-01".to_string(),
8267                updated_time: String::new(),
8268                deleted_time: String::new(),
8269                drift_status: String::new(),
8270                last_drift_check_time: String::new(),
8271                status_reason: String::new(),
8272                description: "Production stack".to_string(),
8273                detailed_status: String::new(),
8274                root_stack: String::new(),
8275                parent_stack: String::new(),
8276                termination_protection: false,
8277                iam_role: String::new(),
8278                tags: Vec::new(),
8279                stack_policy: String::new(),
8280                rollback_monitoring_time: String::new(),
8281                rollback_alarms: Vec::new(),
8282                notification_arns: Vec::new(),
8283            },
8284            CfnStack {
8285                name: "dev-stack".to_string(),
8286                stack_id: "id2".to_string(),
8287                status: "UPDATE_COMPLETE".to_string(),
8288                created_time: "2024-01-02".to_string(),
8289                updated_time: String::new(),
8290                deleted_time: String::new(),
8291                drift_status: String::new(),
8292                last_drift_check_time: String::new(),
8293                status_reason: String::new(),
8294                description: "Development stack".to_string(),
8295                detailed_status: String::new(),
8296                root_stack: String::new(),
8297                parent_stack: String::new(),
8298                termination_protection: false,
8299                iam_role: String::new(),
8300                tags: Vec::new(),
8301                stack_policy: String::new(),
8302                rollback_monitoring_time: String::new(),
8303                rollback_alarms: Vec::new(),
8304                notification_arns: Vec::new(),
8305            },
8306        ];
8307        app.cfn_state.table.filter = "prod".to_string();
8308
8309        let filtered = app.filtered_cloudformation_stacks();
8310        assert_eq!(filtered.len(), 1);
8311        assert_eq!(filtered[0].name, "prod-stack");
8312    }
8313
8314    #[test]
8315    fn test_cloudformation_right_arrow_expands() {
8316        let mut app = test_app();
8317        app.current_service = Service::CloudFormationStacks;
8318        app.service_selected = true;
8319        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8320        app.cfn_state.table.items = vec![CfnStack {
8321            name: "test-stack".to_string(),
8322            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8323                .to_string(),
8324            status: "CREATE_COMPLETE".to_string(),
8325            created_time: "2024-01-01".to_string(),
8326            updated_time: String::new(),
8327            deleted_time: String::new(),
8328            drift_status: String::new(),
8329            last_drift_check_time: String::new(),
8330            status_reason: String::new(),
8331            description: "Test stack".to_string(),
8332            detailed_status: String::new(),
8333            root_stack: String::new(),
8334            parent_stack: String::new(),
8335            termination_protection: false,
8336            iam_role: String::new(),
8337            tags: Vec::new(),
8338            stack_policy: String::new(),
8339            rollback_monitoring_time: String::new(),
8340            rollback_alarms: Vec::new(),
8341            notification_arns: Vec::new(),
8342        }];
8343        app.cfn_state.table.reset();
8344
8345        assert_eq!(app.cfn_state.table.expanded_item, None);
8346
8347        app.handle_action(Action::NextPane);
8348        assert_eq!(app.cfn_state.table.expanded_item, Some(0));
8349    }
8350
8351    #[test]
8352    fn test_cloudformation_left_arrow_collapses() {
8353        let mut app = test_app();
8354        app.current_service = Service::CloudFormationStacks;
8355        app.service_selected = true;
8356        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8357        app.cfn_state.table.items = vec![CfnStack {
8358            name: "test-stack".to_string(),
8359            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8360                .to_string(),
8361            status: "CREATE_COMPLETE".to_string(),
8362            created_time: "2024-01-01".to_string(),
8363            updated_time: String::new(),
8364            deleted_time: String::new(),
8365            drift_status: String::new(),
8366            last_drift_check_time: String::new(),
8367            status_reason: String::new(),
8368            description: "Test stack".to_string(),
8369            detailed_status: String::new(),
8370            root_stack: String::new(),
8371            parent_stack: String::new(),
8372            termination_protection: false,
8373            iam_role: String::new(),
8374            tags: Vec::new(),
8375            stack_policy: String::new(),
8376            rollback_monitoring_time: String::new(),
8377            rollback_alarms: Vec::new(),
8378            notification_arns: Vec::new(),
8379        }];
8380        app.cfn_state.table.reset();
8381        app.cfn_state.table.expanded_item = Some(0);
8382
8383        app.handle_action(Action::PrevPane);
8384        assert_eq!(app.cfn_state.table.expanded_item, None);
8385    }
8386
8387    #[test]
8388    fn test_cloudformation_enter_drills_into_stack() {
8389        let mut app = test_app();
8390        app.current_service = Service::CloudFormationStacks;
8391        app.service_selected = true;
8392        app.mode = Mode::Normal;
8393        app.tabs = vec![Tab {
8394            service: Service::CloudFormationStacks,
8395            title: "CloudFormation > Stacks".to_string(),
8396            breadcrumb: "CloudFormation > Stacks".to_string(),
8397        }];
8398        app.current_tab = 0;
8399        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8400        app.cfn_state.table.items = vec![CfnStack {
8401            name: "test-stack".to_string(),
8402            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8403                .to_string(),
8404            status: "CREATE_COMPLETE".to_string(),
8405            created_time: "2024-01-01".to_string(),
8406            updated_time: String::new(),
8407            deleted_time: String::new(),
8408            drift_status: String::new(),
8409            last_drift_check_time: String::new(),
8410            status_reason: String::new(),
8411            description: "Test stack".to_string(),
8412            detailed_status: String::new(),
8413            root_stack: String::new(),
8414            parent_stack: String::new(),
8415            termination_protection: false,
8416            iam_role: String::new(),
8417            tags: Vec::new(),
8418            stack_policy: String::new(),
8419            rollback_monitoring_time: String::new(),
8420            rollback_alarms: Vec::new(),
8421            notification_arns: Vec::new(),
8422        }];
8423        app.cfn_state.table.reset();
8424
8425        // Verify filtering works
8426        let filtered = app.filtered_cloudformation_stacks();
8427        assert_eq!(filtered.len(), 1);
8428        assert_eq!(filtered[0].name, "test-stack");
8429
8430        assert_eq!(app.cfn_state.current_stack, None);
8431
8432        // Enter drills into stack detail view
8433        app.handle_action(Action::Select);
8434        assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
8435    }
8436
8437    #[test]
8438    fn test_cloudformation_copy_to_clipboard() {
8439        let mut app = test_app();
8440        app.current_service = Service::CloudFormationStacks;
8441        app.service_selected = true;
8442        app.mode = Mode::Normal;
8443        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8444        app.cfn_state.table.items = vec![
8445            CfnStack {
8446                name: "stack1".to_string(),
8447                stack_id: "id1".to_string(),
8448                status: "CREATE_COMPLETE".to_string(),
8449                created_time: "2024-01-01".to_string(),
8450                updated_time: String::new(),
8451                deleted_time: String::new(),
8452                drift_status: String::new(),
8453                last_drift_check_time: String::new(),
8454                status_reason: String::new(),
8455                description: String::new(),
8456                detailed_status: String::new(),
8457                root_stack: String::new(),
8458                parent_stack: String::new(),
8459                termination_protection: false,
8460                iam_role: String::new(),
8461                tags: Vec::new(),
8462                stack_policy: String::new(),
8463                rollback_monitoring_time: String::new(),
8464                rollback_alarms: Vec::new(),
8465                notification_arns: Vec::new(),
8466            },
8467            CfnStack {
8468                name: "stack2".to_string(),
8469                stack_id: "id2".to_string(),
8470                status: "UPDATE_COMPLETE".to_string(),
8471                created_time: "2024-01-02".to_string(),
8472                updated_time: String::new(),
8473                deleted_time: String::new(),
8474                drift_status: String::new(),
8475                last_drift_check_time: String::new(),
8476                status_reason: String::new(),
8477                description: String::new(),
8478                detailed_status: String::new(),
8479                root_stack: String::new(),
8480                parent_stack: String::new(),
8481                termination_protection: false,
8482                iam_role: String::new(),
8483                tags: Vec::new(),
8484                stack_policy: String::new(),
8485                rollback_monitoring_time: String::new(),
8486                rollback_alarms: Vec::new(),
8487                notification_arns: Vec::new(),
8488            },
8489        ];
8490
8491        assert!(!app.snapshot_requested);
8492        app.handle_action(Action::CopyToClipboard);
8493
8494        // Should set snapshot_requested flag
8495        assert!(app.snapshot_requested);
8496    }
8497
8498    #[test]
8499    fn test_cloudformation_expansion_shows_all_visible_columns() {
8500        let mut app = test_app();
8501        app.current_service = Service::CloudFormationStacks;
8502        app.cfn_state.status_filter = crate::ui::cfn::StatusFilter::Complete;
8503        app.cfn_state.table.items = vec![CfnStack {
8504            name: "test-stack".to_string(),
8505            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8506                .to_string(),
8507            status: "CREATE_COMPLETE".to_string(),
8508            created_time: "2024-01-01".to_string(),
8509            updated_time: "2024-01-02".to_string(),
8510            deleted_time: String::new(),
8511            drift_status: "IN_SYNC".to_string(),
8512            last_drift_check_time: "2024-01-03".to_string(),
8513            status_reason: String::new(),
8514            description: "Test description".to_string(),
8515            detailed_status: String::new(),
8516            root_stack: String::new(),
8517            parent_stack: String::new(),
8518            termination_protection: false,
8519            iam_role: String::new(),
8520            tags: Vec::new(),
8521            stack_policy: String::new(),
8522            rollback_monitoring_time: String::new(),
8523            rollback_alarms: Vec::new(),
8524            notification_arns: Vec::new(),
8525        }];
8526
8527        // Set visible columns
8528        app.visible_cfn_columns = vec![
8529            CfnColumn::Name,
8530            CfnColumn::Status,
8531            CfnColumn::CreatedTime,
8532            CfnColumn::Description,
8533        ];
8534
8535        app.cfn_state.table.expanded_item = Some(0);
8536
8537        // Verify all visible columns would be shown in expansion
8538        // (This is a structural test - actual rendering is in UI layer)
8539        assert_eq!(app.visible_cfn_columns.len(), 4);
8540        assert!(app.cfn_state.table.has_expanded_item());
8541    }
8542
8543    #[test]
8544    fn test_cloudformation_empty_list_shows_page_1() {
8545        let mut app = test_app();
8546        app.current_service = Service::CloudFormationStacks;
8547        app.cfn_state.table.items = vec![];
8548
8549        let filtered = app.filtered_cloudformation_stacks();
8550        assert_eq!(filtered.len(), 0);
8551
8552        // Pagination should still show [1] even with 0 items
8553        let page_size = app.cfn_state.table.page_size.value();
8554        let total_pages = filtered.len().div_ceil(page_size);
8555        assert_eq!(total_pages, 0);
8556
8557        // render_pagination_text(0, 0) should return "[1]"
8558        // This is tested in UI layer
8559    }
8560}
8561
8562impl App {
8563    pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
8564        let mut all = AwsRegion::all();
8565
8566        // Add latencies to regions
8567        for region in &mut all {
8568            region.latency_ms = self.region_latencies.get(region.code).copied();
8569        }
8570
8571        // Filter by search term
8572        let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
8573            all
8574        } else {
8575            let filter_lower = self.region_filter.to_lowercase();
8576            all.into_iter()
8577                .filter(|r| {
8578                    r.name.to_lowercase().contains(&filter_lower)
8579                        || r.code.to_lowercase().contains(&filter_lower)
8580                        || r.group.to_lowercase().contains(&filter_lower)
8581                })
8582                .collect()
8583        };
8584
8585        // Sort by latency (lowest first), treat None as 1000ms
8586        let mut sorted = filtered;
8587        sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
8588        sorted
8589    }
8590
8591    pub fn measure_region_latencies(&mut self) {
8592        use std::time::Instant;
8593        self.region_latencies.clear();
8594
8595        let regions = AwsRegion::all();
8596        let start_all = Instant::now();
8597        tracing::info!("Starting latency measurement for {} regions", regions.len());
8598
8599        let handles: Vec<_> = regions
8600            .iter()
8601            .map(|region| {
8602                let code = region.code.to_string();
8603                std::thread::spawn(move || {
8604                    // Use STS endpoint - fastest and most reliable
8605                    let endpoint = format!("https://sts.{}.amazonaws.com", code);
8606                    let start = Instant::now();
8607
8608                    match ureq::get(&endpoint)
8609                        .timeout(std::time::Duration::from_secs(2))
8610                        .call()
8611                    {
8612                        Ok(_) => {
8613                            let latency = start.elapsed().as_millis() as u64;
8614                            Some((code, latency))
8615                        }
8616                        Err(e) => {
8617                            tracing::debug!("Failed to measure {}: {}", code, e);
8618                            Some((code, 9999))
8619                        }
8620                    }
8621                })
8622            })
8623            .collect();
8624
8625        for handle in handles {
8626            if let Ok(Some((code, latency))) = handle.join() {
8627                self.region_latencies.insert(code, latency);
8628            }
8629        }
8630
8631        tracing::info!(
8632            "Measured {} regions in {:?}",
8633            self.region_latencies.len(),
8634            start_all.elapsed()
8635        );
8636    }
8637
8638    pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
8639        crate::aws::filter_profiles(&self.available_profiles, &self.profile_filter)
8640    }
8641
8642    pub fn get_filtered_sessions(&self) -> Vec<&crate::session::Session> {
8643        if self.session_filter.is_empty() {
8644            return self.sessions.iter().collect();
8645        }
8646        let filter_lower = self.session_filter.to_lowercase();
8647        self.sessions
8648            .iter()
8649            .filter(|s| {
8650                s.profile.to_lowercase().contains(&filter_lower)
8651                    || s.region.to_lowercase().contains(&filter_lower)
8652                    || s.account_id.to_lowercase().contains(&filter_lower)
8653                    || s.role_arn.to_lowercase().contains(&filter_lower)
8654            })
8655            .collect()
8656    }
8657
8658    pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
8659        if self.tab_filter.is_empty() {
8660            return self.tabs.iter().enumerate().collect();
8661        }
8662        let filter_lower = self.tab_filter.to_lowercase();
8663        self.tabs
8664            .iter()
8665            .enumerate()
8666            .filter(|(_, tab)| {
8667                tab.title.to_lowercase().contains(&filter_lower)
8668                    || tab.breadcrumb.to_lowercase().contains(&filter_lower)
8669            })
8670            .collect()
8671    }
8672
8673    pub fn load_aws_profiles() -> Vec<AwsProfile> {
8674        AwsProfile::load_all()
8675    }
8676
8677    pub async fn fetch_profile_accounts(&mut self) {
8678        for profile in &mut self.available_profiles {
8679            if profile.account.is_none() {
8680                let region = profile
8681                    .region
8682                    .clone()
8683                    .unwrap_or_else(|| "us-east-1".to_string());
8684                if let Ok(account) =
8685                    rusticity_core::AwsConfig::get_account_for_profile(&profile.name, &region).await
8686                {
8687                    profile.account = Some(account);
8688                }
8689            }
8690        }
8691    }
8692
8693    fn save_current_session(&mut self) {
8694        // If no tabs, delete the session if it exists
8695        if self.tabs.is_empty() {
8696            if let Some(ref session) = self.current_session {
8697                let _ = session.delete();
8698                self.current_session = None;
8699            }
8700            return;
8701        }
8702
8703        let session = if let Some(ref mut current) = self.current_session {
8704            // Update existing session
8705            current.tabs = self
8706                .tabs
8707                .iter()
8708                .map(|t| crate::session::SessionTab {
8709                    service: format!("{:?}", t.service),
8710                    title: t.title.clone(),
8711                    breadcrumb: t.breadcrumb.clone(),
8712                    filter: match t.service {
8713                        Service::CloudWatchLogGroups => {
8714                            Some(self.log_groups_state.log_groups.filter.clone())
8715                        }
8716                        _ => None,
8717                    },
8718                    selected_item: None,
8719                })
8720                .collect();
8721            current.clone()
8722        } else {
8723            // Create new session
8724            let mut session = crate::session::Session::new(
8725                self.profile.clone(),
8726                self.region.clone(),
8727                self.config.account_id.clone(),
8728                self.config.role_arn.clone(),
8729            );
8730            session.tabs = self
8731                .tabs
8732                .iter()
8733                .map(|t| crate::session::SessionTab {
8734                    service: format!("{:?}", t.service),
8735                    title: t.title.clone(),
8736                    breadcrumb: t.breadcrumb.clone(),
8737                    filter: match t.service {
8738                        Service::CloudWatchLogGroups => {
8739                            Some(self.log_groups_state.log_groups.filter.clone())
8740                        }
8741                        _ => None,
8742                    },
8743                    selected_item: None,
8744                })
8745                .collect();
8746            self.current_session = Some(session.clone());
8747            session
8748        };
8749
8750        let _ = session.save();
8751    }
8752}
8753
8754#[cfg(test)]
8755mod iam_policy_view_tests {
8756    use super::*;
8757    use test_helpers::*;
8758
8759    #[test]
8760    fn test_enter_opens_policy_view() {
8761        let mut app = test_app();
8762        app.current_service = Service::IamRoles;
8763        app.service_selected = true;
8764        app.mode = Mode::Normal;
8765        app.view_mode = ViewMode::Detail;
8766        app.iam_state.current_role = Some("TestRole".to_string());
8767        app.iam_state.policies.items = vec![crate::iam::Policy {
8768            policy_name: "TestPolicy".to_string(),
8769            policy_type: "Inline".to_string(),
8770            attached_via: "Direct".to_string(),
8771            attached_entities: "1".to_string(),
8772            description: "Test".to_string(),
8773            creation_time: "2023-01-01".to_string(),
8774            edited_time: "2023-01-01".to_string(),
8775            policy_arn: None,
8776        }];
8777        app.iam_state.policies.reset();
8778
8779        app.handle_action(Action::Select);
8780
8781        assert_eq!(app.view_mode, ViewMode::PolicyView);
8782        assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
8783        assert_eq!(app.iam_state.policy_scroll, 0);
8784        assert!(app.iam_state.policies.loading);
8785    }
8786
8787    #[test]
8788    fn test_escape_closes_policy_view() {
8789        let mut app = test_app();
8790        app.current_service = Service::IamRoles;
8791        app.service_selected = true;
8792        app.mode = Mode::Normal;
8793        app.view_mode = ViewMode::PolicyView;
8794        app.iam_state.current_role = Some("TestRole".to_string());
8795        app.iam_state.current_policy = Some("TestPolicy".to_string());
8796        app.iam_state.policy_document = "{\n  \"test\": \"value\"\n}".to_string();
8797        app.iam_state.policy_scroll = 5;
8798
8799        app.handle_action(Action::PrevPane);
8800
8801        assert_eq!(app.view_mode, ViewMode::Detail);
8802        assert_eq!(app.iam_state.current_policy, None);
8803        assert_eq!(app.iam_state.policy_document, "");
8804        assert_eq!(app.iam_state.policy_scroll, 0);
8805    }
8806
8807    #[test]
8808    fn test_ctrl_d_scrolls_down_in_policy_view() {
8809        let mut app = test_app();
8810        app.current_service = Service::IamRoles;
8811        app.service_selected = true;
8812        app.mode = Mode::Normal;
8813        app.view_mode = ViewMode::PolicyView;
8814        app.iam_state.current_role = Some("TestRole".to_string());
8815        app.iam_state.current_policy = Some("TestPolicy".to_string());
8816        app.iam_state.policy_document = (0..100)
8817            .map(|i| format!("line {}", i))
8818            .collect::<Vec<_>>()
8819            .join("\n");
8820        app.iam_state.policy_scroll = 0;
8821
8822        app.handle_action(Action::ScrollDown);
8823
8824        assert_eq!(app.iam_state.policy_scroll, 10);
8825
8826        app.handle_action(Action::ScrollDown);
8827
8828        assert_eq!(app.iam_state.policy_scroll, 20);
8829    }
8830
8831    #[test]
8832    fn test_ctrl_u_scrolls_up_in_policy_view() {
8833        let mut app = test_app();
8834        app.current_service = Service::IamRoles;
8835        app.service_selected = true;
8836        app.mode = Mode::Normal;
8837        app.view_mode = ViewMode::PolicyView;
8838        app.iam_state.current_role = Some("TestRole".to_string());
8839        app.iam_state.current_policy = Some("TestPolicy".to_string());
8840        app.iam_state.policy_document = (0..100)
8841            .map(|i| format!("line {}", i))
8842            .collect::<Vec<_>>()
8843            .join("\n");
8844        app.iam_state.policy_scroll = 30;
8845
8846        app.handle_action(Action::ScrollUp);
8847
8848        assert_eq!(app.iam_state.policy_scroll, 20);
8849
8850        app.handle_action(Action::ScrollUp);
8851
8852        assert_eq!(app.iam_state.policy_scroll, 10);
8853    }
8854
8855    #[test]
8856    fn test_scroll_does_not_go_negative() {
8857        let mut app = test_app();
8858        app.current_service = Service::IamRoles;
8859        app.service_selected = true;
8860        app.mode = Mode::Normal;
8861        app.view_mode = ViewMode::PolicyView;
8862        app.iam_state.current_role = Some("TestRole".to_string());
8863        app.iam_state.current_policy = Some("TestPolicy".to_string());
8864        app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
8865        app.iam_state.policy_scroll = 0;
8866
8867        app.handle_action(Action::ScrollUp);
8868
8869        assert_eq!(app.iam_state.policy_scroll, 0);
8870    }
8871
8872    #[test]
8873    fn test_scroll_does_not_exceed_max() {
8874        let mut app = test_app();
8875        app.current_service = Service::IamRoles;
8876        app.service_selected = true;
8877        app.mode = Mode::Normal;
8878        app.view_mode = ViewMode::PolicyView;
8879        app.iam_state.current_role = Some("TestRole".to_string());
8880        app.iam_state.current_policy = Some("TestPolicy".to_string());
8881        app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
8882        app.iam_state.policy_scroll = 0;
8883
8884        app.handle_action(Action::ScrollDown);
8885
8886        assert_eq!(app.iam_state.policy_scroll, 2); // Max is 2 (3 lines - 1)
8887    }
8888
8889    #[test]
8890    fn test_policy_view_console_url() {
8891        let mut app = test_app();
8892        app.current_service = Service::IamRoles;
8893        app.service_selected = true;
8894        app.view_mode = ViewMode::PolicyView;
8895        app.iam_state.current_role = Some("TestRole".to_string());
8896        app.iam_state.current_policy = Some("TestPolicy".to_string());
8897
8898        let url = app.get_console_url();
8899
8900        assert!(url.contains("us-east-1.console.aws.amazon.com"));
8901        assert!(url.contains("/roles/details/TestRole"));
8902        assert!(url.contains("/editPolicy/TestPolicy"));
8903        assert!(url.contains("step=addPermissions"));
8904    }
8905
8906    #[test]
8907    fn test_esc_from_policy_view_goes_to_role_detail() {
8908        let mut app = test_app();
8909        app.current_service = Service::IamRoles;
8910        app.service_selected = true;
8911        app.mode = Mode::Normal;
8912        app.view_mode = ViewMode::PolicyView;
8913        app.iam_state.current_role = Some("TestRole".to_string());
8914        app.iam_state.current_policy = Some("TestPolicy".to_string());
8915        app.iam_state.policy_document = "test".to_string();
8916        app.iam_state.policy_scroll = 5;
8917
8918        app.handle_action(Action::GoBack);
8919
8920        assert_eq!(app.view_mode, ViewMode::Detail);
8921        assert_eq!(app.iam_state.current_policy, None);
8922        assert_eq!(app.iam_state.policy_document, "");
8923        assert_eq!(app.iam_state.policy_scroll, 0);
8924        assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
8925    }
8926
8927    #[test]
8928    fn test_esc_from_role_detail_goes_to_role_list() {
8929        let mut app = test_app();
8930        app.current_service = Service::IamRoles;
8931        app.service_selected = true;
8932        app.mode = Mode::Normal;
8933        app.view_mode = ViewMode::Detail;
8934        app.iam_state.current_role = Some("TestRole".to_string());
8935
8936        app.handle_action(Action::GoBack);
8937
8938        assert_eq!(app.iam_state.current_role, None);
8939    }
8940
8941    #[test]
8942    fn test_right_arrow_expands_policy_row() {
8943        let mut app = test_app();
8944        app.current_service = Service::IamRoles;
8945        app.service_selected = true;
8946        app.mode = Mode::Normal;
8947        app.view_mode = ViewMode::Detail;
8948        app.iam_state.current_role = Some("TestRole".to_string());
8949        app.iam_state.policies.items = vec![crate::iam::Policy {
8950            policy_name: "TestPolicy".to_string(),
8951            policy_type: "Inline".to_string(),
8952            attached_via: "Direct".to_string(),
8953            attached_entities: "1".to_string(),
8954            description: "Test".to_string(),
8955            creation_time: "2023-01-01".to_string(),
8956            edited_time: "2023-01-01".to_string(),
8957            policy_arn: None,
8958        }];
8959        app.iam_state.policies.reset();
8960
8961        app.handle_action(Action::NextPane);
8962
8963        // Should expand, not drill down
8964        assert_eq!(app.view_mode, ViewMode::Detail);
8965        assert_eq!(app.iam_state.current_policy, None);
8966        assert_eq!(app.iam_state.policies.expanded_item, Some(0));
8967    }
8968}
8969
8970#[cfg(test)]
8971mod tab_filter_tests {
8972    use super::*;
8973    use test_helpers::*;
8974
8975    #[test]
8976    fn test_space_t_opens_tab_picker() {
8977        let mut app = test_app();
8978        app.tabs = vec![
8979            Tab {
8980                service: Service::CloudWatchLogGroups,
8981                title: "Tab 1".to_string(),
8982                breadcrumb: "CloudWatch > Log groups".to_string(),
8983            },
8984            Tab {
8985                service: Service::S3Buckets,
8986                title: "Tab 2".to_string(),
8987                breadcrumb: "S3 > Buckets".to_string(),
8988            },
8989        ];
8990        app.current_tab = 0;
8991
8992        app.handle_action(Action::OpenTabPicker);
8993
8994        assert_eq!(app.mode, Mode::TabPicker);
8995        assert_eq!(app.tab_picker_selected, 0);
8996    }
8997
8998    #[test]
8999    fn test_tab_filter_works() {
9000        let mut app = test_app();
9001        app.tabs = vec![
9002            Tab {
9003                service: Service::CloudWatchLogGroups,
9004                title: "CloudWatch Logs".to_string(),
9005                breadcrumb: "CloudWatch > Log groups".to_string(),
9006            },
9007            Tab {
9008                service: Service::S3Buckets,
9009                title: "S3 Buckets".to_string(),
9010                breadcrumb: "S3 > Buckets".to_string(),
9011            },
9012            Tab {
9013                service: Service::CloudWatchAlarms,
9014                title: "CloudWatch Alarms".to_string(),
9015                breadcrumb: "CloudWatch > Alarms".to_string(),
9016            },
9017        ];
9018        app.mode = Mode::TabPicker;
9019
9020        // Filter for "s3"
9021        app.handle_action(Action::FilterInput('s'));
9022        app.handle_action(Action::FilterInput('3'));
9023
9024        let filtered = app.get_filtered_tabs();
9025        assert_eq!(filtered.len(), 1);
9026        assert_eq!(filtered[0].1.title, "S3 Buckets");
9027    }
9028
9029    #[test]
9030    fn test_tab_filter_by_breadcrumb() {
9031        let mut app = test_app();
9032        app.tabs = vec![
9033            Tab {
9034                service: Service::CloudWatchLogGroups,
9035                title: "Tab 1".to_string(),
9036                breadcrumb: "CloudWatch > Log groups".to_string(),
9037            },
9038            Tab {
9039                service: Service::S3Buckets,
9040                title: "Tab 2".to_string(),
9041                breadcrumb: "S3 > Buckets".to_string(),
9042            },
9043        ];
9044        app.mode = Mode::TabPicker;
9045
9046        // Filter for "cloudwatch"
9047        app.handle_action(Action::FilterInput('c'));
9048        app.handle_action(Action::FilterInput('l'));
9049        app.handle_action(Action::FilterInput('o'));
9050        app.handle_action(Action::FilterInput('u'));
9051        app.handle_action(Action::FilterInput('d'));
9052
9053        let filtered = app.get_filtered_tabs();
9054        assert_eq!(filtered.len(), 1);
9055        assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
9056    }
9057
9058    #[test]
9059    fn test_tab_filter_backspace() {
9060        let mut app = test_app();
9061        app.tabs = vec![
9062            Tab {
9063                service: Service::CloudWatchLogGroups,
9064                title: "CloudWatch Logs".to_string(),
9065                breadcrumb: "CloudWatch > Log groups".to_string(),
9066            },
9067            Tab {
9068                service: Service::S3Buckets,
9069                title: "S3 Buckets".to_string(),
9070                breadcrumb: "S3 > Buckets".to_string(),
9071            },
9072        ];
9073        app.mode = Mode::TabPicker;
9074
9075        app.handle_action(Action::FilterInput('s'));
9076        app.handle_action(Action::FilterInput('3'));
9077        assert_eq!(app.tab_filter, "s3");
9078
9079        app.handle_action(Action::FilterBackspace);
9080        assert_eq!(app.tab_filter, "s");
9081
9082        let filtered = app.get_filtered_tabs();
9083        assert_eq!(filtered.len(), 2); // Both match "s"
9084    }
9085
9086    #[test]
9087    fn test_tab_selection_with_filter() {
9088        let mut app = test_app();
9089        app.tabs = vec![
9090            Tab {
9091                service: Service::CloudWatchLogGroups,
9092                title: "CloudWatch Logs".to_string(),
9093                breadcrumb: "CloudWatch > Log groups".to_string(),
9094            },
9095            Tab {
9096                service: Service::S3Buckets,
9097                title: "S3 Buckets".to_string(),
9098                breadcrumb: "S3 > Buckets".to_string(),
9099            },
9100        ];
9101        app.mode = Mode::TabPicker;
9102        app.current_tab = 0;
9103
9104        // Filter for "s3"
9105        app.handle_action(Action::FilterInput('s'));
9106        app.handle_action(Action::FilterInput('3'));
9107
9108        // Select the filtered tab
9109        app.handle_action(Action::Select);
9110
9111        assert_eq!(app.current_tab, 1); // Should select the S3 tab (index 1)
9112        assert_eq!(app.mode, Mode::Normal);
9113        assert_eq!(app.tab_filter, ""); // Filter should be cleared
9114    }
9115}
9116
9117#[cfg(test)]
9118mod region_latency_tests {
9119    use super::*;
9120    use test_helpers::*;
9121
9122    #[test]
9123    fn test_regions_sorted_by_latency() {
9124        let mut app = test_app();
9125
9126        // Add some latencies
9127        app.region_latencies.insert("us-west-2".to_string(), 50);
9128        app.region_latencies.insert("us-east-1".to_string(), 10);
9129        app.region_latencies.insert("eu-west-1".to_string(), 100);
9130
9131        let filtered = app.get_filtered_regions();
9132
9133        // Should be sorted by latency (lowest first)
9134        let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
9135
9136        assert!(with_latency.len() >= 3);
9137        assert_eq!(with_latency[0].code, "us-east-1");
9138        assert_eq!(with_latency[0].latency_ms, Some(10));
9139        assert_eq!(with_latency[1].code, "us-west-2");
9140        assert_eq!(with_latency[1].latency_ms, Some(50));
9141        assert_eq!(with_latency[2].code, "eu-west-1");
9142        assert_eq!(with_latency[2].latency_ms, Some(100));
9143    }
9144
9145    #[test]
9146    fn test_regions_with_latency_before_without() {
9147        let mut app = test_app();
9148
9149        // Only add latency for one region
9150        app.region_latencies.insert("eu-west-1".to_string(), 100);
9151
9152        let filtered = app.get_filtered_regions();
9153
9154        // Region with latency should come first
9155        assert_eq!(filtered[0].code, "eu-west-1");
9156        assert_eq!(filtered[0].latency_ms, Some(100));
9157
9158        // Rest should be sorted by name
9159        for region in &filtered[1..] {
9160            assert!(region.latency_ms.is_none());
9161        }
9162    }
9163
9164    #[test]
9165    fn test_region_filter_with_latency() {
9166        let mut app = test_app();
9167
9168        app.region_latencies.insert("us-east-1".to_string(), 10);
9169        app.region_latencies.insert("us-west-2".to_string(), 50);
9170        app.region_filter = "us".to_string();
9171
9172        let filtered = app.get_filtered_regions();
9173
9174        // Should only have US regions, sorted by latency
9175        assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
9176        assert_eq!(filtered[0].code, "us-east-1");
9177        assert_eq!(filtered[1].code, "us-west-2");
9178    }
9179
9180    #[test]
9181    fn test_latency_persists_across_filters() {
9182        let mut app = test_app();
9183
9184        app.region_latencies.insert("us-east-1".to_string(), 10);
9185
9186        // Filter to something else
9187        app.region_filter = "eu".to_string();
9188        let filtered = app.get_filtered_regions();
9189        assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
9190
9191        // Clear filter
9192        app.region_filter.clear();
9193        let all = app.get_filtered_regions();
9194
9195        // Latency should still be there
9196        let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
9197        assert_eq!(us_east.latency_ms, Some(10));
9198    }
9199
9200    #[test]
9201    fn test_measure_region_latencies_clears_previous() {
9202        let mut app = test_app();
9203
9204        // Add some fake latencies
9205        app.region_latencies.insert("us-east-1".to_string(), 100);
9206        app.region_latencies.insert("eu-west-1".to_string(), 200);
9207
9208        // Measure again (will fail to connect but should clear)
9209        app.measure_region_latencies();
9210
9211        // Old latencies should be cleared
9212        assert!(
9213            app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
9214        );
9215    }
9216
9217    #[test]
9218    fn test_regions_with_latency_sorted_first() {
9219        let mut app = test_app();
9220
9221        // Add latencies: one fast, one slow (>1000ms would be treated as >1s)
9222        app.region_latencies.insert("us-east-1".to_string(), 50);
9223        app.region_latencies.insert("eu-west-1".to_string(), 500);
9224
9225        let filtered = app.get_filtered_regions();
9226
9227        // Should show all regions
9228        assert!(filtered.len() > 2);
9229
9230        // Fast regions first
9231        assert_eq!(filtered[0].code, "us-east-1");
9232        assert_eq!(filtered[0].latency_ms, Some(50));
9233        assert_eq!(filtered[1].code, "eu-west-1");
9234        assert_eq!(filtered[1].latency_ms, Some(500));
9235
9236        // Regions without latency treated as 1000ms, so they come after 500ms
9237        for region in &filtered[2..] {
9238            assert!(region.latency_ms.is_none());
9239        }
9240    }
9241
9242    #[test]
9243    fn test_regions_without_latency_sorted_as_1000ms() {
9244        let mut app = test_app();
9245
9246        // Add one region with 1500ms (slower than default 1000ms)
9247        app.region_latencies
9248            .insert("ap-southeast-2".to_string(), 1500);
9249        // Add one region with 50ms (faster)
9250        app.region_latencies.insert("us-east-1".to_string(), 50);
9251
9252        let filtered = app.get_filtered_regions();
9253
9254        // Fast region first
9255        assert_eq!(filtered[0].code, "us-east-1");
9256        assert_eq!(filtered[0].latency_ms, Some(50));
9257
9258        // Regions without latency (treated as 1000ms) come before 1500ms
9259        let slow_region_idx = filtered
9260            .iter()
9261            .position(|r| r.code == "ap-southeast-2")
9262            .unwrap();
9263        assert!(slow_region_idx > 1); // Should be after fast region and regions without latency
9264
9265        // All regions between index 1 and slow_region_idx should have no latency
9266        for region in filtered.iter().take(slow_region_idx).skip(1) {
9267            assert!(region.latency_ms.is_none());
9268        }
9269    }
9270
9271    #[test]
9272    fn test_region_picker_opens_with_latencies() {
9273        let mut app = test_app();
9274
9275        // Simulate opening region picker
9276        app.region_filter.clear();
9277        app.region_picker_selected = 0;
9278        app.measure_region_latencies();
9279
9280        // Should have attempted to measure (even if all fail in test env)
9281        // The map should be initialized
9282        assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
9283    }
9284
9285    #[test]
9286    fn test_ecr_tab_next() {
9287        assert_eq!(EcrTab::Private.next(), EcrTab::Public);
9288        assert_eq!(EcrTab::Public.next(), EcrTab::Private);
9289    }
9290
9291    #[test]
9292    fn test_ecr_tab_switching() {
9293        let mut app = test_app();
9294        app.current_service = Service::EcrRepositories;
9295        app.service_selected = true;
9296        app.ecr_state.tab = EcrTab::Private;
9297
9298        app.handle_action(Action::NextDetailTab);
9299        assert_eq!(app.ecr_state.tab, EcrTab::Public);
9300        assert_eq!(app.ecr_state.repositories.selected, 0);
9301
9302        app.handle_action(Action::NextDetailTab);
9303        assert_eq!(app.ecr_state.tab, EcrTab::Private);
9304    }
9305
9306    #[test]
9307    fn test_ecr_navigation() {
9308        let mut app = test_app();
9309        app.current_service = Service::EcrRepositories;
9310        app.service_selected = true;
9311        app.mode = Mode::Normal;
9312        app.ecr_state.repositories.items = vec![
9313            EcrRepository {
9314                name: "repo1".to_string(),
9315                uri: "uri1".to_string(),
9316                created_at: "2023-01-01".to_string(),
9317                tag_immutability: "MUTABLE".to_string(),
9318                encryption_type: "AES256".to_string(),
9319            },
9320            EcrRepository {
9321                name: "repo2".to_string(),
9322                uri: "uri2".to_string(),
9323                created_at: "2023-01-02".to_string(),
9324                tag_immutability: "IMMUTABLE".to_string(),
9325                encryption_type: "KMS".to_string(),
9326            },
9327        ];
9328
9329        app.handle_action(Action::NextItem);
9330        assert_eq!(app.ecr_state.repositories.selected, 1);
9331
9332        app.handle_action(Action::PrevItem);
9333        assert_eq!(app.ecr_state.repositories.selected, 0);
9334    }
9335
9336    #[test]
9337    fn test_ecr_filter() {
9338        let mut app = test_app();
9339        app.current_service = Service::EcrRepositories;
9340        app.service_selected = true;
9341        app.ecr_state.repositories.items = vec![
9342            EcrRepository {
9343                name: "my-app".to_string(),
9344                uri: "uri1".to_string(),
9345                created_at: "2023-01-01".to_string(),
9346                tag_immutability: "MUTABLE".to_string(),
9347                encryption_type: "AES256".to_string(),
9348            },
9349            EcrRepository {
9350                name: "other-service".to_string(),
9351                uri: "uri2".to_string(),
9352                created_at: "2023-01-02".to_string(),
9353                tag_immutability: "IMMUTABLE".to_string(),
9354                encryption_type: "KMS".to_string(),
9355            },
9356        ];
9357
9358        app.ecr_state.repositories.filter = "app".to_string();
9359        let filtered = app.filtered_ecr_repositories();
9360        assert_eq!(filtered.len(), 1);
9361        assert_eq!(filtered[0].name, "my-app");
9362    }
9363
9364    #[test]
9365    fn test_ecr_filter_input() {
9366        let mut app = test_app();
9367        app.current_service = Service::EcrRepositories;
9368        app.service_selected = true;
9369        app.mode = Mode::FilterInput;
9370
9371        app.handle_action(Action::FilterInput('t'));
9372        app.handle_action(Action::FilterInput('e'));
9373        app.handle_action(Action::FilterInput('s'));
9374        app.handle_action(Action::FilterInput('t'));
9375        assert_eq!(app.ecr_state.repositories.filter, "test");
9376
9377        app.handle_action(Action::FilterBackspace);
9378        assert_eq!(app.ecr_state.repositories.filter, "tes");
9379    }
9380
9381    #[test]
9382    fn test_iam_users_filter_input() {
9383        let mut app = test_app();
9384        app.current_service = Service::IamUsers;
9385        app.service_selected = true;
9386        app.mode = Mode::FilterInput;
9387
9388        app.handle_action(Action::FilterInput('a'));
9389        app.handle_action(Action::FilterInput('d'));
9390        app.handle_action(Action::FilterInput('m'));
9391        app.handle_action(Action::FilterInput('i'));
9392        app.handle_action(Action::FilterInput('n'));
9393        assert_eq!(app.iam_state.users.filter, "admin");
9394
9395        app.handle_action(Action::FilterBackspace);
9396        assert_eq!(app.iam_state.users.filter, "admi");
9397    }
9398
9399    #[test]
9400    fn test_iam_policies_filter_input() {
9401        let mut app = test_app();
9402        app.current_service = Service::IamUsers;
9403        app.service_selected = true;
9404        app.iam_state.current_user = Some("testuser".to_string());
9405        app.mode = Mode::FilterInput;
9406
9407        app.handle_action(Action::FilterInput('r'));
9408        app.handle_action(Action::FilterInput('e'));
9409        app.handle_action(Action::FilterInput('a'));
9410        app.handle_action(Action::FilterInput('d'));
9411        assert_eq!(app.iam_state.policies.filter, "read");
9412
9413        app.handle_action(Action::FilterBackspace);
9414        assert_eq!(app.iam_state.policies.filter, "rea");
9415    }
9416
9417    #[test]
9418    fn test_iam_start_filter() {
9419        let mut app = test_app();
9420        app.current_service = Service::IamUsers;
9421        app.service_selected = true;
9422        app.mode = Mode::Normal;
9423
9424        app.handle_action(Action::StartFilter);
9425        assert_eq!(app.mode, Mode::FilterInput);
9426    }
9427
9428    #[test]
9429    fn test_iam_roles_filter_input() {
9430        let mut app = test_app();
9431        app.current_service = Service::IamRoles;
9432        app.service_selected = true;
9433        app.mode = Mode::FilterInput;
9434
9435        app.handle_action(Action::FilterInput('a'));
9436        app.handle_action(Action::FilterInput('d'));
9437        app.handle_action(Action::FilterInput('m'));
9438        app.handle_action(Action::FilterInput('i'));
9439        app.handle_action(Action::FilterInput('n'));
9440        assert_eq!(app.iam_state.roles.filter, "admin");
9441
9442        app.handle_action(Action::FilterBackspace);
9443        assert_eq!(app.iam_state.roles.filter, "admi");
9444    }
9445
9446    #[test]
9447    fn test_iam_roles_start_filter() {
9448        let mut app = test_app();
9449        app.current_service = Service::IamRoles;
9450        app.service_selected = true;
9451        app.mode = Mode::Normal;
9452
9453        app.handle_action(Action::StartFilter);
9454        assert_eq!(app.mode, Mode::FilterInput);
9455    }
9456
9457    #[test]
9458    fn test_iam_roles_navigation() {
9459        let mut app = test_app();
9460        app.current_service = Service::IamRoles;
9461        app.service_selected = true;
9462        app.mode = Mode::Normal;
9463        app.iam_state.roles.items = (0..10)
9464            .map(|i| crate::iam::IamRole {
9465                role_name: format!("role{}", i),
9466                path: "/".to_string(),
9467                trusted_entities: String::new(),
9468                last_activity: String::new(),
9469                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
9470                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9471                description: String::new(),
9472                max_session_duration: "3600 seconds".to_string(),
9473            })
9474            .collect();
9475
9476        assert_eq!(app.iam_state.roles.selected, 0);
9477
9478        app.handle_action(Action::NextItem);
9479        assert_eq!(app.iam_state.roles.selected, 1);
9480
9481        app.handle_action(Action::NextItem);
9482        assert_eq!(app.iam_state.roles.selected, 2);
9483
9484        app.handle_action(Action::PrevItem);
9485        assert_eq!(app.iam_state.roles.selected, 1);
9486    }
9487
9488    #[test]
9489    fn test_iam_roles_page_hotkey() {
9490        let mut app = test_app();
9491        app.current_service = Service::IamRoles;
9492        app.service_selected = true;
9493        app.mode = Mode::Normal;
9494        app.iam_state.roles.page_size = PageSize::Ten;
9495        app.iam_state.roles.items = (0..100)
9496            .map(|i| crate::iam::IamRole {
9497                role_name: format!("role{}", i),
9498                path: "/".to_string(),
9499                trusted_entities: String::new(),
9500                last_activity: String::new(),
9501                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
9502                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9503                description: String::new(),
9504                max_session_duration: "3600 seconds".to_string(),
9505            })
9506            .collect();
9507
9508        app.handle_action(Action::FilterInput('2'));
9509        app.handle_action(Action::OpenColumnSelector);
9510        assert_eq!(app.iam_state.roles.selected, 10); // Page 2 = index 10 (with page size 10)
9511    }
9512
9513    #[test]
9514    fn test_iam_users_page_hotkey() {
9515        let mut app = test_app();
9516        app.current_service = Service::IamUsers;
9517        app.service_selected = true;
9518        app.mode = Mode::Normal;
9519        app.iam_state.users.page_size = PageSize::Ten;
9520        app.iam_state.users.items = (0..100)
9521            .map(|i| crate::iam::IamUser {
9522                user_name: format!("user{}", i),
9523                path: "/".to_string(),
9524                groups: String::new(),
9525                last_activity: String::new(),
9526                mfa: String::new(),
9527                password_age: String::new(),
9528                console_last_sign_in: String::new(),
9529                access_key_id: String::new(),
9530                active_key_age: String::new(),
9531                access_key_last_used: String::new(),
9532                arn: format!("arn:aws:iam::123456789012:user/user{}", i),
9533                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9534                console_access: String::new(),
9535                signing_certs: String::new(),
9536            })
9537            .collect();
9538
9539        app.handle_action(Action::FilterInput('3'));
9540        app.handle_action(Action::OpenColumnSelector);
9541        assert_eq!(app.iam_state.users.selected, 20); // Page 3 = index 20 (with page size 10)
9542    }
9543
9544    #[test]
9545    fn test_ecr_scroll_navigation() {
9546        let mut app = test_app();
9547        app.current_service = Service::EcrRepositories;
9548        app.service_selected = true;
9549        app.ecr_state.repositories.items = (0..20)
9550            .map(|i| EcrRepository {
9551                name: format!("repo{}", i),
9552                uri: format!("uri{}", i),
9553                created_at: "2023-01-01".to_string(),
9554                tag_immutability: "MUTABLE".to_string(),
9555                encryption_type: "AES256".to_string(),
9556            })
9557            .collect();
9558
9559        app.handle_action(Action::ScrollDown);
9560        assert_eq!(app.ecr_state.repositories.selected, 10);
9561
9562        app.handle_action(Action::ScrollUp);
9563        assert_eq!(app.ecr_state.repositories.selected, 0);
9564    }
9565
9566    #[test]
9567    fn test_ecr_tab_switching_triggers_reload() {
9568        let mut app = test_app();
9569        app.current_service = Service::EcrRepositories;
9570        app.service_selected = true;
9571        app.ecr_state.tab = EcrTab::Private;
9572        app.ecr_state.repositories.loading = false;
9573        app.ecr_state.repositories.items = vec![EcrRepository {
9574            name: "private-repo".to_string(),
9575            uri: "uri".to_string(),
9576            created_at: "2023-01-01".to_string(),
9577            tag_immutability: "MUTABLE".to_string(),
9578            encryption_type: "AES256".to_string(),
9579        }];
9580
9581        app.handle_action(Action::NextDetailTab);
9582        assert_eq!(app.ecr_state.tab, EcrTab::Public);
9583        assert!(app.ecr_state.repositories.loading);
9584        assert_eq!(app.ecr_state.repositories.selected, 0);
9585    }
9586
9587    #[test]
9588    fn test_ecr_tab_cycles_between_private_and_public() {
9589        let mut app = test_app();
9590        app.current_service = Service::EcrRepositories;
9591        app.service_selected = true;
9592        app.ecr_state.tab = EcrTab::Private;
9593
9594        app.handle_action(Action::NextDetailTab);
9595        assert_eq!(app.ecr_state.tab, EcrTab::Public);
9596
9597        app.handle_action(Action::NextDetailTab);
9598        assert_eq!(app.ecr_state.tab, EcrTab::Private);
9599    }
9600
9601    #[test]
9602    fn test_page_size_values() {
9603        assert_eq!(PageSize::Ten.value(), 10);
9604        assert_eq!(PageSize::TwentyFive.value(), 25);
9605        assert_eq!(PageSize::Fifty.value(), 50);
9606        assert_eq!(PageSize::OneHundred.value(), 100);
9607    }
9608
9609    #[test]
9610    fn test_page_size_next() {
9611        assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
9612        assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
9613        assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
9614        assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
9615    }
9616
9617    #[test]
9618    fn test_ecr_enter_drills_into_repository() {
9619        let mut app = test_app();
9620        app.current_service = Service::EcrRepositories;
9621        app.service_selected = true;
9622        app.mode = Mode::Normal;
9623        app.ecr_state.repositories.items = vec![EcrRepository {
9624            name: "my-repo".to_string(),
9625            uri: "uri".to_string(),
9626            created_at: "2023-01-01".to_string(),
9627            tag_immutability: "MUTABLE".to_string(),
9628            encryption_type: "AES256".to_string(),
9629        }];
9630
9631        app.handle_action(Action::Select);
9632        assert_eq!(
9633            app.ecr_state.current_repository,
9634            Some("my-repo".to_string())
9635        );
9636        assert!(app.ecr_state.repositories.loading);
9637    }
9638
9639    #[test]
9640    fn test_ecr_repository_expansion() {
9641        let mut app = test_app();
9642        app.current_service = Service::EcrRepositories;
9643        app.service_selected = true;
9644        app.ecr_state.repositories.items = vec![EcrRepository {
9645            name: "my-repo".to_string(),
9646            uri: "uri".to_string(),
9647            created_at: "2023-01-01".to_string(),
9648            tag_immutability: "MUTABLE".to_string(),
9649            encryption_type: "AES256".to_string(),
9650        }];
9651        app.ecr_state.repositories.selected = 0;
9652
9653        assert_eq!(app.ecr_state.repositories.expanded_item, None);
9654
9655        app.handle_action(Action::NextPane);
9656        assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
9657
9658        app.handle_action(Action::PrevPane);
9659        assert_eq!(app.ecr_state.repositories.expanded_item, None);
9660    }
9661
9662    #[test]
9663    fn test_ecr_ctrl_d_scrolls_down() {
9664        let mut app = test_app();
9665        app.current_service = Service::EcrRepositories;
9666        app.service_selected = true;
9667        app.mode = Mode::Normal;
9668        app.ecr_state.repositories.items = (0..30)
9669            .map(|i| EcrRepository {
9670                name: format!("repo{}", i),
9671                uri: format!("uri{}", i),
9672                created_at: "2023-01-01".to_string(),
9673                tag_immutability: "MUTABLE".to_string(),
9674                encryption_type: "AES256".to_string(),
9675            })
9676            .collect();
9677        app.ecr_state.repositories.selected = 0;
9678
9679        app.handle_action(Action::PageDown);
9680        assert_eq!(app.ecr_state.repositories.selected, 10);
9681    }
9682
9683    #[test]
9684    fn test_ecr_ctrl_u_scrolls_up() {
9685        let mut app = test_app();
9686        app.current_service = Service::EcrRepositories;
9687        app.service_selected = true;
9688        app.mode = Mode::Normal;
9689        app.ecr_state.repositories.items = (0..30)
9690            .map(|i| EcrRepository {
9691                name: format!("repo{}", i),
9692                uri: format!("uri{}", i),
9693                created_at: "2023-01-01".to_string(),
9694                tag_immutability: "MUTABLE".to_string(),
9695                encryption_type: "AES256".to_string(),
9696            })
9697            .collect();
9698        app.ecr_state.repositories.selected = 15;
9699
9700        app.handle_action(Action::PageUp);
9701        assert_eq!(app.ecr_state.repositories.selected, 5);
9702    }
9703
9704    #[test]
9705    fn test_ecr_images_ctrl_d_scrolls_down() {
9706        let mut app = test_app();
9707        app.current_service = Service::EcrRepositories;
9708        app.service_selected = true;
9709        app.mode = Mode::Normal;
9710        app.ecr_state.current_repository = Some("repo".to_string());
9711        app.ecr_state.images.items = (0..30)
9712            .map(|i| EcrImage {
9713                tag: format!("tag{}", i),
9714                artifact_type: "container".to_string(),
9715                pushed_at: "2023-01-01T12:00:00Z".to_string(),
9716                size_bytes: 104857600,
9717                uri: format!("uri{}", i),
9718                digest: format!("sha256:{}", i),
9719                last_pull_time: String::new(),
9720            })
9721            .collect();
9722        app.ecr_state.images.selected = 0;
9723
9724        app.handle_action(Action::PageDown);
9725        assert_eq!(app.ecr_state.images.selected, 10);
9726    }
9727
9728    #[test]
9729    fn test_ecr_esc_goes_back_from_images_to_repos() {
9730        let mut app = test_app();
9731        app.current_service = Service::EcrRepositories;
9732        app.service_selected = true;
9733        app.mode = Mode::Normal;
9734        app.ecr_state.current_repository = Some("my-repo".to_string());
9735        app.ecr_state.images.items = vec![EcrImage {
9736            tag: "latest".to_string(),
9737            artifact_type: "container".to_string(),
9738            pushed_at: "2023-01-01T12:00:00Z".to_string(),
9739            size_bytes: 104857600,
9740            uri: "uri".to_string(),
9741            digest: "sha256:abc".to_string(),
9742            last_pull_time: String::new(),
9743        }];
9744
9745        app.handle_action(Action::GoBack);
9746        assert_eq!(app.ecr_state.current_repository, None);
9747        assert!(app.ecr_state.images.items.is_empty());
9748    }
9749
9750    #[test]
9751    fn test_ecr_esc_collapses_expanded_image_first() {
9752        let mut app = test_app();
9753        app.current_service = Service::EcrRepositories;
9754        app.service_selected = true;
9755        app.mode = Mode::Normal;
9756        app.ecr_state.current_repository = Some("my-repo".to_string());
9757        app.ecr_state.images.expanded_item = Some(0);
9758
9759        app.handle_action(Action::GoBack);
9760        assert_eq!(app.ecr_state.images.expanded_item, None);
9761        assert_eq!(
9762            app.ecr_state.current_repository,
9763            Some("my-repo".to_string())
9764        );
9765    }
9766
9767    #[test]
9768    fn test_pagination_with_lowercase_p() {
9769        let mut app = test_app();
9770        app.current_service = Service::EcrRepositories;
9771        app.service_selected = true;
9772        app.mode = Mode::Normal;
9773        app.ecr_state.repositories.items = (0..100)
9774            .map(|i| EcrRepository {
9775                name: format!("repo{}", i),
9776                uri: format!("uri{}", i),
9777                created_at: "2023-01-01".to_string(),
9778                tag_immutability: "MUTABLE".to_string(),
9779                encryption_type: "AES256".to_string(),
9780            })
9781            .collect();
9782
9783        // Type "2" then "p" to go to page 2
9784        app.handle_action(Action::FilterInput('2'));
9785        assert_eq!(app.page_input, "2");
9786
9787        app.handle_action(Action::OpenColumnSelector); // 'p' key
9788        assert_eq!(app.ecr_state.repositories.selected, 50); // Page 2 starts at index 50
9789        assert_eq!(app.page_input, ""); // Should be cleared
9790    }
9791
9792    #[test]
9793    fn test_lowercase_p_without_number_opens_preferences() {
9794        let mut app = test_app();
9795        app.current_service = Service::EcrRepositories;
9796        app.service_selected = true;
9797        app.mode = Mode::Normal;
9798
9799        app.handle_action(Action::OpenColumnSelector); // 'p' key without number
9800        assert_eq!(app.mode, Mode::ColumnSelector);
9801    }
9802
9803    #[test]
9804    fn test_ctrl_o_generates_correct_console_url() {
9805        let mut app = test_app();
9806        app.current_service = Service::EcrRepositories;
9807        app.service_selected = true;
9808        app.mode = Mode::Normal;
9809        app.config.account_id = "123456789012".to_string();
9810
9811        // Test repository list URL
9812        let url = app.get_console_url();
9813        assert!(url.contains("ecr/private-registry/repositories"));
9814        assert!(url.contains("region=us-east-1"));
9815
9816        // Test images URL
9817        app.ecr_state.current_repository = Some("my-repo".to_string());
9818        let url = app.get_console_url();
9819        assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
9820        assert!(url.contains("region=us-east-1"));
9821    }
9822
9823    #[test]
9824    fn test_page_input_display_and_reset() {
9825        let mut app = test_app();
9826        app.current_service = Service::EcrRepositories;
9827        app.service_selected = true;
9828        app.mode = Mode::Normal;
9829        app.ecr_state.repositories.items = (0..100)
9830            .map(|i| EcrRepository {
9831                name: format!("repo{}", i),
9832                uri: format!("uri{}", i),
9833                created_at: "2023-01-01".to_string(),
9834                tag_immutability: "MUTABLE".to_string(),
9835                encryption_type: "AES256".to_string(),
9836            })
9837            .collect();
9838
9839        // Type "2"
9840        app.handle_action(Action::FilterInput('2'));
9841        assert_eq!(app.page_input, "2");
9842
9843        // Press 'p' to go to page 2
9844        app.handle_action(Action::OpenColumnSelector);
9845        assert_eq!(app.page_input, ""); // Should be cleared
9846        assert_eq!(app.ecr_state.repositories.selected, 50); // Page 2 starts at index 50
9847    }
9848
9849    #[test]
9850    fn test_page_navigation_updates_scroll_offset_for_cfn() {
9851        let mut app = test_app();
9852        app.current_service = Service::CloudFormationStacks;
9853        app.service_selected = true;
9854        app.mode = Mode::Normal;
9855        app.cfn_state.table.items = (0..100)
9856            .map(|i| crate::cfn::Stack {
9857                name: format!("stack-{}", i),
9858                stack_id: format!(
9859                    "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
9860                    i
9861                ),
9862                status: "CREATE_COMPLETE".to_string(),
9863                created_time: "2023-01-01T00:00:00Z".to_string(),
9864                updated_time: "2023-01-01T00:00:00Z".to_string(),
9865                deleted_time: String::new(),
9866                drift_status: "IN_SYNC".to_string(),
9867                last_drift_check_time: String::new(),
9868                status_reason: String::new(),
9869                description: String::new(),
9870                detailed_status: String::new(),
9871                root_stack: String::new(),
9872                parent_stack: String::new(),
9873                termination_protection: false,
9874                iam_role: String::new(),
9875                tags: vec![],
9876                stack_policy: String::new(),
9877                rollback_monitoring_time: String::new(),
9878                rollback_alarms: vec![],
9879                notification_arns: vec![],
9880            })
9881            .collect();
9882
9883        // Type "2" then "p" to go to page 2
9884        app.handle_action(Action::FilterInput('2'));
9885        assert_eq!(app.page_input, "2");
9886
9887        app.handle_action(Action::OpenColumnSelector); // 'p' key
9888        assert_eq!(app.page_input, ""); // Should be cleared
9889
9890        // Verify both selected and scroll_offset are updated
9891        let page_size = app.cfn_state.table.page_size.value();
9892        let expected_offset = page_size; // Page 2 starts at page_size
9893        assert_eq!(app.cfn_state.table.selected, expected_offset);
9894        assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
9895
9896        // Verify pagination display shows page 2
9897        let current_page = app.cfn_state.table.scroll_offset / page_size;
9898        assert_eq!(
9899            current_page, 1,
9900            "2p should go to page 2 (0-indexed as 1), not page 3"
9901        ); // 0-indexed, so page 2 is index 1
9902    }
9903
9904    #[test]
9905    fn test_3p_goes_to_page_3_not_page_5() {
9906        let mut app = test_app();
9907        app.current_service = Service::CloudFormationStacks;
9908        app.service_selected = true;
9909        app.mode = Mode::Normal;
9910        app.cfn_state.table.items = (0..200)
9911            .map(|i| crate::cfn::Stack {
9912                name: format!("stack-{}", i),
9913                stack_id: format!(
9914                    "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
9915                    i
9916                ),
9917                status: "CREATE_COMPLETE".to_string(),
9918                created_time: "2023-01-01T00:00:00Z".to_string(),
9919                updated_time: "2023-01-01T00:00:00Z".to_string(),
9920                deleted_time: String::new(),
9921                drift_status: "IN_SYNC".to_string(),
9922                last_drift_check_time: String::new(),
9923                status_reason: String::new(),
9924                description: String::new(),
9925                detailed_status: String::new(),
9926                root_stack: String::new(),
9927                parent_stack: String::new(),
9928                termination_protection: false,
9929                iam_role: String::new(),
9930                tags: vec![],
9931                stack_policy: String::new(),
9932                rollback_monitoring_time: String::new(),
9933                rollback_alarms: vec![],
9934                notification_arns: vec![],
9935            })
9936            .collect();
9937
9938        // Type "3" then "p" to go to page 3
9939        app.handle_action(Action::FilterInput('3'));
9940        app.handle_action(Action::OpenColumnSelector);
9941
9942        let page_size = app.cfn_state.table.page_size.value();
9943        let current_page = app.cfn_state.table.scroll_offset / page_size;
9944        assert_eq!(
9945            current_page, 2,
9946            "3p should go to page 3 (0-indexed as 2), not page 5"
9947        );
9948        assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
9949    }
9950
9951    #[test]
9952    fn test_log_streams_page_navigation_uses_correct_page_size() {
9953        let mut app = test_app();
9954        app.current_service = Service::CloudWatchLogGroups;
9955        app.view_mode = ViewMode::Detail;
9956        app.service_selected = true;
9957        app.mode = Mode::Normal;
9958        app.log_groups_state.log_streams = (0..100)
9959            .map(|i| LogStream {
9960                name: format!("stream-{}", i),
9961                creation_time: None,
9962                last_event_time: None,
9963            })
9964            .collect();
9965
9966        // Type "2" then "p" to go to page 2
9967        app.handle_action(Action::FilterInput('2'));
9968        app.handle_action(Action::OpenColumnSelector);
9969
9970        // Log streams use page_size=20, so page 2 starts at index 20
9971        assert_eq!(app.log_groups_state.selected_stream, 20);
9972
9973        // Verify pagination display shows page 2 (not page 3)
9974        let page_size = 20;
9975        let current_page = app.log_groups_state.selected_stream / page_size;
9976        assert_eq!(
9977            current_page, 1,
9978            "2p should go to page 2 (0-indexed as 1), not page 3"
9979        );
9980    }
9981
9982    #[test]
9983    fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
9984        let mut app = test_app();
9985        app.current_service = Service::EcrRepositories;
9986        app.service_selected = true;
9987        app.mode = Mode::Normal;
9988        app.ecr_state.repositories.page_size = PageSize::TwentyFive; // Set to 25
9989        app.ecr_state.repositories.items = (0..100)
9990            .map(|i| EcrRepository {
9991                name: format!("repo{}", i),
9992                uri: format!("uri{}", i),
9993                created_at: "2023-01-01".to_string(),
9994                tag_immutability: "MUTABLE".to_string(),
9995                encryption_type: "AES256".to_string(),
9996            })
9997            .collect();
9998
9999        // Type "3" then "p" to go to page 3
10000        app.handle_action(Action::FilterInput('3'));
10001        app.handle_action(Action::OpenColumnSelector);
10002
10003        // With page_size=25, page 3 starts at index 50
10004        assert_eq!(app.ecr_state.repositories.selected, 50);
10005
10006        let page_size = app.ecr_state.repositories.page_size.value();
10007        let current_page = app.ecr_state.repositories.selected / page_size;
10008        assert_eq!(
10009            current_page, 2,
10010            "3p with page_size=25 should go to page 3 (0-indexed as 2)"
10011        );
10012    }
10013
10014    #[test]
10015    fn test_page_navigation_updates_scroll_offset_for_alarms() {
10016        let mut app = test_app();
10017        app.current_service = Service::CloudWatchAlarms;
10018        app.service_selected = true;
10019        app.mode = Mode::Normal;
10020        app.alarms_state.table.items = (0..100)
10021            .map(|i| crate::cw::alarms::Alarm {
10022                name: format!("alarm-{}", i),
10023                state: "OK".to_string(),
10024                state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
10025                description: String::new(),
10026                metric_name: "CPUUtilization".to_string(),
10027                namespace: "AWS/EC2".to_string(),
10028                statistic: "Average".to_string(),
10029                period: 300,
10030                comparison_operator: "GreaterThanThreshold".to_string(),
10031                threshold: 80.0,
10032                actions_enabled: true,
10033                state_reason: String::new(),
10034                resource: String::new(),
10035                dimensions: String::new(),
10036                expression: String::new(),
10037                alarm_type: "MetricAlarm".to_string(),
10038                cross_account: String::new(),
10039            })
10040            .collect();
10041
10042        // Type "2" then "p" to go to page 2
10043        app.handle_action(Action::FilterInput('2'));
10044        app.handle_action(Action::OpenColumnSelector);
10045
10046        // Verify both selected and scroll_offset are updated
10047        let page_size = app.alarms_state.table.page_size.value();
10048        let expected_offset = page_size; // Page 2 starts at page_size
10049        assert_eq!(app.alarms_state.table.selected, expected_offset);
10050        assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
10051    }
10052
10053    #[test]
10054    fn test_ecr_pagination_with_65_repos() {
10055        let mut app = test_app();
10056        app.current_service = Service::EcrRepositories;
10057        app.service_selected = true;
10058        app.mode = Mode::Normal;
10059        app.ecr_state.repositories.items = (0..65)
10060            .map(|i| EcrRepository {
10061                name: format!("repo{:02}", i),
10062                uri: format!("uri{}", i),
10063                created_at: "2023-01-01".to_string(),
10064                tag_immutability: "MUTABLE".to_string(),
10065                encryption_type: "AES256".to_string(),
10066            })
10067            .collect();
10068
10069        // Page 1: items 0-49 (50 items)
10070        assert_eq!(app.ecr_state.repositories.selected, 0);
10071        let page_size = 50;
10072        let current_page = app.ecr_state.repositories.selected / page_size;
10073        assert_eq!(current_page, 0);
10074
10075        // Go to page 2
10076        app.handle_action(Action::FilterInput('2'));
10077        app.handle_action(Action::OpenColumnSelector);
10078        assert_eq!(app.ecr_state.repositories.selected, 50);
10079
10080        // Page 2: items 50-64 (15 items)
10081        let current_page = app.ecr_state.repositories.selected / page_size;
10082        assert_eq!(current_page, 1);
10083    }
10084
10085    #[test]
10086    fn test_ecr_repos_input_focus_tab_cycling() {
10087        let mut app = test_app();
10088        app.current_service = Service::EcrRepositories;
10089        app.service_selected = true;
10090        app.mode = Mode::FilterInput;
10091        app.ecr_state.input_focus = InputFocus::Filter;
10092
10093        // Tab should cycle to Pagination
10094        app.handle_action(Action::NextFilterFocus);
10095        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10096
10097        // Tab again should cycle back to Input
10098        app.handle_action(Action::NextFilterFocus);
10099        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10100
10101        // Shift+Tab should cycle backwards to Pagination
10102        app.handle_action(Action::PrevFilterFocus);
10103        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10104
10105        // Shift+Tab again should cycle back to Input
10106        app.handle_action(Action::PrevFilterFocus);
10107        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10108    }
10109
10110    #[test]
10111    fn test_ecr_images_column_toggle_not_off_by_one() {
10112        use crate::ecr::image::Column as ImageColumn;
10113        let mut app = test_app();
10114        app.current_service = Service::EcrRepositories;
10115        app.service_selected = true;
10116        app.mode = Mode::ColumnSelector;
10117        app.ecr_state.current_repository = Some("test-repo".to_string());
10118
10119        // Start with all columns visible
10120        app.visible_ecr_image_columns = ImageColumn::all();
10121        let initial_count = app.visible_ecr_image_columns.len();
10122
10123        // Select first column (index 0) and toggle it
10124        app.column_selector_index = 0;
10125        app.handle_action(Action::ToggleColumn);
10126
10127        // First column should be removed
10128        assert_eq!(app.visible_ecr_image_columns.len(), initial_count - 1);
10129        assert!(!app.visible_ecr_image_columns.contains(&ImageColumn::Tag));
10130
10131        // Toggle it back
10132        app.handle_action(Action::ToggleColumn);
10133        assert_eq!(app.visible_ecr_image_columns.len(), initial_count);
10134        assert!(app.visible_ecr_image_columns.contains(&ImageColumn::Tag));
10135    }
10136
10137    #[test]
10138    fn test_ecr_repos_column_toggle_works() {
10139        use crate::ecr::repo::Column;
10140        let mut app = test_app();
10141        app.current_service = Service::EcrRepositories;
10142        app.service_selected = true;
10143        app.mode = Mode::ColumnSelector;
10144        app.ecr_state.current_repository = None;
10145
10146        // Start with all columns visible
10147        app.visible_ecr_columns = Column::all();
10148        let initial_count = app.visible_ecr_columns.len();
10149
10150        // Select first column (index 0) and toggle it
10151        app.column_selector_index = 0;
10152        app.handle_action(Action::ToggleColumn);
10153
10154        // First column should be removed
10155        assert_eq!(app.visible_ecr_columns.len(), initial_count - 1);
10156        assert!(!app.visible_ecr_columns.contains(&Column::Name));
10157
10158        // Toggle it back
10159        app.handle_action(Action::ToggleColumn);
10160        assert_eq!(app.visible_ecr_columns.len(), initial_count);
10161        assert!(app.visible_ecr_columns.contains(&Column::Name));
10162    }
10163
10164    #[test]
10165    fn test_ecr_repos_pagination_left_right_navigation() {
10166        use crate::ecr::repo::Repository as EcrRepository;
10167        let mut app = test_app();
10168        app.current_service = Service::EcrRepositories;
10169        app.service_selected = true;
10170        app.mode = Mode::FilterInput;
10171        app.ecr_state.input_focus = InputFocus::Pagination;
10172
10173        // Create 150 repos (3 pages with page size 50)
10174        app.ecr_state.repositories.items = (0..150)
10175            .map(|i| EcrRepository {
10176                name: format!("repo{:03}", i),
10177                uri: format!("uri{}", i),
10178                created_at: "2023-01-01".to_string(),
10179                tag_immutability: "MUTABLE".to_string(),
10180                encryption_type: "AES256".to_string(),
10181            })
10182            .collect();
10183
10184        // Start on page 1 (index 0)
10185        app.ecr_state.repositories.selected = 0;
10186        eprintln!(
10187            "Initial: selected={}, focus={:?}, mode={:?}",
10188            app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
10189        );
10190
10191        // Right arrow (PageDown) should go to page 2
10192        app.handle_action(Action::PageDown);
10193        eprintln!(
10194            "After PageDown: selected={}",
10195            app.ecr_state.repositories.selected
10196        );
10197        assert_eq!(app.ecr_state.repositories.selected, 50);
10198
10199        // Right arrow again should go to page 3
10200        app.handle_action(Action::PageDown);
10201        eprintln!(
10202            "After 2nd PageDown: selected={}",
10203            app.ecr_state.repositories.selected
10204        );
10205        assert_eq!(app.ecr_state.repositories.selected, 100);
10206
10207        // Right arrow at last page should stay at last page
10208        app.handle_action(Action::PageDown);
10209        eprintln!(
10210            "After 3rd PageDown: selected={}",
10211            app.ecr_state.repositories.selected
10212        );
10213        assert_eq!(app.ecr_state.repositories.selected, 100);
10214
10215        // Left arrow (PageUp) should go back to page 2
10216        app.handle_action(Action::PageUp);
10217        eprintln!(
10218            "After PageUp: selected={}",
10219            app.ecr_state.repositories.selected
10220        );
10221        assert_eq!(app.ecr_state.repositories.selected, 50);
10222
10223        // Left arrow again should go to page 1
10224        app.handle_action(Action::PageUp);
10225        eprintln!(
10226            "After 2nd PageUp: selected={}",
10227            app.ecr_state.repositories.selected
10228        );
10229        assert_eq!(app.ecr_state.repositories.selected, 0);
10230
10231        // Left arrow at first page should stay at first page
10232        app.handle_action(Action::PageUp);
10233        eprintln!(
10234            "After 3rd PageUp: selected={}",
10235            app.ecr_state.repositories.selected
10236        );
10237        assert_eq!(app.ecr_state.repositories.selected, 0);
10238    }
10239
10240    #[test]
10241    fn test_ecr_repos_filter_input_when_input_focused() {
10242        use crate::ecr::repo::Repository as EcrRepository;
10243        let mut app = test_app();
10244        app.current_service = Service::EcrRepositories;
10245        app.service_selected = true;
10246        app.mode = Mode::FilterInput;
10247        app.ecr_state.input_focus = InputFocus::Filter;
10248
10249        // Create some repos
10250        app.ecr_state.repositories.items = vec![
10251            EcrRepository {
10252                name: "test-repo".to_string(),
10253                uri: "uri1".to_string(),
10254                created_at: "2023-01-01".to_string(),
10255                tag_immutability: "MUTABLE".to_string(),
10256                encryption_type: "AES256".to_string(),
10257            },
10258            EcrRepository {
10259                name: "prod-repo".to_string(),
10260                uri: "uri2".to_string(),
10261                created_at: "2023-01-01".to_string(),
10262                tag_immutability: "MUTABLE".to_string(),
10263                encryption_type: "AES256".to_string(),
10264            },
10265        ];
10266
10267        // When input is focused, typing should add to filter
10268        assert_eq!(app.ecr_state.repositories.filter, "");
10269        app.handle_action(Action::FilterInput('t'));
10270        assert_eq!(app.ecr_state.repositories.filter, "t");
10271        app.handle_action(Action::FilterInput('e'));
10272        assert_eq!(app.ecr_state.repositories.filter, "te");
10273        app.handle_action(Action::FilterInput('s'));
10274        assert_eq!(app.ecr_state.repositories.filter, "tes");
10275        app.handle_action(Action::FilterInput('t'));
10276        assert_eq!(app.ecr_state.repositories.filter, "test");
10277    }
10278
10279    #[test]
10280    fn test_ecr_repos_digit_input_when_pagination_focused() {
10281        use crate::ecr::repo::Repository as EcrRepository;
10282        let mut app = test_app();
10283        app.current_service = Service::EcrRepositories;
10284        app.service_selected = true;
10285        app.mode = Mode::FilterInput;
10286        app.ecr_state.input_focus = InputFocus::Pagination;
10287
10288        // Create some repos
10289        app.ecr_state.repositories.items = vec![EcrRepository {
10290            name: "test-repo".to_string(),
10291            uri: "uri1".to_string(),
10292            created_at: "2023-01-01".to_string(),
10293            tag_immutability: "MUTABLE".to_string(),
10294            encryption_type: "AES256".to_string(),
10295        }];
10296
10297        // When pagination is focused, digits should go to page_input, not filter
10298        assert_eq!(app.ecr_state.repositories.filter, "");
10299        assert_eq!(app.page_input, "");
10300        app.handle_action(Action::FilterInput('2'));
10301        assert_eq!(app.ecr_state.repositories.filter, "");
10302        assert_eq!(app.page_input, "2");
10303
10304        // Non-digits should not be added to either
10305        app.handle_action(Action::FilterInput('a'));
10306        assert_eq!(app.ecr_state.repositories.filter, "");
10307        assert_eq!(app.page_input, "2");
10308    }
10309
10310    #[test]
10311    fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
10312        use crate::ecr::repo::Repository as EcrRepository;
10313        let mut app = test_app();
10314        app.current_service = Service::EcrRepositories;
10315        app.service_selected = true;
10316        app.mode = Mode::FilterInput;
10317        app.ecr_state.input_focus = InputFocus::Filter;
10318
10319        // Create 150 repos (3 pages)
10320        app.ecr_state.repositories.items = (0..150)
10321            .map(|i| EcrRepository {
10322                name: format!("repo{:03}", i),
10323                uri: format!("uri{}", i),
10324                created_at: "2023-01-01".to_string(),
10325                tag_immutability: "MUTABLE".to_string(),
10326                encryption_type: "AES256".to_string(),
10327            })
10328            .collect();
10329
10330        // Start on page 1
10331        app.ecr_state.repositories.selected = 0;
10332
10333        // When input is focused, left/right should scroll table (not change pages)
10334        app.handle_action(Action::PageDown);
10335        assert_eq!(
10336            app.ecr_state.repositories.selected, 10,
10337            "Should scroll down by 10"
10338        );
10339
10340        app.handle_action(Action::PageUp);
10341        assert_eq!(
10342            app.ecr_state.repositories.selected, 0,
10343            "Should scroll back up"
10344        );
10345    }
10346
10347    #[test]
10348    fn test_ecr_repos_pagination_control_actually_works() {
10349        use crate::ecr::repo::Repository as EcrRepository;
10350
10351        // Test that verifies the exact conditions needed for pagination to work
10352        let mut app = test_app();
10353        app.current_service = Service::EcrRepositories;
10354        app.service_selected = true;
10355        app.mode = Mode::FilterInput;
10356        app.ecr_state.current_repository = None;
10357        app.ecr_state.input_focus = InputFocus::Pagination;
10358
10359        // Create 100 repos (2 pages with page size 50)
10360        app.ecr_state.repositories.items = (0..100)
10361            .map(|i| EcrRepository {
10362                name: format!("repo{:03}", i),
10363                uri: format!("uri{}", i),
10364                created_at: "2023-01-01".to_string(),
10365                tag_immutability: "MUTABLE".to_string(),
10366                encryption_type: "AES256".to_string(),
10367            })
10368            .collect();
10369
10370        app.ecr_state.repositories.selected = 0;
10371
10372        // Verify all conditions are met
10373        assert_eq!(app.mode, Mode::FilterInput);
10374        assert_eq!(app.current_service, Service::EcrRepositories);
10375        assert_eq!(app.ecr_state.current_repository, None);
10376        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10377
10378        // Now test pagination
10379        app.handle_action(Action::PageDown);
10380        assert_eq!(
10381            app.ecr_state.repositories.selected, 50,
10382            "PageDown should move to page 2"
10383        );
10384
10385        app.handle_action(Action::PageUp);
10386        assert_eq!(
10387            app.ecr_state.repositories.selected, 0,
10388            "PageUp should move back to page 1"
10389        );
10390    }
10391
10392    #[test]
10393    fn test_ecr_repos_start_filter_resets_focus_to_input() {
10394        let mut app = test_app();
10395        app.current_service = Service::EcrRepositories;
10396        app.service_selected = true;
10397        app.mode = Mode::Normal;
10398        app.ecr_state.current_repository = None;
10399
10400        // Set focus to Pagination
10401        app.ecr_state.input_focus = InputFocus::Pagination;
10402
10403        // Start filter mode
10404        app.handle_action(Action::StartFilter);
10405
10406        // Should reset to Input focus
10407        assert_eq!(app.mode, Mode::FilterInput);
10408        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10409    }
10410
10411    #[test]
10412    fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
10413        use crate::ecr::repo::Repository as EcrRepository;
10414
10415        let mut app = test_app();
10416        app.current_service = Service::EcrRepositories;
10417        app.service_selected = true;
10418        app.mode = Mode::Normal;
10419        app.ecr_state.current_repository = None;
10420
10421        // Create 100 repos (2 pages)
10422        app.ecr_state.repositories.items = (0..100)
10423            .map(|i| EcrRepository {
10424                name: format!("repo{:03}", i),
10425                uri: format!("uri{}", i),
10426                created_at: "2023-01-01".to_string(),
10427                tag_immutability: "MUTABLE".to_string(),
10428                encryption_type: "AES256".to_string(),
10429            })
10430            .collect();
10431
10432        app.ecr_state.repositories.selected = 0;
10433
10434        // User presses 'i' to enter filter mode
10435        app.handle_action(Action::StartFilter);
10436        assert_eq!(app.mode, Mode::FilterInput);
10437        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10438
10439        // User presses Tab to switch to pagination
10440        app.handle_action(Action::NextFilterFocus);
10441        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10442
10443        // User presses right arrow (PageDown)
10444        eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
10445            app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
10446        app.handle_action(Action::PageDown);
10447        eprintln!(
10448            "After PageDown: selected={}",
10449            app.ecr_state.repositories.selected
10450        );
10451
10452        // Should move to page 2
10453        assert_eq!(
10454            app.ecr_state.repositories.selected, 50,
10455            "Right arrow should move to page 2"
10456        );
10457
10458        // User presses left arrow (PageUp)
10459        app.handle_action(Action::PageUp);
10460        assert_eq!(
10461            app.ecr_state.repositories.selected, 0,
10462            "Left arrow should move back to page 1"
10463        );
10464    }
10465
10466    #[test]
10467    fn test_service_picker_i_key_activates_filter() {
10468        let mut app = test_app();
10469
10470        // Start in ServicePicker mode (service picker)
10471        assert_eq!(app.mode, Mode::ServicePicker);
10472        assert!(app.service_picker.filter.is_empty());
10473
10474        // Press 'i' to start filtering
10475        app.handle_action(Action::FilterInput('i'));
10476
10477        // Should still be in ServicePicker mode and filter should have 'i'
10478        assert_eq!(app.mode, Mode::ServicePicker);
10479        assert_eq!(app.service_picker.filter, "i");
10480    }
10481
10482    #[test]
10483    fn test_service_picker_typing_filters_services() {
10484        let mut app = test_app();
10485
10486        // Start in ServicePicker mode
10487        assert_eq!(app.mode, Mode::ServicePicker);
10488
10489        // Type "s3" to filter
10490        app.handle_action(Action::FilterInput('s'));
10491        app.handle_action(Action::FilterInput('3'));
10492
10493        assert_eq!(app.service_picker.filter, "s3");
10494        assert_eq!(app.mode, Mode::ServicePicker);
10495    }
10496
10497    #[test]
10498    fn test_service_picker_resets_on_open() {
10499        let mut app = test_app();
10500
10501        // Select a service to get into Normal mode
10502        app.service_selected = true;
10503        app.mode = Mode::Normal;
10504
10505        // Simulate having previous filter and selection
10506        app.service_picker.filter = "previous".to_string();
10507        app.service_picker.selected = 5;
10508
10509        // Open space menu (service picker)
10510        app.handle_action(Action::OpenSpaceMenu);
10511
10512        // Filter and selection should be reset
10513        assert_eq!(app.mode, Mode::SpaceMenu);
10514        assert!(app.service_picker.filter.is_empty());
10515        assert_eq!(app.service_picker.selected, 0);
10516    }
10517
10518    #[test]
10519    fn test_no_pii_in_test_data() {
10520        // Ensure test data uses placeholder account IDs, not real ones
10521        let test_repo = EcrRepository {
10522            name: "test-repo".to_string(),
10523            uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
10524            created_at: "2024-01-01".to_string(),
10525            tag_immutability: "MUTABLE".to_string(),
10526            encryption_type: "AES256".to_string(),
10527        };
10528
10529        // Verify placeholder account ID is used
10530        assert!(test_repo.uri.starts_with("123456789012"));
10531        assert!(!test_repo.uri.contains("123456789013")); // Not a real account
10532    }
10533
10534    #[test]
10535    fn test_lambda_versions_tab_triggers_loading() {
10536        let mut app = test_app();
10537        app.current_service = Service::LambdaFunctions;
10538        app.service_selected = true;
10539
10540        // Simulate selecting a function
10541        app.lambda_state.current_function = Some("test-function".to_string());
10542        app.lambda_state.detail_tab = LambdaDetailTab::Code;
10543
10544        // Initially no versions
10545        assert!(app.lambda_state.version_table.items.is_empty());
10546
10547        // Switch to Versions tab
10548        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10549
10550        // The main loop should detect this change and load versions
10551        // We verify the state is set up correctly for loading
10552        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
10553        assert!(app.lambda_state.current_function.is_some());
10554    }
10555
10556    #[test]
10557    fn test_lambda_versions_navigation() {
10558        use crate::lambda::Version;
10559
10560        let mut app = test_app();
10561        app.current_service = Service::LambdaFunctions;
10562        app.service_selected = true;
10563        app.lambda_state.current_function = Some("test-function".to_string());
10564        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10565
10566        // Add test versions
10567        app.lambda_state.version_table.items = vec![
10568            Version {
10569                version: "3".to_string(),
10570                aliases: "prod".to_string(),
10571                description: "".to_string(),
10572                last_modified: "".to_string(),
10573                architecture: "X86_64".to_string(),
10574            },
10575            Version {
10576                version: "2".to_string(),
10577                aliases: "".to_string(),
10578                description: "".to_string(),
10579                last_modified: "".to_string(),
10580                architecture: "X86_64".to_string(),
10581            },
10582            Version {
10583                version: "1".to_string(),
10584                aliases: "".to_string(),
10585                description: "".to_string(),
10586                last_modified: "".to_string(),
10587                architecture: "X86_64".to_string(),
10588            },
10589        ];
10590
10591        // Verify versions are loaded
10592        assert_eq!(app.lambda_state.version_table.items.len(), 3);
10593        assert_eq!(app.lambda_state.version_table.items[0].version, "3");
10594        assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
10595
10596        // Verify selection can be changed
10597        app.lambda_state.version_table.selected = 1;
10598        assert_eq!(app.lambda_state.version_table.selected, 1);
10599    }
10600
10601    #[test]
10602    fn test_lambda_versions_with_aliases() {
10603        use crate::lambda::Version;
10604
10605        let version = Version {
10606            version: "35".to_string(),
10607            aliases: "prod, staging".to_string(),
10608            description: "Production version".to_string(),
10609            last_modified: "2024-01-01".to_string(),
10610            architecture: "X86_64".to_string(),
10611        };
10612
10613        assert_eq!(version.aliases, "prod, staging");
10614        assert!(!version.aliases.is_empty());
10615    }
10616
10617    #[test]
10618    fn test_lambda_versions_expansion() {
10619        use crate::lambda::Version;
10620
10621        let mut app = test_app();
10622        app.current_service = Service::LambdaFunctions;
10623        app.service_selected = true;
10624        app.lambda_state.current_function = Some("test-function".to_string());
10625        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10626
10627        // Add test versions
10628        app.lambda_state.version_table.items = vec![
10629            Version {
10630                version: "2".to_string(),
10631                aliases: "prod".to_string(),
10632                description: "Production".to_string(),
10633                last_modified: "2024-01-01".to_string(),
10634                architecture: "X86_64".to_string(),
10635            },
10636            Version {
10637                version: "1".to_string(),
10638                aliases: "".to_string(),
10639                description: "".to_string(),
10640                last_modified: "2024-01-01".to_string(),
10641                architecture: "Arm64".to_string(),
10642            },
10643        ];
10644
10645        app.lambda_state.version_table.selected = 0;
10646
10647        // Verify expansion can be set
10648        app.lambda_state.version_table.expanded_item = Some(0);
10649        assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
10650
10651        // Select different version
10652        app.lambda_state.version_table.selected = 1;
10653        app.lambda_state.version_table.expanded_item = Some(1);
10654        assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
10655    }
10656
10657    #[test]
10658    fn test_lambda_versions_page_navigation() {
10659        use crate::lambda::Version;
10660
10661        let mut app = test_app();
10662        app.current_service = Service::LambdaFunctions;
10663        app.service_selected = true;
10664        app.lambda_state.current_function = Some("test-function".to_string());
10665        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10666
10667        // Add 30 test versions
10668        app.lambda_state.version_table.items = (1..=30)
10669            .map(|i| Version {
10670                version: i.to_string(),
10671                aliases: "".to_string(),
10672                description: "".to_string(),
10673                last_modified: "".to_string(),
10674                architecture: "X86_64".to_string(),
10675            })
10676            .collect();
10677
10678        app.lambda_state.version_table.page_size = PageSize::Ten;
10679        app.lambda_state.version_table.selected = 0;
10680
10681        // Go to page 2
10682        app.page_input = "2".to_string();
10683        app.handle_action(Action::OpenColumnSelector);
10684
10685        // Should be at index 10 (start of page 2)
10686        assert_eq!(app.lambda_state.version_table.selected, 10);
10687    }
10688
10689    #[test]
10690    fn test_lambda_versions_pagination_arrow_keys() {
10691        use crate::lambda::Version;
10692
10693        let mut app = test_app();
10694        app.current_service = Service::LambdaFunctions;
10695        app.service_selected = true;
10696        app.lambda_state.current_function = Some("test-function".to_string());
10697        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10698        app.mode = Mode::FilterInput;
10699        app.lambda_state.version_input_focus = InputFocus::Pagination;
10700
10701        // Add 30 test versions
10702        app.lambda_state.version_table.items = (1..=30)
10703            .map(|i| Version {
10704                version: i.to_string(),
10705                aliases: "".to_string(),
10706                description: "".to_string(),
10707                last_modified: "".to_string(),
10708                architecture: "X86_64".to_string(),
10709            })
10710            .collect();
10711
10712        app.lambda_state.version_table.page_size = PageSize::Ten;
10713        app.lambda_state.version_table.selected = 0;
10714
10715        // Right arrow (PageDown) should go to next page
10716        app.handle_action(Action::PageDown);
10717        assert_eq!(app.lambda_state.version_table.selected, 10);
10718
10719        // Left arrow (PageUp) should go back
10720        app.handle_action(Action::PageUp);
10721        assert_eq!(app.lambda_state.version_table.selected, 0);
10722    }
10723
10724    #[test]
10725    fn test_lambda_versions_page_input_in_filter_mode() {
10726        use crate::lambda::Version;
10727
10728        let mut app = test_app();
10729        app.current_service = Service::LambdaFunctions;
10730        app.service_selected = true;
10731        app.lambda_state.current_function = Some("test-function".to_string());
10732        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10733        app.mode = Mode::FilterInput;
10734        app.lambda_state.version_input_focus = InputFocus::Pagination;
10735
10736        // Add 30 test versions
10737        app.lambda_state.version_table.items = (1..=30)
10738            .map(|i| Version {
10739                version: i.to_string(),
10740                aliases: "".to_string(),
10741                description: "".to_string(),
10742                last_modified: "".to_string(),
10743                architecture: "X86_64".to_string(),
10744            })
10745            .collect();
10746
10747        app.lambda_state.version_table.page_size = PageSize::Ten;
10748        app.lambda_state.version_table.selected = 0;
10749
10750        // Type "2" when focused on Pagination
10751        app.handle_action(Action::FilterInput('2'));
10752        assert_eq!(app.page_input, "2");
10753        assert_eq!(app.lambda_state.version_table.filter, ""); // Should not go to filter
10754
10755        // Press 'p' to go to page 2
10756        app.handle_action(Action::OpenColumnSelector);
10757        assert_eq!(app.lambda_state.version_table.selected, 10);
10758        assert_eq!(app.page_input, ""); // Should be cleared
10759    }
10760
10761    #[test]
10762    fn test_lambda_versions_filter_input() {
10763        use crate::lambda::Version;
10764
10765        let mut app = test_app();
10766        app.current_service = Service::LambdaFunctions;
10767        app.service_selected = true;
10768        app.lambda_state.current_function = Some("test-function".to_string());
10769        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10770        app.mode = Mode::FilterInput;
10771        app.lambda_state.version_input_focus = InputFocus::Filter;
10772
10773        // Add test versions
10774        app.lambda_state.version_table.items = vec![
10775            Version {
10776                version: "1".to_string(),
10777                aliases: "prod".to_string(),
10778                description: "Production".to_string(),
10779                last_modified: "".to_string(),
10780                architecture: "X86_64".to_string(),
10781            },
10782            Version {
10783                version: "2".to_string(),
10784                aliases: "staging".to_string(),
10785                description: "Staging".to_string(),
10786                last_modified: "".to_string(),
10787                architecture: "X86_64".to_string(),
10788            },
10789        ];
10790
10791        // Type filter text
10792        app.handle_action(Action::FilterInput('p'));
10793        app.handle_action(Action::FilterInput('r'));
10794        app.handle_action(Action::FilterInput('o'));
10795        app.handle_action(Action::FilterInput('d'));
10796        assert_eq!(app.lambda_state.version_table.filter, "prod");
10797
10798        // Backspace should work
10799        app.handle_action(Action::FilterBackspace);
10800        assert_eq!(app.lambda_state.version_table.filter, "pro");
10801    }
10802
10803    #[test]
10804    fn test_lambda_aliases_table_expansion() {
10805        use crate::lambda::Alias;
10806
10807        let mut app = test_app();
10808        app.current_service = Service::LambdaFunctions;
10809        app.service_selected = true;
10810        app.lambda_state.current_function = Some("test-function".to_string());
10811        app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
10812        app.mode = Mode::Normal;
10813
10814        app.lambda_state.alias_table.items = vec![
10815            Alias {
10816                name: "prod".to_string(),
10817                versions: "1".to_string(),
10818                description: "Production alias".to_string(),
10819            },
10820            Alias {
10821                name: "staging".to_string(),
10822                versions: "2".to_string(),
10823                description: "Staging alias".to_string(),
10824            },
10825        ];
10826
10827        app.lambda_state.alias_table.selected = 0;
10828
10829        // Select first alias - should open alias detail view (no tab change)
10830        app.handle_action(Action::Select);
10831        assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
10832        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
10833
10834        // Go back
10835        app.handle_action(Action::GoBack);
10836        assert_eq!(app.lambda_state.current_alias, None);
10837        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
10838
10839        // Select second alias
10840        app.lambda_state.alias_table.selected = 1;
10841        app.handle_action(Action::Select);
10842        assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
10843    }
10844
10845    #[test]
10846    fn test_lambda_versions_arrow_key_expansion() {
10847        use crate::lambda::Version;
10848
10849        let mut app = test_app();
10850        app.current_service = Service::LambdaFunctions;
10851        app.service_selected = true;
10852        app.lambda_state.current_function = Some("test-function".to_string());
10853        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10854        app.mode = Mode::Normal;
10855
10856        app.lambda_state.version_table.items = vec![Version {
10857            version: "1".to_string(),
10858            aliases: "prod".to_string(),
10859            description: "Production".to_string(),
10860            last_modified: "2024-01-01".to_string(),
10861            architecture: "X86_64".to_string(),
10862        }];
10863
10864        app.lambda_state.version_table.selected = 0;
10865
10866        // Right arrow expands
10867        app.handle_action(Action::NextPane);
10868        assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
10869
10870        // Left arrow collapses
10871        app.handle_action(Action::PrevPane);
10872        assert_eq!(app.lambda_state.version_table.expanded_item, None);
10873    }
10874
10875    #[test]
10876    fn test_lambda_version_detail_view() {
10877        use crate::lambda::Function;
10878
10879        let mut app = test_app();
10880        app.current_service = Service::LambdaFunctions;
10881        app.service_selected = true;
10882        app.lambda_state.current_function = Some("test-function".to_string());
10883        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10884        app.mode = Mode::Normal;
10885
10886        app.lambda_state.table.items = vec![Function {
10887            name: "test-function".to_string(),
10888            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
10889            application: None,
10890            description: "Test".to_string(),
10891            package_type: "Zip".to_string(),
10892            runtime: "python3.12".to_string(),
10893            architecture: "X86_64".to_string(),
10894            code_size: 1024,
10895            code_sha256: "hash".to_string(),
10896            memory_mb: 128,
10897            timeout_seconds: 30,
10898            last_modified: "2024-01-01".to_string(),
10899            layers: vec![],
10900        }];
10901
10902        app.lambda_state.version_table.items = vec![crate::lambda::Version {
10903            version: "1".to_string(),
10904            aliases: "prod".to_string(),
10905            description: "Production".to_string(),
10906            last_modified: "2024-01-01".to_string(),
10907            architecture: "X86_64".to_string(),
10908        }];
10909
10910        app.lambda_state.version_table.selected = 0;
10911
10912        // Select version to open detail view
10913        app.handle_action(Action::Select);
10914        assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
10915        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
10916
10917        // GoBack should go back to versions list
10918        app.handle_action(Action::GoBack);
10919        assert_eq!(app.lambda_state.current_version, None);
10920        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
10921    }
10922
10923    #[test]
10924    fn test_lambda_version_detail_tabs() {
10925        use crate::lambda::Function;
10926
10927        let mut app = test_app();
10928        app.current_service = Service::LambdaFunctions;
10929        app.service_selected = true;
10930        app.lambda_state.current_function = Some("test-function".to_string());
10931        app.lambda_state.current_version = Some("1".to_string());
10932        app.lambda_state.detail_tab = LambdaDetailTab::Code;
10933        app.mode = Mode::Normal;
10934
10935        app.lambda_state.table.items = vec![Function {
10936            name: "test-function".to_string(),
10937            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
10938            application: None,
10939            description: "Test".to_string(),
10940            package_type: "Zip".to_string(),
10941            runtime: "python3.12".to_string(),
10942            architecture: "X86_64".to_string(),
10943            code_size: 1024,
10944            code_sha256: "hash".to_string(),
10945            memory_mb: 128,
10946            timeout_seconds: 30,
10947            last_modified: "2024-01-01".to_string(),
10948            layers: vec![],
10949        }];
10950
10951        // Tab should cycle between Code and Configuration only
10952        app.handle_action(Action::NextDetailTab);
10953        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
10954
10955        app.handle_action(Action::NextDetailTab);
10956        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
10957
10958        // BackTab should cycle backward
10959        app.handle_action(Action::PrevDetailTab);
10960        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
10961    }
10962
10963    #[test]
10964    fn test_lambda_aliases_arrow_key_expansion() {
10965        use crate::lambda::Alias;
10966
10967        let mut app = test_app();
10968        app.current_service = Service::LambdaFunctions;
10969        app.service_selected = true;
10970        app.lambda_state.current_function = Some("test-function".to_string());
10971        app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
10972        app.mode = Mode::Normal;
10973
10974        app.lambda_state.alias_table.items = vec![Alias {
10975            name: "prod".to_string(),
10976            versions: "1".to_string(),
10977            description: "Production alias".to_string(),
10978        }];
10979
10980        app.lambda_state.alias_table.selected = 0;
10981
10982        // Right arrow expands
10983        app.handle_action(Action::NextPane);
10984        assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
10985
10986        // Left arrow collapses
10987        app.handle_action(Action::PrevPane);
10988        assert_eq!(app.lambda_state.alias_table.expanded_item, None);
10989    }
10990
10991    #[test]
10992    fn test_lambda_functions_arrow_key_expansion() {
10993        use crate::lambda::Function;
10994
10995        let mut app = test_app();
10996        app.current_service = Service::LambdaFunctions;
10997        app.service_selected = true;
10998        app.mode = Mode::Normal;
10999
11000        app.lambda_state.table.items = vec![Function {
11001            name: "test-function".to_string(),
11002            arn: "arn".to_string(),
11003            application: None,
11004            description: "Test".to_string(),
11005            package_type: "Zip".to_string(),
11006            runtime: "python3.12".to_string(),
11007            architecture: "X86_64".to_string(),
11008            code_size: 1024,
11009            code_sha256: "hash".to_string(),
11010            memory_mb: 128,
11011            timeout_seconds: 30,
11012            last_modified: "2024-01-01".to_string(),
11013            layers: vec![],
11014        }];
11015
11016        app.lambda_state.table.selected = 0;
11017
11018        // Right arrow expands
11019        app.handle_action(Action::NextPane);
11020        assert_eq!(app.lambda_state.table.expanded_item, Some(0));
11021
11022        // Left arrow collapses
11023        app.handle_action(Action::PrevPane);
11024        assert_eq!(app.lambda_state.table.expanded_item, None);
11025    }
11026
11027    #[test]
11028    fn test_lambda_version_detail_with_application() {
11029        use crate::lambda::Function;
11030
11031        let mut app = test_app();
11032        app.current_service = Service::LambdaFunctions;
11033        app.service_selected = true;
11034        app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
11035        app.lambda_state.current_version = Some("1".to_string());
11036        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11037        app.mode = Mode::Normal;
11038
11039        app.lambda_state.table.items = vec![Function {
11040            name: "storefront-studio-beta-api".to_string(),
11041            arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
11042                .to_string(),
11043            application: Some("storefront-studio-beta".to_string()),
11044            description: "API function".to_string(),
11045            package_type: "Zip".to_string(),
11046            runtime: "python3.12".to_string(),
11047            architecture: "X86_64".to_string(),
11048            code_size: 1024,
11049            code_sha256: "hash".to_string(),
11050            memory_mb: 128,
11051            timeout_seconds: 30,
11052            last_modified: "2024-01-01".to_string(),
11053            layers: vec![],
11054        }];
11055
11056        // Verify function has application extracted
11057        assert_eq!(
11058            app.lambda_state.table.items[0].application,
11059            Some("storefront-studio-beta".to_string())
11060        );
11061        assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11062    }
11063
11064    #[test]
11065    fn test_lambda_layer_navigation() {
11066        use crate::lambda::{Function, Layer};
11067
11068        let mut app = test_app();
11069        app.current_service = Service::LambdaFunctions;
11070        app.service_selected = true;
11071        app.lambda_state.current_function = Some("test-function".to_string());
11072        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11073        app.mode = Mode::Normal;
11074
11075        app.lambda_state.table.items = vec![Function {
11076            name: "test-function".to_string(),
11077            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11078            application: None,
11079            description: "Test".to_string(),
11080            package_type: "Zip".to_string(),
11081            runtime: "python3.12".to_string(),
11082            architecture: "X86_64".to_string(),
11083            code_size: 1024,
11084            code_sha256: "hash".to_string(),
11085            memory_mb: 128,
11086            timeout_seconds: 30,
11087            last_modified: "2024-01-01".to_string(),
11088            layers: vec![
11089                Layer {
11090                    arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11091                    code_size: 1024,
11092                },
11093                Layer {
11094                    arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11095                    code_size: 2048,
11096                },
11097                Layer {
11098                    arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
11099                    code_size: 3072,
11100                },
11101            ],
11102        }];
11103
11104        assert_eq!(app.lambda_state.layer_selected, 0);
11105
11106        app.handle_action(Action::NextItem);
11107        assert_eq!(app.lambda_state.layer_selected, 1);
11108
11109        app.handle_action(Action::NextItem);
11110        assert_eq!(app.lambda_state.layer_selected, 2);
11111
11112        app.handle_action(Action::NextItem);
11113        assert_eq!(app.lambda_state.layer_selected, 2);
11114
11115        app.handle_action(Action::PrevItem);
11116        assert_eq!(app.lambda_state.layer_selected, 1);
11117
11118        app.handle_action(Action::PrevItem);
11119        assert_eq!(app.lambda_state.layer_selected, 0);
11120
11121        app.handle_action(Action::PrevItem);
11122        assert_eq!(app.lambda_state.layer_selected, 0);
11123    }
11124
11125    #[test]
11126    fn test_lambda_layer_expansion() {
11127        use crate::lambda::{Function, Layer};
11128
11129        let mut app = test_app();
11130        app.current_service = Service::LambdaFunctions;
11131        app.service_selected = true;
11132        app.lambda_state.current_function = Some("test-function".to_string());
11133        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11134        app.mode = Mode::Normal;
11135
11136        app.lambda_state.table.items = vec![Function {
11137            name: "test-function".to_string(),
11138            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11139            application: None,
11140            description: "Test".to_string(),
11141            package_type: "Zip".to_string(),
11142            runtime: "python3.12".to_string(),
11143            architecture: "X86_64".to_string(),
11144            code_size: 1024,
11145            code_sha256: "hash".to_string(),
11146            memory_mb: 128,
11147            timeout_seconds: 30,
11148            last_modified: "2024-01-01".to_string(),
11149            layers: vec![Layer {
11150                arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
11151                code_size: 1024,
11152            }],
11153        }];
11154
11155        assert_eq!(app.lambda_state.layer_expanded, None);
11156
11157        app.handle_action(Action::NextPane);
11158        assert_eq!(app.lambda_state.layer_expanded, Some(0));
11159
11160        app.handle_action(Action::PrevPane);
11161        assert_eq!(app.lambda_state.layer_expanded, None);
11162
11163        app.handle_action(Action::NextPane);
11164        assert_eq!(app.lambda_state.layer_expanded, Some(0));
11165
11166        app.handle_action(Action::NextPane);
11167        assert_eq!(app.lambda_state.layer_expanded, None);
11168    }
11169
11170    #[test]
11171    fn test_lambda_layer_selection_and_expansion_workflow() {
11172        use crate::lambda::{Function, Layer};
11173
11174        let mut app = test_app();
11175        app.current_service = Service::LambdaFunctions;
11176        app.service_selected = true;
11177        app.lambda_state.current_function = Some("test-function".to_string());
11178        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11179        app.mode = Mode::Normal;
11180
11181        app.lambda_state.table.items = vec![Function {
11182            name: "test-function".to_string(),
11183            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11184            application: None,
11185            description: "Test".to_string(),
11186            package_type: "Zip".to_string(),
11187            runtime: "python3.12".to_string(),
11188            architecture: "X86_64".to_string(),
11189            code_size: 1024,
11190            code_sha256: "hash".to_string(),
11191            memory_mb: 128,
11192            timeout_seconds: 30,
11193            last_modified: "2024-01-01".to_string(),
11194            layers: vec![
11195                Layer {
11196                    arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11197                    code_size: 1024,
11198                },
11199                Layer {
11200                    arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11201                    code_size: 2048,
11202                },
11203            ],
11204        }];
11205
11206        // Start at layer 0
11207        assert_eq!(app.lambda_state.layer_selected, 0);
11208        assert_eq!(app.lambda_state.layer_expanded, None);
11209
11210        // Expand layer 0
11211        app.handle_action(Action::NextPane);
11212        assert_eq!(app.lambda_state.layer_selected, 0);
11213        assert_eq!(app.lambda_state.layer_expanded, Some(0));
11214
11215        // Navigate to layer 1 while layer 0 is expanded
11216        app.handle_action(Action::NextItem);
11217        assert_eq!(app.lambda_state.layer_selected, 1);
11218        assert_eq!(app.lambda_state.layer_expanded, Some(0)); // Still expanded
11219
11220        // Expand layer 1 (should collapse layer 0 and expand layer 1)
11221        app.handle_action(Action::NextPane);
11222        assert_eq!(app.lambda_state.layer_selected, 1);
11223        assert_eq!(app.lambda_state.layer_expanded, Some(1));
11224
11225        // Collapse layer 1
11226        app.handle_action(Action::PrevPane);
11227        assert_eq!(app.lambda_state.layer_selected, 1);
11228        assert_eq!(app.lambda_state.layer_expanded, None);
11229
11230        // Navigate back to layer 0
11231        app.handle_action(Action::PrevItem);
11232        assert_eq!(app.lambda_state.layer_selected, 0);
11233        assert_eq!(app.lambda_state.layer_expanded, None);
11234    }
11235
11236    #[test]
11237    fn test_backtab_cycles_detail_tabs_backward() {
11238        let mut app = test_app();
11239        app.mode = Mode::Normal;
11240
11241        // Test Lambda detail tabs
11242        app.current_service = Service::LambdaFunctions;
11243        app.service_selected = true;
11244        app.lambda_state.current_function = Some("test-function".to_string());
11245        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11246
11247        app.handle_action(Action::PrevDetailTab);
11248        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11249
11250        app.handle_action(Action::PrevDetailTab);
11251        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11252
11253        // Test IAM Roles detail tabs
11254        app.current_service = Service::IamRoles;
11255        app.iam_state.current_role = Some("test-role".to_string());
11256        app.iam_state.role_tab = RoleTab::Permissions;
11257
11258        app.handle_action(Action::PrevDetailTab);
11259        assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
11260
11261        // Test IAM Users detail tabs
11262        app.current_service = Service::IamUsers;
11263        app.iam_state.current_user = Some("test-user".to_string());
11264        app.iam_state.user_tab = UserTab::Permissions;
11265
11266        app.handle_action(Action::PrevDetailTab);
11267        assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
11268
11269        // Test IAM Groups detail tabs
11270        app.current_service = Service::IamUserGroups;
11271        app.iam_state.current_group = Some("test-group".to_string());
11272        app.iam_state.group_tab = GroupTab::Permissions;
11273
11274        app.handle_action(Action::PrevDetailTab);
11275        assert_eq!(app.iam_state.group_tab, GroupTab::Users);
11276
11277        // Test S3 object tabs
11278        app.current_service = Service::S3Buckets;
11279        app.s3_state.current_bucket = Some("test-bucket".to_string());
11280        app.s3_state.object_tab = S3ObjectTab::Properties;
11281
11282        app.handle_action(Action::PrevDetailTab);
11283        assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
11284
11285        // Test ECR repository tabs (Private/Public)
11286        app.current_service = Service::EcrRepositories;
11287        app.ecr_state.current_repository = None;
11288        app.ecr_state.tab = EcrTab::Private;
11289
11290        app.handle_action(Action::PrevDetailTab);
11291        assert_eq!(app.ecr_state.tab, EcrTab::Public);
11292
11293        // Test CloudFormation detail tabs
11294        app.current_service = Service::CloudFormationStacks;
11295        app.cfn_state.current_stack = Some("test-stack".to_string());
11296        app.cfn_state.detail_tab = CfnDetailTab::Resources;
11297    }
11298
11299    #[test]
11300    fn test_cloudformation_status_filter_active() {
11301        use crate::ui::cfn::StatusFilter;
11302        let filter = StatusFilter::Active;
11303        assert!(filter.matches("CREATE_IN_PROGRESS"));
11304        assert!(filter.matches("UPDATE_IN_PROGRESS"));
11305        assert!(!filter.matches("CREATE_COMPLETE"));
11306        assert!(!filter.matches("DELETE_COMPLETE"));
11307        assert!(!filter.matches("CREATE_FAILED"));
11308    }
11309
11310    #[test]
11311    fn test_cloudformation_status_filter_complete() {
11312        use crate::ui::cfn::StatusFilter;
11313        let filter = StatusFilter::Complete;
11314        assert!(filter.matches("CREATE_COMPLETE"));
11315        assert!(filter.matches("UPDATE_COMPLETE"));
11316        assert!(!filter.matches("DELETE_COMPLETE"));
11317        assert!(!filter.matches("CREATE_IN_PROGRESS"));
11318    }
11319
11320    #[test]
11321    fn test_cloudformation_status_filter_failed() {
11322        use crate::ui::cfn::StatusFilter;
11323        let filter = StatusFilter::Failed;
11324        assert!(filter.matches("CREATE_FAILED"));
11325        assert!(filter.matches("UPDATE_FAILED"));
11326        assert!(!filter.matches("CREATE_COMPLETE"));
11327    }
11328
11329    #[test]
11330    fn test_cloudformation_status_filter_deleted() {
11331        use crate::ui::cfn::StatusFilter;
11332        let filter = StatusFilter::Deleted;
11333        assert!(filter.matches("DELETE_COMPLETE"));
11334        assert!(filter.matches("DELETE_IN_PROGRESS"));
11335        assert!(!filter.matches("CREATE_COMPLETE"));
11336    }
11337
11338    #[test]
11339    fn test_cloudformation_status_filter_in_progress() {
11340        use crate::ui::cfn::StatusFilter;
11341        let filter = StatusFilter::InProgress;
11342        assert!(filter.matches("CREATE_IN_PROGRESS"));
11343        assert!(filter.matches("UPDATE_IN_PROGRESS"));
11344        assert!(filter.matches("DELETE_IN_PROGRESS"));
11345        assert!(!filter.matches("CREATE_COMPLETE"));
11346    }
11347
11348    #[test]
11349    fn test_cloudformation_status_filter_cycle() {
11350        use crate::ui::cfn::StatusFilter;
11351        let filter = StatusFilter::All;
11352        assert_eq!(filter.next(), StatusFilter::Active);
11353        assert_eq!(filter.next().next(), StatusFilter::Complete);
11354        assert_eq!(filter.next().next().next(), StatusFilter::Failed);
11355        assert_eq!(filter.next().next().next().next(), StatusFilter::Deleted);
11356        assert_eq!(
11357            filter.next().next().next().next().next(),
11358            StatusFilter::InProgress
11359        );
11360        assert_eq!(
11361            filter.next().next().next().next().next().next(),
11362            StatusFilter::All
11363        );
11364    }
11365
11366    #[test]
11367    fn test_cloudformation_default_columns() {
11368        let app = test_app();
11369        assert_eq!(app.visible_cfn_columns.len(), 4);
11370        assert!(app.visible_cfn_columns.contains(&CfnColumn::Name));
11371        assert!(app.visible_cfn_columns.contains(&CfnColumn::Status));
11372        assert!(app.visible_cfn_columns.contains(&CfnColumn::CreatedTime));
11373        assert!(app.visible_cfn_columns.contains(&CfnColumn::Description));
11374    }
11375
11376    #[test]
11377    fn test_cloudformation_all_columns() {
11378        let app = test_app();
11379        assert_eq!(app.all_cfn_columns.len(), 10);
11380    }
11381
11382    #[test]
11383    fn test_cloudformation_filter_by_name() {
11384        use crate::ui::cfn::StatusFilter;
11385        let mut app = test_app();
11386        app.cfn_state.status_filter = StatusFilter::Complete;
11387        app.cfn_state.table.items = vec![
11388            CfnStack {
11389                name: "my-stack".to_string(),
11390                stack_id: "id1".to_string(),
11391                status: "CREATE_COMPLETE".to_string(),
11392                created_time: "2024-01-01".to_string(),
11393                updated_time: String::new(),
11394                deleted_time: String::new(),
11395                drift_status: String::new(),
11396                last_drift_check_time: String::new(),
11397                status_reason: String::new(),
11398                description: String::new(),
11399                detailed_status: String::new(),
11400                root_stack: String::new(),
11401                parent_stack: String::new(),
11402                termination_protection: false,
11403                iam_role: String::new(),
11404                tags: Vec::new(),
11405                stack_policy: String::new(),
11406                rollback_monitoring_time: String::new(),
11407                rollback_alarms: Vec::new(),
11408                notification_arns: Vec::new(),
11409            },
11410            CfnStack {
11411                name: "other-stack".to_string(),
11412                stack_id: "id2".to_string(),
11413                status: "CREATE_COMPLETE".to_string(),
11414                created_time: "2024-01-02".to_string(),
11415                updated_time: String::new(),
11416                deleted_time: String::new(),
11417                drift_status: String::new(),
11418                last_drift_check_time: String::new(),
11419                status_reason: String::new(),
11420                description: String::new(),
11421                detailed_status: String::new(),
11422                root_stack: String::new(),
11423                parent_stack: String::new(),
11424                termination_protection: false,
11425                iam_role: String::new(),
11426                tags: Vec::new(),
11427                stack_policy: String::new(),
11428                rollback_monitoring_time: String::new(),
11429                rollback_alarms: Vec::new(),
11430                notification_arns: Vec::new(),
11431            },
11432        ];
11433
11434        app.cfn_state.table.filter = "my".to_string();
11435        let filtered = app.filtered_cloudformation_stacks();
11436        assert_eq!(filtered.len(), 1);
11437        assert_eq!(filtered[0].name, "my-stack");
11438    }
11439
11440    #[test]
11441    fn test_cloudformation_filter_by_description() {
11442        use crate::ui::cfn::StatusFilter;
11443        let mut app = test_app();
11444        app.cfn_state.status_filter = StatusFilter::Complete;
11445        app.cfn_state.table.items = vec![CfnStack {
11446            name: "stack1".to_string(),
11447            stack_id: "id1".to_string(),
11448            status: "CREATE_COMPLETE".to_string(),
11449            created_time: "2024-01-01".to_string(),
11450            updated_time: String::new(),
11451            deleted_time: String::new(),
11452            drift_status: String::new(),
11453            last_drift_check_time: String::new(),
11454            status_reason: String::new(),
11455            description: "production stack".to_string(),
11456            detailed_status: String::new(),
11457            root_stack: String::new(),
11458            parent_stack: String::new(),
11459            termination_protection: false,
11460            iam_role: String::new(),
11461            tags: Vec::new(),
11462            stack_policy: String::new(),
11463            rollback_monitoring_time: String::new(),
11464            rollback_alarms: Vec::new(),
11465            notification_arns: Vec::new(),
11466        }];
11467
11468        app.cfn_state.table.filter = "production".to_string();
11469        let filtered = app.filtered_cloudformation_stacks();
11470        assert_eq!(filtered.len(), 1);
11471    }
11472
11473    #[test]
11474    fn test_cloudformation_status_filter_applied() {
11475        use crate::ui::cfn::StatusFilter;
11476        let mut app = test_app();
11477        app.cfn_state.table.items = vec![
11478            CfnStack {
11479                name: "complete-stack".to_string(),
11480                stack_id: "id1".to_string(),
11481                status: "CREATE_COMPLETE".to_string(),
11482                created_time: "2024-01-01".to_string(),
11483                updated_time: String::new(),
11484                deleted_time: String::new(),
11485                drift_status: String::new(),
11486                last_drift_check_time: String::new(),
11487                status_reason: String::new(),
11488                description: String::new(),
11489                detailed_status: String::new(),
11490                root_stack: String::new(),
11491                parent_stack: String::new(),
11492                termination_protection: false,
11493                iam_role: String::new(),
11494                tags: Vec::new(),
11495                stack_policy: String::new(),
11496                rollback_monitoring_time: String::new(),
11497                rollback_alarms: Vec::new(),
11498                notification_arns: Vec::new(),
11499            },
11500            CfnStack {
11501                name: "failed-stack".to_string(),
11502                stack_id: "id2".to_string(),
11503                status: "CREATE_FAILED".to_string(),
11504                created_time: "2024-01-02".to_string(),
11505                updated_time: String::new(),
11506                deleted_time: String::new(),
11507                drift_status: String::new(),
11508                last_drift_check_time: String::new(),
11509                status_reason: String::new(),
11510                description: String::new(),
11511                detailed_status: String::new(),
11512                root_stack: String::new(),
11513                parent_stack: String::new(),
11514                termination_protection: false,
11515                iam_role: String::new(),
11516                tags: Vec::new(),
11517                stack_policy: String::new(),
11518                rollback_monitoring_time: String::new(),
11519                rollback_alarms: Vec::new(),
11520                notification_arns: Vec::new(),
11521            },
11522        ];
11523
11524        app.cfn_state.status_filter = StatusFilter::Complete;
11525        let filtered = app.filtered_cloudformation_stacks();
11526        assert_eq!(filtered.len(), 1);
11527        assert_eq!(filtered[0].name, "complete-stack");
11528
11529        app.cfn_state.status_filter = StatusFilter::Failed;
11530        let filtered = app.filtered_cloudformation_stacks();
11531        assert_eq!(filtered.len(), 1);
11532        assert_eq!(filtered[0].name, "failed-stack");
11533    }
11534
11535    #[test]
11536    fn test_cloudformation_default_page_size() {
11537        let app = test_app();
11538        assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
11539    }
11540
11541    #[test]
11542    fn test_cloudformation_default_status_filter() {
11543        use crate::ui::cfn::StatusFilter;
11544        let app = test_app();
11545        assert_eq!(app.cfn_state.status_filter, StatusFilter::All);
11546    }
11547
11548    #[test]
11549    fn test_cloudformation_view_nested_default_false() {
11550        let app = test_app();
11551        assert!(!app.cfn_state.view_nested);
11552    }
11553
11554    #[test]
11555    fn test_cloudformation_pagination_hotkeys() {
11556        use crate::ui::cfn::StatusFilter;
11557        let mut app = test_app();
11558        app.current_service = Service::CloudFormationStacks;
11559        app.service_selected = true;
11560        app.cfn_state.status_filter = StatusFilter::All;
11561
11562        // Add 150 stacks
11563        for i in 0..150 {
11564            app.cfn_state.table.items.push(CfnStack {
11565                name: format!("stack-{}", i),
11566                stack_id: format!("id-{}", i),
11567                status: "CREATE_COMPLETE".to_string(),
11568                created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
11569                updated_time: String::new(),
11570                deleted_time: String::new(),
11571                drift_status: String::new(),
11572                last_drift_check_time: String::new(),
11573                status_reason: String::new(),
11574                description: String::new(),
11575                detailed_status: String::new(),
11576                root_stack: String::new(),
11577                parent_stack: String::new(),
11578                termination_protection: false,
11579                iam_role: String::new(),
11580                tags: vec![],
11581                stack_policy: String::new(),
11582                rollback_monitoring_time: String::new(),
11583                rollback_alarms: vec![],
11584                notification_arns: vec![],
11585            });
11586        }
11587
11588        // Go to page 2
11589        app.go_to_page(2);
11590        assert_eq!(app.cfn_state.table.selected, 50);
11591
11592        // Go to page 3
11593        app.go_to_page(3);
11594        assert_eq!(app.cfn_state.table.selected, 100);
11595
11596        // Go to page 1
11597        app.go_to_page(1);
11598        assert_eq!(app.cfn_state.table.selected, 0);
11599    }
11600
11601    #[test]
11602    fn test_cloudformation_tab_cycling_in_filter_mode() {
11603        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
11604        let mut app = test_app();
11605        app.current_service = Service::CloudFormationStacks;
11606        app.service_selected = true;
11607        app.mode = Mode::FilterInput;
11608        app.cfn_state.input_focus = InputFocus::Filter;
11609
11610        // Tab to StatusFilter
11611        app.handle_action(Action::NextFilterFocus);
11612        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
11613
11614        // Tab to ViewNested
11615        app.handle_action(Action::NextFilterFocus);
11616        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
11617
11618        // Tab to Pagination
11619        app.handle_action(Action::NextFilterFocus);
11620        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
11621
11622        // Tab back to Filter
11623        app.handle_action(Action::NextFilterFocus);
11624        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
11625    }
11626
11627    #[test]
11628    fn test_cloudformation_timestamp_format_includes_utc() {
11629        let stack = CfnStack {
11630            name: "test-stack".to_string(),
11631            stack_id: "id-123".to_string(),
11632            status: "CREATE_COMPLETE".to_string(),
11633            created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
11634            updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
11635            deleted_time: String::new(),
11636            drift_status: String::new(),
11637            last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
11638            status_reason: String::new(),
11639            description: String::new(),
11640            detailed_status: String::new(),
11641            root_stack: String::new(),
11642            parent_stack: String::new(),
11643            termination_protection: false,
11644            iam_role: String::new(),
11645            tags: vec![],
11646            stack_policy: String::new(),
11647            rollback_monitoring_time: String::new(),
11648            rollback_alarms: vec![],
11649            notification_arns: vec![],
11650        };
11651
11652        assert!(stack.created_time.contains("(UTC)"));
11653        assert!(stack.updated_time.contains("(UTC)"));
11654        assert!(stack.last_drift_check_time.contains("(UTC)"));
11655        assert_eq!(stack.created_time.len(), 25);
11656    }
11657
11658    #[test]
11659    fn test_cloudformation_enter_drills_into_stack_view() {
11660        use crate::ui::cfn::StatusFilter;
11661        let mut app = test_app();
11662        app.current_service = Service::CloudFormationStacks;
11663        app.service_selected = true;
11664        app.mode = Mode::Normal;
11665        app.cfn_state.status_filter = StatusFilter::All;
11666        app.tabs = vec![Tab {
11667            service: Service::CloudFormationStacks,
11668            title: "CloudFormation > Stacks".to_string(),
11669            breadcrumb: "CloudFormation > Stacks".to_string(),
11670        }];
11671        app.current_tab = 0;
11672
11673        app.cfn_state.table.items.push(CfnStack {
11674            name: "test-stack".to_string(),
11675            stack_id: "id-123".to_string(),
11676            status: "CREATE_COMPLETE".to_string(),
11677            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
11678            updated_time: String::new(),
11679            deleted_time: String::new(),
11680            drift_status: String::new(),
11681            last_drift_check_time: String::new(),
11682            status_reason: String::new(),
11683            description: String::new(),
11684            detailed_status: String::new(),
11685            root_stack: String::new(),
11686            parent_stack: String::new(),
11687            termination_protection: false,
11688            iam_role: String::new(),
11689            tags: vec![],
11690            stack_policy: String::new(),
11691            rollback_monitoring_time: String::new(),
11692            rollback_alarms: vec![],
11693            notification_arns: vec![],
11694        });
11695
11696        app.cfn_state.table.reset();
11697        assert_eq!(app.cfn_state.current_stack, None);
11698
11699        // Press Enter - should drill into stack detail view
11700        app.handle_action(Action::Select);
11701        assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
11702    }
11703
11704    #[test]
11705    fn test_cloudformation_arrow_keys_expand_collapse() {
11706        use crate::ui::cfn::StatusFilter;
11707        let mut app = test_app();
11708        app.current_service = Service::CloudFormationStacks;
11709        app.service_selected = true;
11710        app.mode = Mode::Normal;
11711        app.cfn_state.status_filter = StatusFilter::All;
11712
11713        app.cfn_state.table.items.push(CfnStack {
11714            name: "test-stack".to_string(),
11715            stack_id: "id-123".to_string(),
11716            status: "CREATE_COMPLETE".to_string(),
11717            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
11718            updated_time: String::new(),
11719            deleted_time: String::new(),
11720            drift_status: String::new(),
11721            last_drift_check_time: String::new(),
11722            status_reason: String::new(),
11723            description: String::new(),
11724            detailed_status: String::new(),
11725            root_stack: String::new(),
11726            parent_stack: String::new(),
11727            termination_protection: false,
11728            iam_role: String::new(),
11729            tags: vec![],
11730            stack_policy: String::new(),
11731            rollback_monitoring_time: String::new(),
11732            rollback_alarms: vec![],
11733            notification_arns: vec![],
11734        });
11735
11736        app.cfn_state.table.reset();
11737        assert_eq!(app.cfn_state.table.expanded_item, None);
11738
11739        // Right arrow - should expand
11740        app.handle_action(Action::NextPane);
11741        assert_eq!(app.cfn_state.table.expanded_item, Some(0));
11742
11743        // Left arrow - should collapse
11744        app.handle_action(Action::PrevPane);
11745        assert_eq!(app.cfn_state.table.expanded_item, None);
11746
11747        // Verify current_stack is still None (not drilled in)
11748        assert_eq!(app.cfn_state.current_stack, None);
11749    }
11750
11751    #[test]
11752    fn test_cloudformation_tab_cycling() {
11753        use crate::ui::cfn::{DetailTab, StatusFilter};
11754        let mut app = test_app();
11755        app.current_service = Service::CloudFormationStacks;
11756        app.service_selected = true;
11757        app.mode = Mode::Normal;
11758        app.cfn_state.status_filter = StatusFilter::All;
11759        app.cfn_state.current_stack = Some("test-stack".to_string());
11760
11761        assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
11762    }
11763
11764    #[test]
11765    fn test_cloudformation_console_url() {
11766        use crate::ui::cfn::{DetailTab, StatusFilter};
11767        let mut app = test_app();
11768        app.current_service = Service::CloudFormationStacks;
11769        app.service_selected = true;
11770        app.cfn_state.status_filter = StatusFilter::All;
11771
11772        app.cfn_state.table.items.push(CfnStack {
11773            name: "test-stack".to_string(),
11774            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11775                .to_string(),
11776            status: "CREATE_COMPLETE".to_string(),
11777            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
11778            updated_time: String::new(),
11779            deleted_time: String::new(),
11780            drift_status: String::new(),
11781            last_drift_check_time: String::new(),
11782            status_reason: String::new(),
11783            description: String::new(),
11784            detailed_status: String::new(),
11785            root_stack: String::new(),
11786            parent_stack: String::new(),
11787            termination_protection: false,
11788            iam_role: String::new(),
11789            tags: vec![],
11790            stack_policy: String::new(),
11791            rollback_monitoring_time: String::new(),
11792            rollback_alarms: vec![],
11793            notification_arns: vec![],
11794        });
11795
11796        app.cfn_state.current_stack = Some("test-stack".to_string());
11797
11798        // Stack info URL
11799        app.cfn_state.detail_tab = DetailTab::StackInfo;
11800        let url = app.get_console_url();
11801        assert!(url.contains("stackinfo"));
11802        assert!(url.contains("arn%3Aaws%3Acloudformation"));
11803
11804        // Events URL
11805        app.cfn_state.detail_tab = DetailTab::Events;
11806        let url = app.get_console_url();
11807        assert!(url.contains("events"));
11808        assert!(url.contains("arn%3Aaws%3Acloudformation"));
11809    }
11810
11811    #[test]
11812    fn test_iam_role_select() {
11813        let mut app = test_app();
11814        app.current_service = Service::IamRoles;
11815        app.service_selected = true;
11816        app.mode = Mode::Normal;
11817
11818        app.iam_state.roles.items = vec![
11819            crate::iam::IamRole {
11820                role_name: "role1".to_string(),
11821                path: "/".to_string(),
11822                trusted_entities: "AWS Service: ec2".to_string(),
11823                last_activity: "-".to_string(),
11824                arn: "arn:aws:iam::123456789012:role/role1".to_string(),
11825                creation_time: "2025-01-01".to_string(),
11826                description: "Test role 1".to_string(),
11827                max_session_duration: "3600 seconds".to_string(),
11828            },
11829            crate::iam::IamRole {
11830                role_name: "role2".to_string(),
11831                path: "/".to_string(),
11832                trusted_entities: "AWS Service: lambda".to_string(),
11833                last_activity: "-".to_string(),
11834                arn: "arn:aws:iam::123456789012:role/role2".to_string(),
11835                creation_time: "2025-01-02".to_string(),
11836                description: "Test role 2".to_string(),
11837                max_session_duration: "7200 seconds".to_string(),
11838            },
11839        ];
11840
11841        // Select first role
11842        app.iam_state.roles.selected = 0;
11843        app.handle_action(Action::Select);
11844
11845        assert_eq!(
11846            app.iam_state.current_role,
11847            Some("role1".to_string()),
11848            "Should open role detail view"
11849        );
11850        assert_eq!(
11851            app.iam_state.role_tab,
11852            RoleTab::Permissions,
11853            "Should default to Permissions tab"
11854        );
11855    }
11856
11857    #[test]
11858    fn test_iam_role_back_navigation() {
11859        let mut app = test_app();
11860        app.current_service = Service::IamRoles;
11861        app.service_selected = true;
11862        app.iam_state.current_role = Some("test-role".to_string());
11863
11864        app.handle_action(Action::GoBack);
11865
11866        assert_eq!(
11867            app.iam_state.current_role, None,
11868            "Should return to roles list"
11869        );
11870    }
11871
11872    #[test]
11873    fn test_iam_role_tab_navigation() {
11874        let mut app = test_app();
11875        app.current_service = Service::IamRoles;
11876        app.service_selected = true;
11877        app.iam_state.current_role = Some("test-role".to_string());
11878        app.iam_state.role_tab = RoleTab::Permissions;
11879
11880        app.handle_action(Action::NextDetailTab);
11881
11882        assert_eq!(
11883            app.iam_state.role_tab,
11884            RoleTab::TrustRelationships,
11885            "Should move to next tab"
11886        );
11887    }
11888
11889    #[test]
11890    fn test_iam_role_tab_cycle_order() {
11891        let mut app = test_app();
11892        app.current_service = Service::IamRoles;
11893        app.service_selected = true;
11894        app.iam_state.current_role = Some("test-role".to_string());
11895        app.iam_state.role_tab = RoleTab::Permissions;
11896
11897        app.handle_action(Action::NextDetailTab);
11898        assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
11899
11900        app.handle_action(Action::NextDetailTab);
11901        assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
11902
11903        app.handle_action(Action::NextDetailTab);
11904        assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
11905
11906        app.handle_action(Action::NextDetailTab);
11907        assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
11908
11909        app.handle_action(Action::NextDetailTab);
11910        assert_eq!(
11911            app.iam_state.role_tab,
11912            RoleTab::Permissions,
11913            "Should cycle back to first tab"
11914        );
11915    }
11916
11917    #[test]
11918    fn test_iam_role_pagination() {
11919        let mut app = test_app();
11920        app.current_service = Service::IamRoles;
11921        app.service_selected = true;
11922        app.iam_state.roles.page_size = crate::common::PageSize::Ten;
11923
11924        app.iam_state.roles.items = (0..25)
11925            .map(|i| crate::iam::IamRole {
11926                role_name: format!("role{}", i),
11927                path: "/".to_string(),
11928                trusted_entities: "AWS Service: ec2".to_string(),
11929                last_activity: "-".to_string(),
11930                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
11931                creation_time: "2025-01-01".to_string(),
11932                description: format!("Test role {}", i),
11933                max_session_duration: "3600 seconds".to_string(),
11934            })
11935            .collect();
11936
11937        // Jump to page 2
11938        app.go_to_page(2);
11939
11940        assert_eq!(
11941            app.iam_state.roles.selected, 10,
11942            "Should select first item of page 2"
11943        );
11944        assert_eq!(
11945            app.iam_state.roles.scroll_offset, 10,
11946            "Should update scroll offset"
11947        );
11948    }
11949
11950    #[test]
11951    fn test_tags_table_populated_on_role_detail() {
11952        let mut app = test_app();
11953        app.current_service = Service::IamRoles;
11954        app.service_selected = true;
11955        app.mode = Mode::Normal;
11956        app.iam_state.roles.items = vec![crate::iam::IamRole {
11957            role_name: "TestRole".to_string(),
11958            path: "/".to_string(),
11959            trusted_entities: String::new(),
11960            last_activity: String::new(),
11961            arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
11962            creation_time: "2025-01-01".to_string(),
11963            description: String::new(),
11964            max_session_duration: "3600 seconds".to_string(),
11965        }];
11966
11967        // Manually populate tags to test table rendering
11968        app.iam_state.tags.items = vec![
11969            crate::iam::RoleTag {
11970                key: "Environment".to_string(),
11971                value: "Production".to_string(),
11972            },
11973            crate::iam::RoleTag {
11974                key: "Team".to_string(),
11975                value: "Platform".to_string(),
11976            },
11977        ];
11978
11979        assert_eq!(app.iam_state.tags.items.len(), 2);
11980        assert_eq!(app.iam_state.tags.items[0].key, "Environment");
11981        assert_eq!(app.iam_state.tags.items[0].value, "Production");
11982        assert_eq!(app.iam_state.tags.selected, 0);
11983    }
11984
11985    #[test]
11986    fn test_tags_table_navigation() {
11987        let mut app = test_app();
11988        app.current_service = Service::IamRoles;
11989        app.service_selected = true;
11990        app.mode = Mode::Normal;
11991        app.iam_state.current_role = Some("TestRole".to_string());
11992        app.iam_state.role_tab = RoleTab::Tags;
11993        app.iam_state.tags.items = vec![
11994            crate::iam::RoleTag {
11995                key: "Tag1".to_string(),
11996                value: "Value1".to_string(),
11997            },
11998            crate::iam::RoleTag {
11999                key: "Tag2".to_string(),
12000                value: "Value2".to_string(),
12001            },
12002        ];
12003
12004        app.handle_action(Action::NextItem);
12005        assert_eq!(app.iam_state.tags.selected, 1);
12006
12007        app.handle_action(Action::PrevItem);
12008        assert_eq!(app.iam_state.tags.selected, 0);
12009    }
12010
12011    #[test]
12012    fn test_last_accessed_table_navigation() {
12013        let mut app = test_app();
12014        app.current_service = Service::IamRoles;
12015        app.service_selected = true;
12016        app.mode = Mode::Normal;
12017        app.iam_state.current_role = Some("TestRole".to_string());
12018        app.iam_state.role_tab = RoleTab::LastAccessed;
12019        app.iam_state.last_accessed_services.items = vec![
12020            crate::iam::LastAccessedService {
12021                service: "S3".to_string(),
12022                policies_granting: "Policy1".to_string(),
12023                last_accessed: "2025-01-01".to_string(),
12024            },
12025            crate::iam::LastAccessedService {
12026                service: "EC2".to_string(),
12027                policies_granting: "Policy2".to_string(),
12028                last_accessed: "2025-01-02".to_string(),
12029            },
12030        ];
12031
12032        app.handle_action(Action::NextItem);
12033        assert_eq!(app.iam_state.last_accessed_services.selected, 1);
12034
12035        app.handle_action(Action::PrevItem);
12036        assert_eq!(app.iam_state.last_accessed_services.selected, 0);
12037    }
12038
12039    #[test]
12040    fn test_cfn_input_focus_next() {
12041        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12042        let mut app = test_app();
12043        app.current_service = Service::CloudFormationStacks;
12044        app.mode = Mode::FilterInput;
12045        app.cfn_state.input_focus = InputFocus::Filter;
12046
12047        app.handle_action(Action::NextFilterFocus);
12048        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12049
12050        app.handle_action(Action::NextFilterFocus);
12051        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12052
12053        app.handle_action(Action::NextFilterFocus);
12054        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12055
12056        app.handle_action(Action::NextFilterFocus);
12057        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12058    }
12059
12060    #[test]
12061    fn test_cfn_input_focus_prev() {
12062        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12063        let mut app = test_app();
12064        app.current_service = Service::CloudFormationStacks;
12065        app.mode = Mode::FilterInput;
12066        app.cfn_state.input_focus = InputFocus::Filter;
12067
12068        app.handle_action(Action::PrevFilterFocus);
12069        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12070
12071        app.handle_action(Action::PrevFilterFocus);
12072        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12073
12074        app.handle_action(Action::PrevFilterFocus);
12075        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12076
12077        app.handle_action(Action::PrevFilterFocus);
12078        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12079    }
12080
12081    #[test]
12082    fn test_cw_logs_input_focus_prev() {
12083        let mut app = test_app();
12084        app.current_service = Service::CloudWatchLogGroups;
12085        app.mode = Mode::FilterInput;
12086        app.view_mode = ViewMode::Detail;
12087        app.log_groups_state.detail_tab = crate::ui::cw::logs::DetailTab::LogStreams;
12088        app.log_groups_state.input_focus = InputFocus::Filter;
12089
12090        app.handle_action(Action::PrevFilterFocus);
12091        assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
12092
12093        app.handle_action(Action::PrevFilterFocus);
12094        assert_eq!(
12095            app.log_groups_state.input_focus,
12096            InputFocus::Checkbox("ShowExpired")
12097        );
12098
12099        app.handle_action(Action::PrevFilterFocus);
12100        assert_eq!(
12101            app.log_groups_state.input_focus,
12102            InputFocus::Checkbox("ExactMatch")
12103        );
12104
12105        app.handle_action(Action::PrevFilterFocus);
12106        assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
12107    }
12108
12109    #[test]
12110    fn test_cw_events_input_focus_prev() {
12111        use crate::ui::cw::logs::EventFilterFocus;
12112        let mut app = test_app();
12113        app.mode = Mode::EventFilterInput;
12114        app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
12115
12116        app.handle_action(Action::PrevFilterFocus);
12117        assert_eq!(
12118            app.log_groups_state.event_input_focus,
12119            EventFilterFocus::DateRange
12120        );
12121
12122        app.handle_action(Action::PrevFilterFocus);
12123        assert_eq!(
12124            app.log_groups_state.event_input_focus,
12125            EventFilterFocus::Filter
12126        );
12127    }
12128
12129    #[test]
12130    fn test_cfn_input_focus_cycle_complete() {
12131        let mut app = test_app();
12132        app.current_service = Service::CloudFormationStacks;
12133        app.mode = Mode::FilterInput;
12134        app.cfn_state.input_focus = InputFocus::Filter;
12135
12136        // Cycle forward through all controls
12137        for _ in 0..4 {
12138            app.handle_action(Action::NextFilterFocus);
12139        }
12140        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12141
12142        // Cycle backward through all controls
12143        for _ in 0..4 {
12144            app.handle_action(Action::PrevFilterFocus);
12145        }
12146        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12147    }
12148
12149    #[test]
12150    fn test_cfn_filter_status_arrow_keys() {
12151        use crate::ui::cfn::{StatusFilter, STATUS_FILTER};
12152        let mut app = test_app();
12153        app.current_service = Service::CloudFormationStacks;
12154        app.mode = Mode::FilterInput;
12155        app.cfn_state.input_focus = STATUS_FILTER;
12156        app.cfn_state.status_filter = StatusFilter::All;
12157
12158        app.handle_action(Action::NextItem);
12159        assert_eq!(app.cfn_state.status_filter, StatusFilter::Active);
12160
12161        app.handle_action(Action::PrevItem);
12162        assert_eq!(app.cfn_state.status_filter, StatusFilter::All);
12163    }
12164
12165    #[test]
12166    fn test_cfn_filter_shift_tab_cycles_backward() {
12167        use crate::ui::cfn::STATUS_FILTER;
12168        let mut app = test_app();
12169        app.current_service = Service::CloudFormationStacks;
12170        app.mode = Mode::FilterInput;
12171        app.cfn_state.input_focus = STATUS_FILTER;
12172
12173        // Shift+Tab should go backward
12174        app.handle_action(Action::PrevFilterFocus);
12175        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12176
12177        // From Input, Shift+Tab should wrap to Pagination
12178        app.handle_action(Action::PrevFilterFocus);
12179        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12180    }
12181
12182    #[test]
12183    fn test_cfn_pagination_arrow_keys() {
12184        let mut app = test_app();
12185        app.current_service = Service::CloudFormationStacks;
12186        app.mode = Mode::FilterInput;
12187        app.cfn_state.input_focus = InputFocus::Pagination;
12188        app.cfn_state.table.scroll_offset = 0;
12189        app.cfn_state.table.page_size = crate::common::PageSize::Ten;
12190
12191        // Add some test stacks
12192        app.cfn_state.table.items = (0..30)
12193            .map(|i| crate::cfn::Stack {
12194                name: format!("stack-{}", i),
12195                stack_id: format!("id-{}", i),
12196                status: "CREATE_COMPLETE".to_string(),
12197                created_time: "2024-01-01".to_string(),
12198                updated_time: String::new(),
12199                deleted_time: String::new(),
12200                drift_status: String::new(),
12201                last_drift_check_time: String::new(),
12202                status_reason: String::new(),
12203                description: String::new(),
12204                detailed_status: String::new(),
12205                root_stack: String::new(),
12206                parent_stack: String::new(),
12207                termination_protection: false,
12208                iam_role: String::new(),
12209                tags: Vec::new(),
12210                stack_policy: String::new(),
12211                rollback_monitoring_time: String::new(),
12212                rollback_alarms: Vec::new(),
12213                notification_arns: Vec::new(),
12214            })
12215            .collect();
12216
12217        // Right arrow should page forward
12218        app.handle_action(Action::PageDown);
12219        assert_eq!(app.cfn_state.table.scroll_offset, 10);
12220        // Verify page number calculation
12221        let page_size = app.cfn_state.table.page_size.value();
12222        let current_page = app.cfn_state.table.scroll_offset / page_size;
12223        assert_eq!(current_page, 1);
12224
12225        // Left arrow should page backward
12226        app.handle_action(Action::PageUp);
12227        assert_eq!(app.cfn_state.table.scroll_offset, 0);
12228        let current_page = app.cfn_state.table.scroll_offset / page_size;
12229        assert_eq!(current_page, 0);
12230    }
12231
12232    #[test]
12233    fn test_cfn_page_navigation_updates_selection() {
12234        let mut app = test_app();
12235        app.current_service = Service::CloudFormationStacks;
12236        app.mode = Mode::Normal;
12237
12238        // Add 30 test stacks
12239        app.cfn_state.table.items = (0..30)
12240            .map(|i| crate::cfn::Stack {
12241                name: format!("stack-{}", i),
12242                stack_id: format!("id-{}", i),
12243                status: "CREATE_COMPLETE".to_string(),
12244                created_time: "2024-01-01".to_string(),
12245                updated_time: String::new(),
12246                deleted_time: String::new(),
12247                drift_status: String::new(),
12248                last_drift_check_time: String::new(),
12249                status_reason: String::new(),
12250                description: String::new(),
12251                detailed_status: String::new(),
12252                root_stack: String::new(),
12253                parent_stack: String::new(),
12254                termination_protection: false,
12255                iam_role: String::new(),
12256                tags: Vec::new(),
12257                stack_policy: String::new(),
12258                rollback_monitoring_time: String::new(),
12259                rollback_alarms: Vec::new(),
12260                notification_arns: Vec::new(),
12261            })
12262            .collect();
12263
12264        app.cfn_state.table.reset();
12265        app.cfn_state.table.scroll_offset = 0;
12266
12267        // Page down should update selection
12268        app.handle_action(Action::PageDown);
12269        assert_eq!(app.cfn_state.table.selected, 10);
12270
12271        // Page down again
12272        app.handle_action(Action::PageDown);
12273        assert_eq!(app.cfn_state.table.selected, 20);
12274
12275        // Page up should update selection
12276        app.handle_action(Action::PageUp);
12277        assert_eq!(app.cfn_state.table.selected, 10);
12278    }
12279
12280    #[test]
12281    fn test_cfn_filter_input_only_when_focused() {
12282        use crate::ui::cfn::STATUS_FILTER;
12283        let mut app = test_app();
12284        app.current_service = Service::CloudFormationStacks;
12285        app.mode = Mode::FilterInput;
12286        app.cfn_state.input_focus = STATUS_FILTER;
12287        app.cfn_state.table.filter = String::new();
12288
12289        // Typing should not add to filter when focus is not on Input
12290        app.handle_action(Action::FilterInput('t'));
12291        app.handle_action(Action::FilterInput('e'));
12292        app.handle_action(Action::FilterInput('s'));
12293        app.handle_action(Action::FilterInput('t'));
12294        assert_eq!(app.cfn_state.table.filter, "");
12295
12296        // Switch to Input focus
12297        app.cfn_state.input_focus = InputFocus::Filter;
12298        app.handle_action(Action::FilterInput('t'));
12299        app.handle_action(Action::FilterInput('e'));
12300        app.handle_action(Action::FilterInput('s'));
12301        app.handle_action(Action::FilterInput('t'));
12302        assert_eq!(app.cfn_state.table.filter, "test");
12303    }
12304
12305    #[test]
12306    fn test_cfn_input_focus_resets_on_start() {
12307        let mut app = test_app();
12308        app.current_service = Service::CloudFormationStacks;
12309        app.service_selected = true;
12310        app.mode = Mode::Normal;
12311        app.cfn_state.input_focus = InputFocus::Pagination;
12312
12313        // Start filter should reset focus to Input
12314        app.handle_action(Action::StartFilter);
12315        assert_eq!(app.mode, Mode::FilterInput);
12316        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12317    }
12318
12319    #[test]
12320    fn test_iam_roles_input_focus_cycles_forward() {
12321        let mut app = test_app();
12322        app.current_service = Service::IamRoles;
12323        app.mode = Mode::FilterInput;
12324        app.iam_state.role_input_focus = InputFocus::Filter;
12325
12326        app.handle_action(Action::NextFilterFocus);
12327        assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12328
12329        app.handle_action(Action::NextFilterFocus);
12330        assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12331    }
12332
12333    #[test]
12334    fn test_iam_roles_input_focus_cycles_backward() {
12335        let mut app = test_app();
12336        app.current_service = Service::IamRoles;
12337        app.mode = Mode::FilterInput;
12338        app.iam_state.role_input_focus = InputFocus::Filter;
12339
12340        app.handle_action(Action::PrevFilterFocus);
12341        assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12342
12343        app.handle_action(Action::PrevFilterFocus);
12344        assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12345    }
12346
12347    #[test]
12348    fn test_iam_roles_filter_input_only_when_focused() {
12349        let mut app = test_app();
12350        app.current_service = Service::IamRoles;
12351        app.mode = Mode::FilterInput;
12352        app.iam_state.role_input_focus = InputFocus::Pagination;
12353        app.iam_state.roles.filter = String::new();
12354
12355        // Typing should not add to filter when focus is on Pagination
12356        app.handle_action(Action::FilterInput('t'));
12357        app.handle_action(Action::FilterInput('e'));
12358        app.handle_action(Action::FilterInput('s'));
12359        app.handle_action(Action::FilterInput('t'));
12360        assert_eq!(app.iam_state.roles.filter, "");
12361
12362        // Switch to Input focus
12363        app.iam_state.role_input_focus = InputFocus::Filter;
12364        app.handle_action(Action::FilterInput('t'));
12365        app.handle_action(Action::FilterInput('e'));
12366        app.handle_action(Action::FilterInput('s'));
12367        app.handle_action(Action::FilterInput('t'));
12368        assert_eq!(app.iam_state.roles.filter, "test");
12369    }
12370
12371    #[test]
12372    fn test_iam_roles_page_down_updates_scroll_offset() {
12373        let mut app = test_app();
12374        app.current_service = Service::IamRoles;
12375        app.mode = Mode::Normal;
12376        app.iam_state.roles.items = (0..50)
12377            .map(|i| crate::iam::IamRole {
12378                role_name: format!("role-{}", i),
12379                path: "/".to_string(),
12380                trusted_entities: "AWS Service".to_string(),
12381                last_activity: "N/A".to_string(),
12382                arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
12383                creation_time: "2024-01-01".to_string(),
12384                description: String::new(),
12385                max_session_duration: "1 hour".to_string(),
12386            })
12387            .collect();
12388
12389        app.iam_state.roles.selected = 0;
12390        app.iam_state.roles.scroll_offset = 0;
12391
12392        // Page down should update both selected and scroll_offset
12393        app.handle_action(Action::PageDown);
12394        assert_eq!(app.iam_state.roles.selected, 10);
12395        // scroll_offset should be updated to keep selection visible
12396        assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
12397
12398        // Page down again
12399        app.handle_action(Action::PageDown);
12400        assert_eq!(app.iam_state.roles.selected, 20);
12401        assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
12402    }
12403
12404    #[test]
12405    fn test_application_selection_and_deployments_tab() {
12406        use crate::lambda::Application as LambdaApplication;
12407        use crate::ui::lambda::ApplicationDetailTab;
12408
12409        let mut app = test_app();
12410        app.current_service = Service::LambdaApplications;
12411        app.service_selected = true;
12412        app.mode = Mode::Normal;
12413
12414        app.lambda_application_state.table.items = vec![LambdaApplication {
12415            name: "test-app".to_string(),
12416            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12417            description: "Test application".to_string(),
12418            status: "CREATE_COMPLETE".to_string(),
12419            last_modified: "2024-01-01".to_string(),
12420        }];
12421
12422        // Select application
12423        app.handle_action(Action::Select);
12424        assert_eq!(
12425            app.lambda_application_state.current_application,
12426            Some("test-app".to_string())
12427        );
12428        assert_eq!(
12429            app.lambda_application_state.detail_tab,
12430            ApplicationDetailTab::Overview
12431        );
12432
12433        // Switch to Deployments tab
12434        app.handle_action(Action::NextDetailTab);
12435        assert_eq!(
12436            app.lambda_application_state.detail_tab,
12437            ApplicationDetailTab::Deployments
12438        );
12439
12440        // Go back
12441        app.handle_action(Action::GoBack);
12442        assert_eq!(app.lambda_application_state.current_application, None);
12443    }
12444
12445    #[test]
12446    fn test_application_resources_filter_and_pagination() {
12447        use crate::lambda::Application as LambdaApplication;
12448        use crate::ui::lambda::ApplicationDetailTab;
12449
12450        let mut app = test_app();
12451        app.current_service = Service::LambdaApplications;
12452        app.service_selected = true;
12453        app.mode = Mode::Normal;
12454
12455        app.lambda_application_state.table.items = vec![LambdaApplication {
12456            name: "test-app".to_string(),
12457            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12458            description: "Test application".to_string(),
12459            status: "CREATE_COMPLETE".to_string(),
12460            last_modified: "2024-01-01".to_string(),
12461        }];
12462
12463        // Select application
12464        app.handle_action(Action::Select);
12465        assert_eq!(
12466            app.lambda_application_state.detail_tab,
12467            ApplicationDetailTab::Overview
12468        );
12469
12470        // Verify resources were loaded
12471        assert!(!app.lambda_application_state.resources.items.is_empty());
12472
12473        // Test filter focus cycling
12474        app.mode = Mode::FilterInput;
12475        assert_eq!(
12476            app.lambda_application_state.resource_input_focus,
12477            InputFocus::Filter
12478        );
12479
12480        app.handle_action(Action::NextFilterFocus);
12481        assert_eq!(
12482            app.lambda_application_state.resource_input_focus,
12483            InputFocus::Pagination
12484        );
12485
12486        app.handle_action(Action::PrevFilterFocus);
12487        assert_eq!(
12488            app.lambda_application_state.resource_input_focus,
12489            InputFocus::Filter
12490        );
12491    }
12492
12493    #[test]
12494    fn test_application_deployments_filter_and_pagination() {
12495        use crate::lambda::Application as LambdaApplication;
12496        use crate::ui::lambda::ApplicationDetailTab;
12497
12498        let mut app = test_app();
12499        app.current_service = Service::LambdaApplications;
12500        app.service_selected = true;
12501        app.mode = Mode::Normal;
12502
12503        app.lambda_application_state.table.items = vec![LambdaApplication {
12504            name: "test-app".to_string(),
12505            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12506            description: "Test application".to_string(),
12507            status: "CREATE_COMPLETE".to_string(),
12508            last_modified: "2024-01-01".to_string(),
12509        }];
12510
12511        // Select application and switch to Deployments tab
12512        app.handle_action(Action::Select);
12513        app.handle_action(Action::NextDetailTab);
12514        assert_eq!(
12515            app.lambda_application_state.detail_tab,
12516            ApplicationDetailTab::Deployments
12517        );
12518
12519        // Verify deployments were loaded
12520        assert!(!app.lambda_application_state.deployments.items.is_empty());
12521
12522        // Test filter focus cycling
12523        app.mode = Mode::FilterInput;
12524        assert_eq!(
12525            app.lambda_application_state.deployment_input_focus,
12526            InputFocus::Filter
12527        );
12528
12529        app.handle_action(Action::NextFilterFocus);
12530        assert_eq!(
12531            app.lambda_application_state.deployment_input_focus,
12532            InputFocus::Pagination
12533        );
12534
12535        app.handle_action(Action::PrevFilterFocus);
12536        assert_eq!(
12537            app.lambda_application_state.deployment_input_focus,
12538            InputFocus::Filter
12539        );
12540    }
12541
12542    #[test]
12543    fn test_application_resource_expansion() {
12544        use crate::lambda::Application as LambdaApplication;
12545        use crate::ui::lambda::ApplicationDetailTab;
12546
12547        let mut app = test_app();
12548        app.current_service = Service::LambdaApplications;
12549        app.service_selected = true;
12550        app.mode = Mode::Normal;
12551
12552        app.lambda_application_state.table.items = vec![LambdaApplication {
12553            name: "test-app".to_string(),
12554            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12555            description: "Test application".to_string(),
12556            status: "CREATE_COMPLETE".to_string(),
12557            last_modified: "2024-01-01".to_string(),
12558        }];
12559
12560        // Select application (Overview tab by default)
12561        app.handle_action(Action::Select);
12562        assert_eq!(
12563            app.lambda_application_state.detail_tab,
12564            ApplicationDetailTab::Overview
12565        );
12566
12567        // Expand resource
12568        app.handle_action(Action::NextPane);
12569        assert_eq!(
12570            app.lambda_application_state.resources.expanded_item,
12571            Some(0)
12572        );
12573
12574        // Collapse resource
12575        app.handle_action(Action::PrevPane);
12576        assert_eq!(app.lambda_application_state.resources.expanded_item, None);
12577    }
12578
12579    #[test]
12580    fn test_application_deployment_expansion() {
12581        use crate::lambda::Application as LambdaApplication;
12582        use crate::ui::lambda::ApplicationDetailTab;
12583
12584        let mut app = test_app();
12585        app.current_service = Service::LambdaApplications;
12586        app.service_selected = true;
12587        app.mode = Mode::Normal;
12588
12589        app.lambda_application_state.table.items = vec![LambdaApplication {
12590            name: "test-app".to_string(),
12591            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12592            description: "Test application".to_string(),
12593            status: "CREATE_COMPLETE".to_string(),
12594            last_modified: "2024-01-01".to_string(),
12595        }];
12596
12597        // Select application and switch to Deployments tab
12598        app.handle_action(Action::Select);
12599        app.handle_action(Action::NextDetailTab);
12600        assert_eq!(
12601            app.lambda_application_state.detail_tab,
12602            ApplicationDetailTab::Deployments
12603        );
12604
12605        // Expand deployment
12606        app.handle_action(Action::NextPane);
12607        assert_eq!(
12608            app.lambda_application_state.deployments.expanded_item,
12609            Some(0)
12610        );
12611
12612        // Collapse deployment
12613        app.handle_action(Action::PrevPane);
12614        assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
12615    }
12616
12617    #[test]
12618    fn test_s3_nested_prefix_expansion() {
12619        use crate::s3::Bucket;
12620        use crate::s3::Object as S3Object;
12621
12622        let mut app = test_app();
12623        app.current_service = Service::S3Buckets;
12624        app.service_selected = true;
12625        app.mode = Mode::Normal;
12626
12627        // Setup bucket with nested prefixes (2 levels)
12628        app.s3_state.buckets.items = vec![Bucket {
12629            name: "test-bucket".to_string(),
12630            region: "us-east-1".to_string(),
12631            creation_date: "2024-01-01".to_string(),
12632        }];
12633
12634        // Level 1: bucket preview
12635        app.s3_state.bucket_preview.insert(
12636            "test-bucket".to_string(),
12637            vec![S3Object {
12638                key: "level1/".to_string(),
12639                size: 0,
12640                last_modified: "".to_string(),
12641                is_prefix: true,
12642                storage_class: "".to_string(),
12643            }],
12644        );
12645
12646        // Level 2: nested prefix
12647        app.s3_state.prefix_preview.insert(
12648            "level1/".to_string(),
12649            vec![S3Object {
12650                key: "level1/level2/".to_string(),
12651                size: 0,
12652                last_modified: "".to_string(),
12653                is_prefix: true,
12654                storage_class: "".to_string(),
12655            }],
12656        );
12657
12658        // Expand bucket (row 0)
12659        app.s3_state.selected_row = 0;
12660        app.handle_action(Action::NextPane);
12661        assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
12662
12663        // Expand level1/ (row 1)
12664        app.s3_state.selected_row = 1;
12665        app.handle_action(Action::NextPane);
12666        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
12667
12668        // Expand level2/ (row 2) - verifies nested expansion works
12669        app.s3_state.selected_row = 2;
12670        app.handle_action(Action::NextPane);
12671        assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
12672
12673        // Verify all are still expanded
12674        assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
12675        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
12676    }
12677
12678    #[test]
12679    fn test_s3_nested_prefix_collapse() {
12680        use crate::s3::Bucket;
12681        use crate::s3::Object as S3Object;
12682
12683        let mut app = test_app();
12684        app.current_service = Service::S3Buckets;
12685        app.service_selected = true;
12686        app.mode = Mode::Normal;
12687
12688        app.s3_state.buckets.items = vec![Bucket {
12689            name: "test-bucket".to_string(),
12690            region: "us-east-1".to_string(),
12691            creation_date: "2024-01-01".to_string(),
12692        }];
12693
12694        app.s3_state.bucket_preview.insert(
12695            "test-bucket".to_string(),
12696            vec![S3Object {
12697                key: "level1/".to_string(),
12698                size: 0,
12699                last_modified: "".to_string(),
12700                is_prefix: true,
12701                storage_class: "".to_string(),
12702            }],
12703        );
12704
12705        app.s3_state.prefix_preview.insert(
12706            "level1/".to_string(),
12707            vec![S3Object {
12708                key: "level1/level2/".to_string(),
12709                size: 0,
12710                last_modified: "".to_string(),
12711                is_prefix: true,
12712                storage_class: "".to_string(),
12713            }],
12714        );
12715
12716        // Pre-expand all levels
12717        app.s3_state
12718            .expanded_prefixes
12719            .insert("test-bucket".to_string());
12720        app.s3_state.expanded_prefixes.insert("level1/".to_string());
12721        app.s3_state
12722            .expanded_prefixes
12723            .insert("level1/level2/".to_string());
12724
12725        // Collapse level2/ (row 2)
12726        app.s3_state.selected_row = 2;
12727        app.handle_action(Action::PrevPane);
12728        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
12729        assert!(app.s3_state.expanded_prefixes.contains("level1/")); // Parent still expanded
12730
12731        // Collapse level1/ (row 1)
12732        app.s3_state.selected_row = 1;
12733        app.handle_action(Action::PrevPane);
12734        assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
12735        assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); // Bucket still expanded
12736
12737        // Collapse bucket (row 0)
12738        app.s3_state.selected_row = 0;
12739        app.handle_action(Action::PrevPane);
12740        assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
12741    }
12742}