Skip to main content

rusticity_term/
app.rs

1use crate::apig::api::Column as ApigColumn;
2use crate::apig::resource::Column as ResourceColumn;
3use crate::apig::resource::Resource as ApigResource;
4use crate::apig::route::Column as RouteColumn;
5use crate::apig::route::Route;
6pub use crate::aws::{filter_profiles, Profile as AwsProfile, Region as AwsRegion};
7use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
8use crate::cloudtrail::{CloudTrailEvent, CloudTrailEventColumn, EventResourceColumn};
9use crate::common::{ColumnId, CyclicEnum, InputFocus, PageSize, SortDirection};
10pub use crate::cw::insights::InsightsFocus;
11use crate::cw::insights::InsightsState;
12pub use crate::cw::{Alarm, AlarmColumn};
13pub use crate::ec2::{Column as Ec2Column, Instance as Ec2Instance};
14use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
15use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
16use crate::iam::{
17    self, GroupUser as IamGroupUser, Policy as IamPolicy, RoleColumn, RoleTag as IamRoleTag,
18    UserColumn, UserTag as IamUserTag,
19};
20#[cfg(test)]
21use crate::iam::{IamRole, IamUser, LastAccessedService};
22use crate::keymap::{Action, Mode};
23pub use crate::lambda::{
24    Alias as LambdaAlias, Application as LambdaApplication,
25    ApplicationColumn as LambdaApplicationColumn, Deployment, DeploymentColumn,
26    Function as LambdaFunction, FunctionColumn as LambdaColumn, Layer as LambdaLayer, Resource,
27    ResourceColumn as LambdaResourceColumn, Version as LambdaVersion,
28};
29pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
30use crate::session::{Session, SessionTab};
31pub use crate::sqs::queue::Column as SqsColumn;
32pub use crate::sqs::trigger::Column as SqsTriggerColumn;
33use crate::sqs::{console_url_queue_detail, console_url_queues};
34#[cfg(test)]
35use crate::sqs::{
36    EventBridgePipe, LambdaTrigger, Queue as SqsQueue, QueueTag as SqsQueueTag, SnsSubscription,
37};
38use crate::table::TableState;
39pub use crate::ui::apig::{
40    filtered_apis, State as ApigState, FILTER_CONTROLS as APIG_FILTER_CONTROLS,
41};
42use crate::ui::cfn::State as CfnStateConstants;
43pub use crate::ui::cfn::{
44    filtered_cloudformation_stacks, filtered_outputs, filtered_parameters, filtered_resources,
45    output_column_ids, parameter_column_ids, resource_column_ids, DetailTab as CfnDetailTab,
46    State as CfnState, StatusFilter as CfnStatusFilter,
47};
48pub use crate::ui::cw::alarms::{
49    AlarmTab, AlarmViewMode, FILTER_CONTROLS as ALARM_FILTER_CONTROLS,
50};
51pub use crate::ui::cw::logs::{
52    filtered_log_events, filtered_log_groups, filtered_log_streams, selected_log_group,
53    DetailTab as CwLogsDetailTab, EventFilterFocus, FILTER_CONTROLS as LOG_FILTER_CONTROLS,
54};
55use crate::ui::ec2;
56use crate::ui::ec2::filtered_ec2_instances;
57pub use crate::ui::ec2::{
58    DetailTab as Ec2DetailTab, State as Ec2State, StateFilter as Ec2StateFilter,
59    STATE_FILTER as EC2_STATE_FILTER,
60};
61pub use crate::ui::ecr::{
62    filtered_ecr_images, filtered_ecr_repositories, State as EcrState, Tab as EcrTab,
63    FILTER_CONTROLS as ECR_FILTER_CONTROLS,
64};
65use crate::ui::iam::{
66    filtered_iam_policies, filtered_iam_roles, filtered_iam_users, filtered_last_accessed,
67    filtered_tags as filtered_iam_tags, filtered_user_tags, GroupTab, RoleTab, State as IamState,
68    UserTab,
69};
70pub use crate::ui::lambda::{
71    filtered_lambda_applications, filtered_lambda_functions,
72    ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
73    DetailTab as LambdaDetailTab, State as LambdaState, FILTER_CONTROLS as LAMBDA_FILTER_CONTROLS,
74};
75use crate::ui::monitoring::MonitoringState;
76pub use crate::ui::s3::{
77    calculate_total_bucket_rows, calculate_total_object_rows, BucketType as S3BucketType,
78    ObjectTab as S3ObjectTab, State as S3State,
79};
80pub use crate::ui::sqs::{
81    extract_account_id, extract_region, filtered_eventbridge_pipes, filtered_lambda_triggers,
82    filtered_queues, filtered_subscriptions, filtered_tags, QueueDetailTab as SqsQueueDetailTab,
83    State as SqsState, FILTER_CONTROLS as SQS_FILTER_CONTROLS,
84    SUBSCRIPTION_FILTER_CONTROLS as SQS_SUBSCRIPTION_FILTER_CONTROLS, SUBSCRIPTION_REGION,
85};
86use crate::ui::tree::TreeItem;
87pub use crate::ui::{
88    CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, LogGroupColumn, Preferences,
89    StreamColumn, StreamSort, TimeUnit,
90};
91#[cfg(test)]
92use rusticity_core::LogStream;
93use rusticity_core::{
94    AlarmsClient, ApiGatewayClient, AwsConfig, CloudFormationClient, CloudTrailClient,
95    CloudWatchClient, Ec2Client, EcrClient, IamClient, LambdaClient, S3Client, SqsClient,
96};
97use std::collections::HashMap;
98
99#[derive(Clone)]
100pub struct Tab {
101    pub service: Service,
102    pub title: String,
103    pub breadcrumb: String,
104}
105
106pub struct App {
107    pub running: bool,
108    pub mode: Mode,
109    pub config: AwsConfig,
110    pub cloudwatch_client: CloudWatchClient,
111    pub cloudtrail_client: CloudTrailClient,
112    pub s3_client: S3Client,
113    pub sqs_client: SqsClient,
114    pub alarms_client: AlarmsClient,
115    pub ec2_client: Ec2Client,
116    pub ecr_client: EcrClient,
117    pub apig_client: ApiGatewayClient,
118    pub iam_client: IamClient,
119    pub lambda_client: LambdaClient,
120    pub cloudformation_client: CloudFormationClient,
121    pub current_service: Service,
122    pub tabs: Vec<Tab>,
123    pub current_tab: usize,
124    pub tab_picker_selected: usize,
125    pub tab_filter: String,
126    pub pending_key: Option<char>,
127    pub log_groups_state: CloudWatchLogGroupsState,
128    pub insights_state: CloudWatchInsightsState,
129    pub alarms_state: CloudWatchAlarmsState,
130    pub cloudtrail_state: CloudTrailState,
131    pub s3_state: S3State,
132    pub sqs_state: SqsState,
133    pub ec2_state: Ec2State,
134    pub ecr_state: EcrState,
135    pub apig_state: ApigState,
136    pub lambda_state: LambdaState,
137    pub lambda_application_state: LambdaApplicationState,
138    pub cfn_state: CfnState,
139    pub iam_state: IamState,
140    pub service_picker: ServicePickerState,
141    pub service_selected: bool,
142    pub profile: String,
143    pub region: String,
144    pub region_selector_index: usize,
145    pub cw_log_group_visible_column_ids: Vec<ColumnId>,
146    pub cw_log_group_column_ids: Vec<ColumnId>,
147    pub column_selector_index: usize,
148    pub preference_section: Preferences,
149    pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
150    pub cw_log_stream_column_ids: Vec<ColumnId>,
151    pub cw_log_event_visible_column_ids: Vec<ColumnId>,
152    pub cw_log_event_column_ids: Vec<ColumnId>,
153    pub cw_log_tag_visible_column_ids: Vec<ColumnId>,
154    pub cw_log_tag_column_ids: Vec<ColumnId>,
155    pub cw_alarm_visible_column_ids: Vec<ColumnId>,
156    pub cw_alarm_column_ids: Vec<ColumnId>,
157    pub cloudtrail_event_visible_column_ids: Vec<ColumnId>,
158    pub cloudtrail_event_column_ids: Vec<ColumnId>,
159    pub cloudtrail_resource_visible_column_ids: Vec<ColumnId>,
160    pub cloudtrail_resource_column_ids: Vec<ColumnId>,
161    pub s3_bucket_visible_column_ids: Vec<ColumnId>,
162    pub s3_bucket_column_ids: Vec<ColumnId>,
163    pub sqs_visible_column_ids: Vec<ColumnId>,
164    pub sqs_column_ids: Vec<ColumnId>,
165    pub ec2_visible_column_ids: Vec<ColumnId>,
166    pub ec2_column_ids: Vec<ColumnId>,
167    pub ecr_repo_visible_column_ids: Vec<ColumnId>,
168    pub ecr_repo_column_ids: Vec<ColumnId>,
169    pub ecr_image_visible_column_ids: Vec<ColumnId>,
170    pub ecr_image_column_ids: Vec<ColumnId>,
171    pub apig_api_visible_column_ids: Vec<ColumnId>,
172    pub apig_api_column_ids: Vec<ColumnId>,
173    pub apig_route_visible_column_ids: Vec<ColumnId>,
174    pub apig_route_column_ids: Vec<ColumnId>,
175    pub apig_resource_visible_column_ids: Vec<ColumnId>,
176    pub apig_resource_column_ids: Vec<ColumnId>,
177    pub lambda_application_visible_column_ids: Vec<ColumnId>,
178    pub lambda_application_column_ids: Vec<ColumnId>,
179    pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
180    pub lambda_deployment_column_ids: Vec<ColumnId>,
181    pub lambda_resource_visible_column_ids: Vec<ColumnId>,
182    pub lambda_resource_column_ids: Vec<ColumnId>,
183    pub cfn_visible_column_ids: Vec<ColumnId>,
184    pub cfn_column_ids: Vec<ColumnId>,
185    pub cfn_parameter_visible_column_ids: Vec<ColumnId>,
186    pub cfn_parameter_column_ids: Vec<ColumnId>,
187    pub cfn_output_visible_column_ids: Vec<ColumnId>,
188    pub cfn_output_column_ids: Vec<ColumnId>,
189    pub cfn_resource_visible_column_ids: Vec<ColumnId>,
190    pub cfn_resource_column_ids: Vec<ColumnId>,
191    pub iam_user_visible_column_ids: Vec<ColumnId>,
192    pub iam_user_column_ids: Vec<ColumnId>,
193    pub iam_role_visible_column_ids: Vec<ColumnId>,
194    pub iam_role_column_ids: Vec<ColumnId>,
195    pub iam_group_visible_column_ids: Vec<String>,
196    pub iam_group_column_ids: Vec<String>,
197    pub iam_policy_visible_column_ids: Vec<String>,
198    pub iam_policy_column_ids: Vec<String>,
199    pub view_mode: ViewMode,
200    pub error_message: Option<String>,
201    pub error_scroll: usize,
202    pub page_input: String,
203    pub calendar_date: Option<time::Date>,
204    pub calendar_selecting: CalendarField,
205    pub cursor_pos: usize,
206    pub current_session: Option<Session>,
207    pub sessions: Vec<Session>,
208    pub session_picker_selected: usize,
209    pub session_filter: String,
210    pub session_filter_active: bool,
211    pub region_filter: String,
212    pub region_picker_selected: usize,
213    pub region_filter_active: bool,
214    pub region_latencies: std::collections::HashMap<String, u64>,
215    pub profile_filter: String,
216    pub profile_picker_selected: usize,
217    pub profile_filter_active: bool,
218    pub available_profiles: Vec<AwsProfile>,
219    pub snapshot_requested: bool,
220}
221
222#[derive(Debug, Clone, Copy, PartialEq)]
223pub enum CalendarField {
224    StartDate,
225    EndDate,
226}
227
228pub struct CloudWatchInsightsState {
229    pub insights: InsightsState,
230    pub loading: bool,
231}
232
233pub struct CloudWatchAlarmsState {
234    pub table: TableState<Alarm>,
235    pub alarm_tab: AlarmTab,
236    pub view_as: AlarmViewMode,
237    pub wrap_lines: bool,
238    pub sort_column: String,
239    pub sort_direction: SortDirection,
240    pub input_focus: InputFocus,
241}
242
243#[derive(Debug, Clone)]
244pub struct CloudTrailState {
245    pub table: TableState<CloudTrailEvent>,
246    pub input_focus: InputFocus,
247    pub current_event: Option<CloudTrailEvent>,
248    pub event_json_scroll: usize,
249    pub detail_focus: CloudTrailDetailFocus,
250    pub resources_expanded_index: Option<usize>,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq)]
254pub enum CloudTrailDetailFocus {
255    Resources,
256    EventRecord,
257}
258
259impl CyclicEnum for CloudTrailDetailFocus {
260    const ALL: &'static [Self] = &[Self::Resources, Self::EventRecord];
261}
262
263impl PageSize {
264    pub fn value(&self) -> usize {
265        match self {
266            PageSize::Ten => 10,
267            PageSize::TwentyFive => 25,
268            PageSize::Fifty => 50,
269            PageSize::OneHundred => 100,
270        }
271    }
272
273    pub fn next(&self) -> Self {
274        match self {
275            PageSize::Ten => PageSize::TwentyFive,
276            PageSize::TwentyFive => PageSize::Fifty,
277            PageSize::Fifty => PageSize::OneHundred,
278            PageSize::OneHundred => PageSize::Ten,
279        }
280    }
281}
282
283pub struct ServicePickerState {
284    pub filter: String,
285    pub filter_active: bool,
286    pub selected: usize,
287    pub services: Vec<&'static str>,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq)]
291pub enum ViewMode {
292    List,
293    Detail,
294    Events,
295    InsightsResults,
296    PolicyView,
297}
298
299#[derive(Debug, Clone, Copy, PartialEq)]
300pub enum Service {
301    ApiGatewayApis,
302    CloudWatchLogGroups,
303    CloudWatchInsights,
304    CloudWatchAlarms,
305    CloudTrailEvents,
306    S3Buckets,
307    SqsQueues,
308    Ec2Instances,
309    EcrRepositories,
310    LambdaFunctions,
311    LambdaApplications,
312    CloudFormationStacks,
313    IamUsers,
314    IamRoles,
315    IamUserGroups,
316}
317
318impl Service {
319    pub fn name(&self) -> &str {
320        match self {
321            Service::ApiGatewayApis => "API Gateway › APIs",
322            Service::CloudWatchLogGroups => "CloudWatch › Log Groups",
323            Service::CloudWatchInsights => "CloudWatch › Logs Insights",
324            Service::CloudWatchAlarms => "CloudWatch › Alarms",
325            Service::CloudTrailEvents => "CloudTrail › Event History",
326            Service::S3Buckets => "S3 › Buckets",
327            Service::SqsQueues => "SQS › Queues",
328            Service::Ec2Instances => "EC2 › Instances",
329            Service::EcrRepositories => "ECR › Repositories",
330            Service::LambdaFunctions => "Lambda › Functions",
331            Service::LambdaApplications => "Lambda › Applications",
332            Service::CloudFormationStacks => "CloudFormation › Stacks",
333            Service::IamUsers => "IAM › Users",
334            Service::IamRoles => "IAM › Roles",
335            Service::IamUserGroups => "IAM › User Groups",
336        }
337    }
338}
339
340fn copy_to_clipboard(text: &str) {
341    use std::io::Write;
342    use std::process::{Command, Stdio};
343    if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
344        if let Some(mut stdin) = child.stdin.take() {
345            let _ = stdin.write_all(text.as_bytes());
346        }
347        let _ = child.wait();
348    }
349}
350
351fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
352    if max > 0 {
353        *selected = (*selected + page_size).min(max - 1);
354    }
355}
356
357fn toggle_iam_preference(
358    idx: usize,
359    column_ids: &[String],
360    visible_column_ids: &mut Vec<String>,
361    page_size: &mut PageSize,
362) {
363    if idx > 0 && idx <= column_ids.len() {
364        if let Some(col) = column_ids.get(idx - 1) {
365            if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
366                visible_column_ids.remove(pos);
367            } else {
368                visible_column_ids.push(col.clone());
369            }
370        }
371    } else if idx == column_ids.len() + 3 {
372        *page_size = PageSize::Ten;
373    } else if idx == column_ids.len() + 4 {
374        *page_size = PageSize::TwentyFive;
375    } else if idx == column_ids.len() + 5 {
376        *page_size = PageSize::Fifty;
377    }
378}
379
380fn toggle_iam_preference_static(
381    idx: usize,
382    column_ids: &[ColumnId],
383    visible_column_ids: &mut Vec<ColumnId>,
384    page_size: &mut PageSize,
385) {
386    if idx > 0 && idx <= column_ids.len() {
387        if let Some(col) = column_ids.get(idx - 1) {
388            if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
389                visible_column_ids.remove(pos);
390            } else {
391                visible_column_ids.push(*col);
392            }
393        }
394    } else if idx == column_ids.len() + 3 {
395        *page_size = PageSize::Ten;
396    } else if idx == column_ids.len() + 4 {
397        *page_size = PageSize::TwentyFive;
398    } else if idx == column_ids.len() + 5 {
399        *page_size = PageSize::Fifty;
400    }
401}
402
403fn toggle_iam_page_size_only(idx: usize, base_idx: usize, page_size: &mut PageSize) {
404    if idx == base_idx {
405        *page_size = PageSize::Ten;
406    } else if idx == base_idx + 1 {
407        *page_size = PageSize::TwentyFive;
408    } else if idx == base_idx + 2 {
409        *page_size = PageSize::Fifty;
410    }
411}
412
413/// Helper to cycle to next preference section (Columns -> PageSize -> Columns)
414fn cycle_preference_next(current_idx: &mut usize, num_columns: usize) {
415    let page_size_idx = num_columns + 2;
416    if *current_idx < page_size_idx {
417        *current_idx = page_size_idx;
418    } else {
419        *current_idx = 0;
420    }
421}
422
423/// Helper to cycle to previous preference section (Columns <- PageSize <- Columns)
424fn cycle_preference_prev(current_idx: &mut usize, num_columns: usize) {
425    let page_size_idx = num_columns + 2;
426    if *current_idx >= page_size_idx {
427        *current_idx = 0;
428    } else {
429        *current_idx = page_size_idx;
430    }
431}
432
433impl App {
434    pub fn get_input_focus(&self) -> InputFocus {
435        InputFocus::Filter
436    }
437
438    fn get_active_filter_mut(&mut self) -> Option<&mut String> {
439        if self.current_service == Service::ApiGatewayApis {
440            if self.apig_state.current_api.is_some()
441                && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
442            {
443                Some(&mut self.apig_state.route_filter)
444            } else {
445                Some(&mut self.apig_state.apis.filter)
446            }
447        } else if self.current_service == Service::CloudWatchAlarms {
448            Some(&mut self.alarms_state.table.filter)
449        } else if self.current_service == Service::CloudTrailEvents {
450            Some(&mut self.cloudtrail_state.table.filter)
451        } else if self.current_service == Service::Ec2Instances {
452            if self.ec2_state.current_instance.is_some()
453                && self.ec2_state.detail_tab == Ec2DetailTab::Tags
454            {
455                Some(&mut self.ec2_state.tags.filter)
456            } else {
457                Some(&mut self.ec2_state.table.filter)
458            }
459        } else if self.current_service == Service::S3Buckets {
460            if self.s3_state.current_bucket.is_some() {
461                Some(&mut self.s3_state.object_filter)
462            } else {
463                Some(&mut self.s3_state.buckets.filter)
464            }
465        } else if self.current_service == Service::EcrRepositories {
466            if self.ecr_state.current_repository.is_some() {
467                Some(&mut self.ecr_state.images.filter)
468            } else {
469                Some(&mut self.ecr_state.repositories.filter)
470            }
471        } else if self.current_service == Service::SqsQueues {
472            if self.sqs_state.current_queue.is_some()
473                && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
474            {
475                Some(&mut self.sqs_state.triggers.filter)
476            } else if self.sqs_state.current_queue.is_some()
477                && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
478            {
479                Some(&mut self.sqs_state.pipes.filter)
480            } else if self.sqs_state.current_queue.is_some()
481                && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
482            {
483                Some(&mut self.sqs_state.tags.filter)
484            } else if self.sqs_state.current_queue.is_some()
485                && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
486            {
487                Some(&mut self.sqs_state.subscriptions.filter)
488            } else {
489                Some(&mut self.sqs_state.queues.filter)
490            }
491        } else if self.current_service == Service::LambdaFunctions {
492            if self.lambda_state.current_version.is_some()
493                && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
494            {
495                Some(&mut self.lambda_state.alias_table.filter)
496            } else if self.lambda_state.current_function.is_some()
497                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
498            {
499                Some(&mut self.lambda_state.version_table.filter)
500            } else if self.lambda_state.current_function.is_some()
501                && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
502            {
503                Some(&mut self.lambda_state.alias_table.filter)
504            } else {
505                Some(&mut self.lambda_state.table.filter)
506            }
507        } else if self.current_service == Service::LambdaApplications {
508            if self.lambda_application_state.current_application.is_some() {
509                if self.lambda_application_state.detail_tab
510                    == LambdaApplicationDetailTab::Deployments
511                {
512                    Some(&mut self.lambda_application_state.deployments.filter)
513                } else {
514                    Some(&mut self.lambda_application_state.resources.filter)
515                }
516            } else {
517                Some(&mut self.lambda_application_state.table.filter)
518            }
519        } else if self.current_service == Service::CloudFormationStacks {
520            if self.cfn_state.current_stack.is_some()
521                && self.cfn_state.detail_tab == CfnDetailTab::Resources
522            {
523                Some(&mut self.cfn_state.resources.filter)
524            } else {
525                Some(&mut self.cfn_state.table.filter)
526            }
527        } else if self.current_service == Service::IamUsers {
528            if self.iam_state.current_user.is_some() {
529                if self.iam_state.user_tab == UserTab::Tags {
530                    Some(&mut self.iam_state.user_tags.filter)
531                } else {
532                    Some(&mut self.iam_state.policies.filter)
533                }
534            } else {
535                Some(&mut self.iam_state.users.filter)
536            }
537        } else if self.current_service == Service::IamRoles {
538            if self.iam_state.current_role.is_some() {
539                if self.iam_state.role_tab == RoleTab::Tags {
540                    Some(&mut self.iam_state.tags.filter)
541                } else if self.iam_state.role_tab == RoleTab::LastAccessed {
542                    Some(&mut self.iam_state.last_accessed_filter)
543                } else {
544                    Some(&mut self.iam_state.policies.filter)
545                }
546            } else {
547                Some(&mut self.iam_state.roles.filter)
548            }
549        } else if self.current_service == Service::IamUserGroups {
550            if self.iam_state.current_group.is_some() {
551                if self.iam_state.group_tab == GroupTab::Permissions {
552                    Some(&mut self.iam_state.policies.filter)
553                } else if self.iam_state.group_tab == GroupTab::Users {
554                    Some(&mut self.iam_state.group_users.filter)
555                } else {
556                    None
557                }
558            } else {
559                Some(&mut self.iam_state.groups.filter)
560            }
561        } else if self.view_mode == ViewMode::List {
562            Some(&mut self.log_groups_state.log_groups.filter)
563        } else if self.view_mode == ViewMode::Detail
564            && self.log_groups_state.detail_tab == DetailTab::LogStreams
565        {
566            Some(&mut self.log_groups_state.stream_filter)
567        } else {
568            None
569        }
570    }
571
572    fn apply_filter_operation<F>(&mut self, op: F)
573    where
574        F: FnOnce(&mut String),
575    {
576        if let Some(filter) = self.get_active_filter_mut() {
577            op(filter);
578            // Automatically reset selection for all services
579            if self.current_service == Service::CloudWatchAlarms {
580                self.alarms_state.table.reset();
581            } else if self.current_service == Service::CloudTrailEvents {
582                self.cloudtrail_state.table.reset();
583            } else if self.current_service == Service::Ec2Instances {
584                self.ec2_state.table.reset();
585            } else if self.current_service == Service::S3Buckets {
586                if self.s3_state.current_bucket.is_some() {
587                    self.s3_state.selected_object = 0;
588                } else {
589                    self.s3_state.buckets.reset();
590                    self.s3_state.selected_row = 0;
591                    self.s3_state.bucket_scroll_offset = 0;
592                }
593            } else if self.current_service == Service::EcrRepositories {
594                if self.ecr_state.current_repository.is_some() {
595                    self.ecr_state.images.reset();
596                } else {
597                    self.ecr_state.repositories.reset();
598                }
599            } else if self.current_service == Service::SqsQueues {
600                self.sqs_state.queues.reset();
601            } else if self.current_service == Service::LambdaFunctions {
602                if self.lambda_state.current_version.is_some()
603                    || self.lambda_state.current_function.is_some()
604                {
605                    self.lambda_state.version_table.reset();
606                    self.lambda_state.alias_table.reset();
607                } else {
608                    self.lambda_state.table.reset();
609                }
610            } else if self.current_service == Service::LambdaApplications {
611                if self.lambda_application_state.current_application.is_some() {
612                    self.lambda_application_state.deployments.reset();
613                    self.lambda_application_state.resources.reset();
614                } else {
615                    self.lambda_application_state.table.reset();
616                }
617            } else if self.current_service == Service::CloudFormationStacks {
618                self.cfn_state.table.reset();
619            } else if self.current_service == Service::IamUsers {
620                if self.iam_state.current_user.is_some() {
621                    self.iam_state.user_tags.reset();
622                    self.iam_state.policies.reset();
623                } else {
624                    self.iam_state.users.reset();
625                }
626            } else if self.current_service == Service::IamRoles {
627                if self.iam_state.current_role.is_some() {
628                    self.iam_state.tags.reset();
629                    self.iam_state.policies.reset();
630                } else {
631                    self.iam_state.roles.reset();
632                }
633            } else if self.current_service == Service::IamUserGroups {
634                if self.iam_state.current_group.is_some() {
635                    self.iam_state.policies.reset();
636                    self.iam_state.group_users.reset();
637                } else {
638                    self.iam_state.groups.reset();
639                }
640            } else if self.current_service == Service::CloudWatchLogGroups {
641                if self.view_mode == ViewMode::List {
642                    self.log_groups_state.log_groups.reset();
643                } else if self.log_groups_state.detail_tab == DetailTab::LogStreams {
644                    self.log_groups_state.selected_stream = 0;
645                }
646            } else if self.current_service == Service::ApiGatewayApis {
647                self.apig_state.apis.reset();
648            }
649        }
650    }
651
652    pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
653        let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
654            .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
655
656        std::env::set_var("AWS_PROFILE", &profile_name);
657
658        let config = AwsConfig::new(region).await?;
659        let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
660        let cloudtrail_client = CloudTrailClient::new(config.clone());
661        let s3_client = S3Client::new(config.clone());
662        let sqs_client = SqsClient::new(config.clone());
663        let alarms_client = AlarmsClient::new(config.clone());
664        let ec2_client = Ec2Client::new(config.clone());
665        let ecr_client = EcrClient::new(config.clone());
666        let apig_client = ApiGatewayClient::new(config.clone());
667        let iam_client = IamClient::new(config.clone());
668        let lambda_client = LambdaClient::new(config.clone());
669        let cloudformation_client = CloudFormationClient::new(config.clone());
670        let region_name = config.region.clone();
671
672        Ok(Self {
673            running: true,
674            mode: Mode::ServicePicker,
675            config,
676            cloudwatch_client,
677            cloudtrail_client,
678            s3_client,
679            sqs_client,
680            alarms_client,
681            ec2_client,
682            ecr_client,
683            apig_client,
684            iam_client,
685            lambda_client,
686            cloudformation_client,
687            current_service: Service::CloudWatchLogGroups,
688            tabs: Vec::new(),
689            current_tab: 0,
690            tab_picker_selected: 0,
691            tab_filter: String::new(),
692            pending_key: None,
693            log_groups_state: CloudWatchLogGroupsState::new(),
694            insights_state: CloudWatchInsightsState::new(),
695            alarms_state: CloudWatchAlarmsState::new(),
696            cloudtrail_state: CloudTrailState {
697                table: TableState::new(),
698                input_focus: InputFocus::Filter,
699                current_event: None,
700                event_json_scroll: 0,
701                detail_focus: CloudTrailDetailFocus::Resources,
702                resources_expanded_index: None,
703            },
704            s3_state: S3State::new(),
705            sqs_state: SqsState::new(),
706            ec2_state: Ec2State::default(),
707            ecr_state: EcrState::new(),
708            apig_state: ApigState::new(),
709            lambda_state: LambdaState::new(),
710            lambda_application_state: LambdaApplicationState::new(),
711            cfn_state: CfnState::new(),
712            iam_state: IamState::new(),
713            service_picker: ServicePickerState::new(),
714            service_selected: false,
715            profile: profile_name,
716            region: region_name,
717            region_selector_index: 0,
718            cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
719            cw_log_group_column_ids: LogGroupColumn::ids(),
720            column_selector_index: 0,
721            cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
722            cw_log_stream_column_ids: StreamColumn::ids(),
723            cw_log_event_visible_column_ids: EventColumn::default_visible(),
724            cw_log_event_column_ids: EventColumn::ids(),
725            cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
726            cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
727            cw_alarm_visible_column_ids: [
728                AlarmColumn::Name,
729                AlarmColumn::State,
730                AlarmColumn::LastStateUpdate,
731                AlarmColumn::Conditions,
732                AlarmColumn::Actions,
733            ]
734            .iter()
735            .map(|c| c.id())
736            .collect(),
737            cw_alarm_column_ids: AlarmColumn::ids(),
738            cloudtrail_event_visible_column_ids: [
739                CloudTrailEventColumn::EventName,
740                CloudTrailEventColumn::EventTime,
741                CloudTrailEventColumn::Username,
742                CloudTrailEventColumn::EventSource,
743                CloudTrailEventColumn::ResourceType,
744                CloudTrailEventColumn::ResourceName,
745            ]
746            .iter()
747            .map(|c| c.id())
748            .collect(),
749            cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
750            cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
751            cloudtrail_resource_column_ids: EventResourceColumn::ids(),
752            s3_bucket_visible_column_ids: S3BucketColumn::ids(),
753            s3_bucket_column_ids: S3BucketColumn::ids(),
754            sqs_visible_column_ids: [
755                SqsColumn::Name,
756                SqsColumn::Type,
757                SqsColumn::Created,
758                SqsColumn::MessagesAvailable,
759                SqsColumn::MessagesInFlight,
760                SqsColumn::Encryption,
761                SqsColumn::ContentBasedDeduplication,
762            ]
763            .iter()
764            .map(|c| c.id())
765            .collect(),
766            sqs_column_ids: SqsColumn::ids(),
767            ec2_visible_column_ids: [
768                Ec2Column::Name,
769                Ec2Column::InstanceId,
770                Ec2Column::InstanceState,
771                Ec2Column::InstanceType,
772                Ec2Column::StatusCheck,
773                Ec2Column::AlarmStatus,
774                Ec2Column::AvailabilityZone,
775                Ec2Column::PublicIpv4Dns,
776                Ec2Column::PublicIpv4Address,
777                Ec2Column::ElasticIp,
778                Ec2Column::Ipv6Ips,
779                Ec2Column::Monitoring,
780                Ec2Column::SecurityGroupName,
781                Ec2Column::KeyName,
782                Ec2Column::LaunchTime,
783                Ec2Column::PlatformDetails,
784            ]
785            .iter()
786            .map(|c| c.id())
787            .collect(),
788            ec2_column_ids: Ec2Column::ids(),
789            ecr_repo_visible_column_ids: EcrColumn::ids(),
790            ecr_repo_column_ids: EcrColumn::ids(),
791            ecr_image_visible_column_ids: EcrImageColumn::ids(),
792            ecr_image_column_ids: EcrImageColumn::ids(),
793            apig_api_visible_column_ids: ApigColumn::ids(),
794            apig_api_column_ids: ApigColumn::ids(),
795            apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
796            apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
797            apig_resource_visible_column_ids: ResourceColumn::all()
798                .iter()
799                .map(|c| c.id())
800                .collect(),
801            apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
802            lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
803            lambda_application_column_ids: LambdaApplicationColumn::ids(),
804            lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
805            lambda_deployment_column_ids: DeploymentColumn::ids(),
806            lambda_resource_visible_column_ids: ResourceColumn::ids(),
807            lambda_resource_column_ids: ResourceColumn::ids(),
808            cfn_visible_column_ids: [
809                CfnColumn::Name,
810                CfnColumn::Status,
811                CfnColumn::CreatedTime,
812                CfnColumn::Description,
813            ]
814            .iter()
815            .map(|c| c.id())
816            .collect(),
817            cfn_column_ids: CfnColumn::ids(),
818            cfn_parameter_visible_column_ids: parameter_column_ids(),
819            cfn_parameter_column_ids: parameter_column_ids(),
820            cfn_output_visible_column_ids: output_column_ids(),
821            cfn_output_column_ids: output_column_ids(),
822            cfn_resource_visible_column_ids: resource_column_ids(),
823            cfn_resource_column_ids: resource_column_ids(),
824            iam_user_visible_column_ids: UserColumn::visible(),
825            iam_user_column_ids: UserColumn::ids(),
826            iam_role_visible_column_ids: RoleColumn::visible(),
827            iam_role_column_ids: RoleColumn::ids(),
828            iam_group_visible_column_ids: vec![
829                "Group name".to_string(),
830                "Users".to_string(),
831                "Permissions".to_string(),
832                "Creation time".to_string(),
833            ],
834            iam_group_column_ids: vec![
835                "Group name".to_string(),
836                "Path".to_string(),
837                "Users".to_string(),
838                "Permissions".to_string(),
839                "Creation time".to_string(),
840            ],
841            iam_policy_visible_column_ids: vec![
842                "Policy name".to_string(),
843                "Type".to_string(),
844                "Attached via".to_string(),
845            ],
846            iam_policy_column_ids: vec![
847                "Policy name".to_string(),
848                "Type".to_string(),
849                "Attached via".to_string(),
850                "Attached entities".to_string(),
851                "Description".to_string(),
852                "Creation time".to_string(),
853                "Edited time".to_string(),
854            ],
855            preference_section: Preferences::Columns,
856            view_mode: ViewMode::List,
857            error_message: None,
858            error_scroll: 0,
859            page_input: String::new(),
860            calendar_date: None,
861            calendar_selecting: CalendarField::StartDate,
862            cursor_pos: 0,
863            current_session: None,
864            sessions: Vec::new(),
865            session_picker_selected: 0,
866            session_filter: String::new(),
867            session_filter_active: false,
868            region_filter: String::new(),
869            region_filter_active: false,
870            region_picker_selected: 0,
871            region_latencies: std::collections::HashMap::new(),
872            profile_filter: String::new(),
873            profile_filter_active: false,
874            profile_picker_selected: 0,
875            available_profiles: Vec::new(),
876            snapshot_requested: false,
877        })
878    }
879
880    pub fn new_without_client(profile: String, region: Option<String>) -> Self {
881        let config = AwsConfig::dummy(region.clone());
882        Self {
883            running: true,
884            mode: Mode::ServicePicker,
885            config: config.clone(),
886            cloudwatch_client: CloudWatchClient::dummy(config.clone()),
887            cloudtrail_client: CloudTrailClient::new(config.clone()),
888            s3_client: S3Client::new(config.clone()),
889            sqs_client: SqsClient::new(config.clone()),
890            alarms_client: AlarmsClient::new(config.clone()),
891            ec2_client: Ec2Client::new(config.clone()),
892            ecr_client: EcrClient::new(config.clone()),
893            apig_client: ApiGatewayClient::new(config.clone()),
894            iam_client: IamClient::new(config.clone()),
895            lambda_client: LambdaClient::new(config.clone()),
896            cloudformation_client: CloudFormationClient::new(config.clone()),
897            current_service: Service::CloudWatchLogGroups,
898            tabs: Vec::new(),
899            current_tab: 0,
900            tab_picker_selected: 0,
901            tab_filter: String::new(),
902            pending_key: None,
903            log_groups_state: CloudWatchLogGroupsState::new(),
904            insights_state: CloudWatchInsightsState::new(),
905            alarms_state: CloudWatchAlarmsState::new(),
906            s3_state: S3State::new(),
907            cloudtrail_state: CloudTrailState {
908                table: TableState::new(),
909                input_focus: InputFocus::Filter,
910                current_event: None,
911                event_json_scroll: 0,
912                detail_focus: CloudTrailDetailFocus::Resources,
913                resources_expanded_index: None,
914            },
915            sqs_state: SqsState::new(),
916            ec2_state: Ec2State::default(),
917            ecr_state: EcrState::new(),
918            apig_state: ApigState::new(),
919            lambda_state: LambdaState::new(),
920            lambda_application_state: LambdaApplicationState::new(),
921            cfn_state: CfnState::new(),
922            iam_state: IamState::new(),
923            service_picker: ServicePickerState::new(),
924            service_selected: false,
925            profile,
926            region: region.unwrap_or_default(),
927            region_selector_index: 0,
928            cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
929            cw_log_group_column_ids: LogGroupColumn::ids(),
930            column_selector_index: 0,
931            preference_section: Preferences::Columns,
932            cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
933            cw_log_stream_column_ids: StreamColumn::ids(),
934            cw_log_event_visible_column_ids: EventColumn::default_visible(),
935            cw_log_event_column_ids: EventColumn::ids(),
936            cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
937            cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
938            cw_alarm_visible_column_ids: [
939                AlarmColumn::Name,
940                AlarmColumn::State,
941                AlarmColumn::LastStateUpdate,
942                AlarmColumn::Conditions,
943                AlarmColumn::Actions,
944            ]
945            .iter()
946            .map(|c| c.id())
947            .collect(),
948            cw_alarm_column_ids: AlarmColumn::ids(),
949            s3_bucket_visible_column_ids: S3BucketColumn::ids(),
950            cloudtrail_event_visible_column_ids: [
951                CloudTrailEventColumn::EventName,
952                CloudTrailEventColumn::EventTime,
953                CloudTrailEventColumn::Username,
954                CloudTrailEventColumn::EventSource,
955                CloudTrailEventColumn::ResourceType,
956                CloudTrailEventColumn::ResourceName,
957            ]
958            .iter()
959            .map(|c| c.id())
960            .collect(),
961            cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
962            cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
963            cloudtrail_resource_column_ids: EventResourceColumn::ids(),
964            s3_bucket_column_ids: S3BucketColumn::ids(),
965            sqs_visible_column_ids: [
966                SqsColumn::Name,
967                SqsColumn::Type,
968                SqsColumn::Created,
969                SqsColumn::MessagesAvailable,
970                SqsColumn::MessagesInFlight,
971                SqsColumn::Encryption,
972                SqsColumn::ContentBasedDeduplication,
973            ]
974            .iter()
975            .map(|c| c.id())
976            .collect(),
977            sqs_column_ids: SqsColumn::ids(),
978            ec2_visible_column_ids: [
979                Ec2Column::Name,
980                Ec2Column::InstanceId,
981                Ec2Column::InstanceState,
982                Ec2Column::InstanceType,
983                Ec2Column::StatusCheck,
984                Ec2Column::AlarmStatus,
985                Ec2Column::AvailabilityZone,
986                Ec2Column::PublicIpv4Dns,
987                Ec2Column::PublicIpv4Address,
988                Ec2Column::ElasticIp,
989                Ec2Column::Ipv6Ips,
990                Ec2Column::Monitoring,
991                Ec2Column::SecurityGroupName,
992                Ec2Column::KeyName,
993                Ec2Column::LaunchTime,
994                Ec2Column::PlatformDetails,
995            ]
996            .iter()
997            .map(|c| c.id())
998            .collect(),
999            ec2_column_ids: Ec2Column::ids(),
1000            ecr_repo_visible_column_ids: EcrColumn::ids(),
1001            ecr_repo_column_ids: EcrColumn::ids(),
1002            ecr_image_visible_column_ids: EcrImageColumn::ids(),
1003            ecr_image_column_ids: EcrImageColumn::ids(),
1004            lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
1005            lambda_application_column_ids: LambdaApplicationColumn::ids(),
1006            apig_api_visible_column_ids: ApigColumn::ids(),
1007            apig_api_column_ids: ApigColumn::ids(),
1008            apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
1009            apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
1010            apig_resource_visible_column_ids: ResourceColumn::all()
1011                .iter()
1012                .map(|c| c.id())
1013                .collect(),
1014            apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
1015            lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
1016            lambda_deployment_column_ids: DeploymentColumn::ids(),
1017            lambda_resource_visible_column_ids: ResourceColumn::ids(),
1018            lambda_resource_column_ids: ResourceColumn::ids(),
1019            cfn_visible_column_ids: [
1020                CfnColumn::Name,
1021                CfnColumn::Status,
1022                CfnColumn::CreatedTime,
1023                CfnColumn::Description,
1024            ]
1025            .iter()
1026            .map(|c| c.id())
1027            .collect(),
1028            cfn_column_ids: CfnColumn::ids(),
1029            iam_user_visible_column_ids: UserColumn::visible(),
1030            cfn_parameter_visible_column_ids: parameter_column_ids(),
1031            cfn_parameter_column_ids: parameter_column_ids(),
1032            cfn_output_visible_column_ids: output_column_ids(),
1033            cfn_output_column_ids: output_column_ids(),
1034            cfn_resource_visible_column_ids: resource_column_ids(),
1035            cfn_resource_column_ids: resource_column_ids(),
1036            iam_user_column_ids: UserColumn::ids(),
1037            iam_role_visible_column_ids: RoleColumn::visible(),
1038            iam_role_column_ids: RoleColumn::ids(),
1039            iam_group_visible_column_ids: vec![
1040                "Group name".to_string(),
1041                "Users".to_string(),
1042                "Permissions".to_string(),
1043                "Creation time".to_string(),
1044            ],
1045            iam_group_column_ids: vec![
1046                "Group name".to_string(),
1047                "Path".to_string(),
1048                "Users".to_string(),
1049                "Permissions".to_string(),
1050                "Creation time".to_string(),
1051            ],
1052            iam_policy_visible_column_ids: vec![
1053                "Policy name".to_string(),
1054                "Type".to_string(),
1055                "Attached via".to_string(),
1056            ],
1057            iam_policy_column_ids: vec![
1058                "Policy name".to_string(),
1059                "Type".to_string(),
1060                "Attached via".to_string(),
1061                "Attached entities".to_string(),
1062                "Description".to_string(),
1063                "Creation time".to_string(),
1064                "Edited time".to_string(),
1065            ],
1066            view_mode: ViewMode::List,
1067            error_message: None,
1068            error_scroll: 0,
1069            page_input: String::new(),
1070            calendar_date: None,
1071            calendar_selecting: CalendarField::StartDate,
1072            cursor_pos: 0,
1073            current_session: None,
1074            sessions: Vec::new(),
1075            session_picker_selected: 0,
1076            session_filter: String::new(),
1077            session_filter_active: false,
1078            region_filter: String::new(),
1079            region_filter_active: false,
1080            region_picker_selected: 0,
1081            region_latencies: std::collections::HashMap::new(),
1082            profile_filter: String::new(),
1083            profile_filter_active: false,
1084            profile_picker_selected: 0,
1085            available_profiles: Vec::new(),
1086            snapshot_requested: false,
1087        }
1088    }
1089
1090    pub fn handle_action(&mut self, action: Action) {
1091        match action {
1092            Action::Noop => {}
1093            Action::EnterFilterMode => match self.mode {
1094                Mode::ServicePicker => self.service_picker.filter_active = true,
1095                Mode::RegionPicker => self.region_filter_active = true,
1096                Mode::SessionPicker => self.session_filter_active = true,
1097                Mode::ProfilePicker => self.profile_filter_active = true,
1098                _ => {}
1099            },
1100            Action::ExitFilterMode => match self.mode {
1101                Mode::ServicePicker => {
1102                    if self.service_picker.filter_active {
1103                        self.service_picker.filter_active = false;
1104                    } else {
1105                        self.mode = Mode::Normal;
1106                    }
1107                }
1108                Mode::RegionPicker => {
1109                    if self.region_filter_active {
1110                        self.region_filter_active = false;
1111                    } else {
1112                        self.mode = Mode::Normal;
1113                    }
1114                }
1115                Mode::SessionPicker => {
1116                    if self.session_filter_active {
1117                        self.session_filter_active = false;
1118                    } else {
1119                        self.mode = Mode::Normal;
1120                    }
1121                }
1122                Mode::ProfilePicker => {
1123                    if self.profile_filter_active {
1124                        self.profile_filter_active = false;
1125                    } else {
1126                        self.mode = Mode::Normal;
1127                    }
1128                }
1129                _ => {}
1130            },
1131            Action::Quit => {
1132                self.save_current_session();
1133                self.running = false;
1134            }
1135            Action::CloseService => {
1136                if !self.tabs.is_empty() {
1137                    // Close the current tab
1138                    self.tabs.remove(self.current_tab);
1139
1140                    if self.tabs.is_empty() {
1141                        // Last tab closed - show service picker
1142                        self.service_selected = false;
1143                        self.current_tab = 0;
1144                        self.mode = Mode::ServicePicker;
1145                    } else {
1146                        // Tabs remain - switch to adjacent tab
1147                        if self.current_tab >= self.tabs.len() {
1148                            self.current_tab = self.tabs.len() - 1;
1149                        }
1150                        self.current_service = self.tabs[self.current_tab].service;
1151                        self.service_selected = true;
1152                        self.mode = Mode::Normal;
1153                    }
1154                } else {
1155                    // No tabs - just close service picker if open
1156                    self.service_selected = false;
1157                    self.mode = Mode::Normal;
1158                }
1159                self.service_picker.filter.clear();
1160                self.service_picker.selected = 0;
1161            }
1162            Action::NextItem => {
1163                // Only navigate when filter is not active
1164                let should_navigate = match self.mode {
1165                    Mode::ServicePicker => !self.service_picker.filter_active,
1166                    Mode::RegionPicker => !self.region_filter_active,
1167                    Mode::SessionPicker => !self.session_filter_active,
1168                    Mode::ProfilePicker => !self.profile_filter_active,
1169                    _ => true,
1170                };
1171                if should_navigate {
1172                    self.next_item();
1173                }
1174            }
1175            Action::PrevItem => {
1176                // Only navigate when filter is not active
1177                let should_navigate = match self.mode {
1178                    Mode::ServicePicker => !self.service_picker.filter_active,
1179                    Mode::RegionPicker => !self.region_filter_active,
1180                    Mode::SessionPicker => !self.session_filter_active,
1181                    Mode::ProfilePicker => !self.profile_filter_active,
1182                    _ => true,
1183                };
1184                if should_navigate {
1185                    self.prev_item();
1186                }
1187            }
1188            Action::PageUp => self.page_up(),
1189            Action::PageDown => self.page_down(),
1190            Action::NextPane => self.next_pane(),
1191            Action::PrevPane => self.prev_pane(),
1192            Action::CollapseRow => self.collapse_row(),
1193            Action::ExpandRow => self.expand_row(),
1194            Action::Select => self.select_item(),
1195            Action::OpenSpaceMenu => {
1196                self.mode = Mode::SpaceMenu;
1197                self.service_picker.filter.clear();
1198                self.service_picker.filter_active = false;
1199                self.service_picker.selected = 0;
1200            }
1201            Action::CloseMenu => {
1202                self.mode = Mode::Normal;
1203                self.service_picker.filter.clear();
1204                // Reset selection when closing filter to avoid out-of-bounds
1205                match self.current_service {
1206                    Service::S3Buckets => {
1207                        self.s3_state.selected_row = 0;
1208                        self.s3_state.selected_object = 0;
1209                    }
1210                    Service::CloudFormationStacks => {
1211                        if self.cfn_state.current_stack.is_some()
1212                            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1213                        {
1214                            self.cfn_state.parameters.reset();
1215                        } else if self.cfn_state.current_stack.is_some()
1216                            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1217                        {
1218                            self.cfn_state.outputs.reset();
1219                        } else {
1220                            self.cfn_state.table.reset();
1221                        }
1222                    }
1223                    Service::LambdaFunctions => {
1224                        self.lambda_state.table.reset();
1225                    }
1226                    Service::SqsQueues => {
1227                        self.sqs_state.queues.reset();
1228                    }
1229                    Service::IamRoles => {
1230                        self.iam_state.roles.reset();
1231                    }
1232                    Service::IamUsers => {
1233                        self.iam_state.users.reset();
1234                    }
1235                    Service::IamUserGroups => {
1236                        self.iam_state.groups.reset();
1237                    }
1238                    Service::CloudWatchAlarms => {
1239                        self.alarms_state.table.reset();
1240                    }
1241                    Service::Ec2Instances => {
1242                        self.ec2_state.table.reset();
1243                    }
1244                    Service::EcrRepositories => {
1245                        self.ecr_state.repositories.reset();
1246                    }
1247                    Service::ApiGatewayApis => {
1248                        self.apig_state.apis.reset();
1249                    }
1250                    Service::LambdaApplications => {
1251                        self.lambda_application_state.table.reset();
1252                    }
1253                    _ => {}
1254                }
1255            }
1256            Action::NextTab => {
1257                if !self.tabs.is_empty() {
1258                    self.current_tab = (self.current_tab + 1) % self.tabs.len();
1259                    self.current_service = self.tabs[self.current_tab].service;
1260                }
1261            }
1262            Action::PrevTab => {
1263                if !self.tabs.is_empty() {
1264                    self.current_tab = if self.current_tab == 0 {
1265                        self.tabs.len() - 1
1266                    } else {
1267                        self.current_tab - 1
1268                    };
1269                    self.current_service = self.tabs[self.current_tab].service;
1270                }
1271            }
1272            Action::CloseTab => {
1273                if !self.tabs.is_empty() {
1274                    self.tabs.remove(self.current_tab);
1275                    if self.tabs.is_empty() {
1276                        // Last tab closed - show service picker
1277                        self.service_selected = false;
1278                        self.current_tab = 0;
1279                        self.service_picker.filter.clear();
1280                        self.service_picker.selected = 0;
1281                        self.mode = Mode::ServicePicker;
1282                    } else {
1283                        // If we closed the last tab, move to the left
1284                        // Otherwise stay at same index (which is now the next tab to the right)
1285                        if self.current_tab >= self.tabs.len() {
1286                            self.current_tab = self.tabs.len() - 1;
1287                        }
1288                        self.current_service = self.tabs[self.current_tab].service;
1289                        self.service_selected = true;
1290                        self.mode = Mode::Normal;
1291                    }
1292                }
1293            }
1294            Action::OpenTabPicker => {
1295                if !self.tabs.is_empty() {
1296                    self.tab_picker_selected = self.current_tab;
1297                    self.mode = Mode::TabPicker;
1298                } else {
1299                    self.mode = Mode::Normal;
1300                }
1301            }
1302            Action::OpenSessionPicker => {
1303                self.save_current_session();
1304                self.sessions = Session::list_all().unwrap_or_default();
1305                self.session_picker_selected = 0;
1306                self.mode = Mode::SessionPicker;
1307            }
1308            Action::LoadSession => {
1309                let filtered_sessions = self.get_filtered_sessions();
1310                if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
1311                    let session = session.clone();
1312                    // Load the session
1313                    self.profile = session.profile.clone();
1314                    self.region = session.region.clone();
1315                    self.config.account_id = session.account_id.clone();
1316                    self.config.role_arn = session.role_arn.clone();
1317
1318                    // Clear existing tabs and load session tabs
1319                    self.tabs.clear();
1320                    for session_tab in &session.tabs {
1321                        // Parse service from string
1322                        let service = match session_tab.service.as_str() {
1323                            "ApiGatewayApis" => Service::ApiGatewayApis,
1324                            "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
1325                            "CloudWatchInsights" => Service::CloudWatchInsights,
1326                            "CloudWatchAlarms" => Service::CloudWatchAlarms,
1327                            "CloudTrailEvents" => Service::CloudTrailEvents,
1328                            "S3Buckets" => Service::S3Buckets,
1329                            "SqsQueues" => Service::SqsQueues,
1330                            "Ec2Instances" => Service::Ec2Instances,
1331                            "EcrRepositories" => Service::EcrRepositories,
1332                            "LambdaFunctions" => Service::LambdaFunctions,
1333                            "LambdaApplications" => Service::LambdaApplications,
1334                            "CloudFormationStacks" => Service::CloudFormationStacks,
1335                            "IamUsers" => Service::IamUsers,
1336                            "IamRoles" => Service::IamRoles,
1337                            "IamUserGroups" => Service::IamUserGroups,
1338                            _ => continue,
1339                        };
1340
1341                        self.tabs.push(Tab {
1342                            service,
1343                            title: session_tab.title.clone(),
1344                            breadcrumb: session_tab.breadcrumb.clone(),
1345                        });
1346
1347                        // Restore filter if present
1348                        if let Some(filter) = &session_tab.filter {
1349                            if service == Service::CloudWatchLogGroups {
1350                                self.log_groups_state.log_groups.filter = filter.clone();
1351                            }
1352                        }
1353                    }
1354
1355                    if !self.tabs.is_empty() {
1356                        self.current_tab = 0;
1357                        self.current_service = self.tabs[0].service;
1358                        self.service_selected = true;
1359                        self.current_session = Some(session.clone());
1360                    }
1361                }
1362                self.mode = Mode::Normal;
1363            }
1364            Action::SaveSession => {
1365                // TODO: Implement session saving
1366            }
1367            Action::OpenServicePicker => {
1368                if self.mode == Mode::ServicePicker {
1369                    self.tabs.push(Tab {
1370                        service: Service::S3Buckets,
1371                        title: "S3 › Buckets".to_string(),
1372                        breadcrumb: "S3 › Buckets".to_string(),
1373                    });
1374                    self.current_tab = self.tabs.len() - 1;
1375                    self.current_service = Service::S3Buckets;
1376                    self.view_mode = ViewMode::List;
1377                    self.service_selected = true;
1378                    self.mode = Mode::Normal;
1379                } else {
1380                    self.mode = Mode::ServicePicker;
1381                    self.service_picker.filter.clear();
1382                    self.service_picker.selected = 0;
1383                }
1384            }
1385            Action::OpenCloudWatch => {
1386                self.current_service = Service::CloudWatchLogGroups;
1387                self.view_mode = ViewMode::List;
1388                self.service_selected = true;
1389                self.mode = Mode::Normal;
1390            }
1391            Action::OpenCloudWatchSplit => {
1392                self.current_service = Service::CloudWatchInsights;
1393                self.view_mode = ViewMode::InsightsResults;
1394                self.service_selected = true;
1395                self.mode = Mode::Normal;
1396            }
1397            Action::OpenCloudWatchAlarms => {
1398                self.current_service = Service::CloudWatchAlarms;
1399                self.view_mode = ViewMode::List;
1400                self.service_selected = true;
1401                self.mode = Mode::Normal;
1402            }
1403            Action::FilterInput(c) => {
1404                if self.mode == Mode::TabPicker {
1405                    self.tab_filter.push(c);
1406                    self.tab_picker_selected = 0;
1407                } else if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1408                    self.service_picker.filter.push(c);
1409                    self.service_picker.selected = 0;
1410                } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1411                    self.region_filter.push(c);
1412                    self.region_picker_selected = 0;
1413                } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1414                    self.profile_filter.push(c);
1415                    self.profile_picker_selected = 0;
1416                } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1417                    self.session_filter.push(c);
1418                    self.session_picker_selected = 0;
1419                } else if self.mode == Mode::InsightsInput {
1420                    match self.insights_state.insights.insights_focus {
1421                        InsightsFocus::Query => {
1422                            self.insights_state.insights.query_text.push(c);
1423                        }
1424                        InsightsFocus::LogGroupSearch => {
1425                            self.insights_state.insights.log_group_search.push(c);
1426                            // Update matches
1427                            if !self.insights_state.insights.log_group_search.is_empty() {
1428                                self.insights_state.insights.log_group_matches = self
1429                                    .log_groups_state
1430                                    .log_groups
1431                                    .items
1432                                    .iter()
1433                                    .filter(|g| {
1434                                        g.name.to_lowercase().contains(
1435                                            &self
1436                                                .insights_state
1437                                                .insights
1438                                                .log_group_search
1439                                                .to_lowercase(),
1440                                        )
1441                                    })
1442                                    .take(50)
1443                                    .map(|g| g.name.clone())
1444                                    .collect();
1445                                self.insights_state.insights.show_dropdown = true;
1446                            } else {
1447                                self.insights_state.insights.log_group_matches.clear();
1448                                self.insights_state.insights.show_dropdown = false;
1449                            }
1450                        }
1451                        _ => {}
1452                    }
1453                } else if self.mode == Mode::FilterInput {
1454                    // Check if we should capture digits for page navigation
1455                    let is_pagination_focused = if self.current_service
1456                        == Service::LambdaApplications
1457                    {
1458                        if self.lambda_application_state.current_application.is_some() {
1459                            if self.lambda_application_state.detail_tab
1460                                == LambdaApplicationDetailTab::Deployments
1461                            {
1462                                self.lambda_application_state.deployment_input_focus
1463                                    == InputFocus::Pagination
1464                            } else {
1465                                self.lambda_application_state.resource_input_focus
1466                                    == InputFocus::Pagination
1467                            }
1468                        } else {
1469                            self.lambda_application_state.input_focus == InputFocus::Pagination
1470                        }
1471                    } else if self.current_service == Service::CloudFormationStacks {
1472                        self.cfn_state.input_focus == InputFocus::Pagination
1473                    } else if self.current_service == Service::IamRoles
1474                        && self.iam_state.current_role.is_none()
1475                    {
1476                        self.iam_state.role_input_focus == InputFocus::Pagination
1477                    } else if self.view_mode == ViewMode::PolicyView {
1478                        self.iam_state.policy_input_focus == InputFocus::Pagination
1479                    } else if self.current_service == Service::CloudWatchAlarms {
1480                        self.alarms_state.input_focus == InputFocus::Pagination
1481                    } else if self.current_service == Service::CloudTrailEvents {
1482                        self.cloudtrail_state.input_focus == InputFocus::Pagination
1483                    } else if self.current_service == Service::Ec2Instances {
1484                        self.ec2_state.input_focus == InputFocus::Pagination
1485                    } else if self.current_service == Service::CloudWatchLogGroups {
1486                        self.log_groups_state.input_focus == InputFocus::Pagination
1487                    } else if self.current_service == Service::ApiGatewayApis {
1488                        self.apig_state.input_focus == InputFocus::Pagination
1489                    } else if self.current_service == Service::EcrRepositories
1490                        && self.ecr_state.current_repository.is_none()
1491                    {
1492                        self.ecr_state.input_focus == InputFocus::Pagination
1493                    } else if self.current_service == Service::LambdaFunctions {
1494                        if self.lambda_state.current_function.is_some()
1495                            && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1496                        {
1497                            self.lambda_state.version_input_focus == InputFocus::Pagination
1498                        } else if self.lambda_state.current_function.is_none() {
1499                            self.lambda_state.input_focus == InputFocus::Pagination
1500                        } else {
1501                            false
1502                        }
1503                    } else if self.current_service == Service::SqsQueues {
1504                        if self.sqs_state.current_queue.is_some()
1505                            && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1506                                || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1507                                || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1508                                || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1509                                || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1510                        {
1511                            self.sqs_state.input_focus == InputFocus::Pagination
1512                        } else {
1513                            false
1514                        }
1515                    } else {
1516                        false
1517                    };
1518
1519                    if is_pagination_focused && c.is_ascii_digit() {
1520                        self.page_input.push(c);
1521                    } else if self.current_service == Service::LambdaApplications {
1522                        let is_input_focused =
1523                            if self.lambda_application_state.current_application.is_some() {
1524                                if self.lambda_application_state.detail_tab
1525                                    == LambdaApplicationDetailTab::Deployments
1526                                {
1527                                    self.lambda_application_state.deployment_input_focus
1528                                        == InputFocus::Filter
1529                                } else {
1530                                    self.lambda_application_state.resource_input_focus
1531                                        == InputFocus::Filter
1532                                }
1533                            } else {
1534                                self.lambda_application_state.input_focus == InputFocus::Filter
1535                            };
1536                        if is_input_focused {
1537                            self.apply_filter_operation(|f| f.push(c));
1538                        }
1539                    } else if self.current_service == Service::CloudFormationStacks {
1540                        if self.cfn_state.current_stack.is_some()
1541                            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1542                        {
1543                            if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1544                                self.cfn_state.parameters.filter.push(c);
1545                                self.cfn_state.parameters.selected = 0;
1546                            }
1547                        } else if self.cfn_state.current_stack.is_some()
1548                            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1549                        {
1550                            if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1551                                self.cfn_state.outputs.filter.push(c);
1552                                self.cfn_state.outputs.selected = 0;
1553                            }
1554                        } else if self.cfn_state.current_stack.is_some()
1555                            && self.cfn_state.detail_tab == CfnDetailTab::Resources
1556                        {
1557                            if self.cfn_state.resources_input_focus == InputFocus::Filter {
1558                                self.cfn_state.resources.filter.push(c);
1559                                self.cfn_state.resources.selected = 0;
1560                            }
1561                        } else if self.cfn_state.input_focus == InputFocus::Filter {
1562                            self.apply_filter_operation(|f| f.push(c));
1563                        }
1564                    } else if self.current_service == Service::EcrRepositories
1565                        && self.ecr_state.current_repository.is_none()
1566                    {
1567                        if self.ecr_state.input_focus == InputFocus::Filter {
1568                            self.apply_filter_operation(|f| f.push(c));
1569                        }
1570                    } else if self.current_service == Service::IamRoles
1571                        && self.iam_state.current_role.is_none()
1572                    {
1573                        if self.iam_state.role_input_focus == InputFocus::Filter {
1574                            self.apply_filter_operation(|f| f.push(c));
1575                        }
1576                    } else if self.view_mode == ViewMode::PolicyView {
1577                        if self.iam_state.policy_input_focus == InputFocus::Filter {
1578                            self.apply_filter_operation(|f| f.push(c));
1579                        }
1580                    } else if self.current_service == Service::ApiGatewayApis
1581                        && self.apig_state.current_api.is_some()
1582                        && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
1583                    {
1584                        if self.apig_state.input_focus == InputFocus::Filter {
1585                            self.apply_filter_operation(|f| f.push(c));
1586                        }
1587                    } else if self.current_service == Service::LambdaFunctions
1588                        && self.lambda_state.current_version.is_some()
1589                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
1590                    {
1591                        if self.lambda_state.alias_input_focus == InputFocus::Filter {
1592                            self.apply_filter_operation(|f| f.push(c));
1593                        }
1594                    } else if self.current_service == Service::LambdaFunctions
1595                        && self.lambda_state.current_function.is_some()
1596                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
1597                    {
1598                        if self.lambda_state.version_input_focus == InputFocus::Filter {
1599                            self.apply_filter_operation(|f| f.push(c));
1600                        }
1601                    } else if self.current_service == Service::LambdaFunctions
1602                        && self.lambda_state.current_function.is_some()
1603                        && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
1604                    {
1605                        if self.lambda_state.alias_input_focus == InputFocus::Filter {
1606                            self.apply_filter_operation(|f| f.push(c));
1607                        }
1608                    } else if self.current_service == Service::SqsQueues
1609                        && self.sqs_state.current_queue.is_some()
1610                        && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1611                            || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1612                            || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1613                            || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1614                            || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1615                    {
1616                        if self.sqs_state.input_focus == InputFocus::Filter {
1617                            self.apply_filter_operation(|f| f.push(c));
1618                        }
1619                    } else if self.current_service == Service::Ec2Instances
1620                        && self.ec2_state.current_instance.is_some()
1621                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1622                    {
1623                        if self.ec2_state.input_focus == InputFocus::Filter {
1624                            self.ec2_state.tags.filter.push(c);
1625                            self.ec2_state.tags.selected = 0;
1626                        }
1627                    } else if self.current_service == Service::CloudWatchLogGroups {
1628                        if self.log_groups_state.input_focus == InputFocus::Filter {
1629                            self.apply_filter_operation(|f| f.push(c));
1630                        }
1631                    } else {
1632                        self.apply_filter_operation(|f| f.push(c));
1633                    }
1634                } else if self.mode == Mode::EventFilterInput {
1635                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1636                        self.log_groups_state.event_filter.push(c);
1637                    } else if c.is_ascii_digit() {
1638                        self.log_groups_state.relative_amount.push(c);
1639                    }
1640                } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1641                    self.page_input.push(c);
1642                }
1643            }
1644            Action::FilterBackspace => {
1645                if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1646                    self.service_picker.filter.pop();
1647                    self.service_picker.selected = 0;
1648                } else if self.mode == Mode::TabPicker {
1649                    self.tab_filter.pop();
1650                    self.tab_picker_selected = 0;
1651                } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1652                    self.region_filter.pop();
1653                    self.region_picker_selected = 0;
1654                } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1655                    self.profile_filter.pop();
1656                    self.profile_picker_selected = 0;
1657                } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1658                    self.session_filter.pop();
1659                    self.session_picker_selected = 0;
1660                } else if self.mode == Mode::InsightsInput {
1661                    match self.insights_state.insights.insights_focus {
1662                        InsightsFocus::Query => {
1663                            self.insights_state.insights.query_text.pop();
1664                        }
1665                        InsightsFocus::LogGroupSearch => {
1666                            self.insights_state.insights.log_group_search.pop();
1667                            // Update matches
1668                            if !self.insights_state.insights.log_group_search.is_empty() {
1669                                self.insights_state.insights.log_group_matches = self
1670                                    .log_groups_state
1671                                    .log_groups
1672                                    .items
1673                                    .iter()
1674                                    .filter(|g| {
1675                                        g.name.to_lowercase().contains(
1676                                            &self
1677                                                .insights_state
1678                                                .insights
1679                                                .log_group_search
1680                                                .to_lowercase(),
1681                                        )
1682                                    })
1683                                    .take(50)
1684                                    .map(|g| g.name.clone())
1685                                    .collect();
1686                                self.insights_state.insights.show_dropdown = true;
1687                            } else {
1688                                self.insights_state.insights.log_group_matches.clear();
1689                                self.insights_state.insights.show_dropdown = false;
1690                            }
1691                        }
1692                        _ => {}
1693                    }
1694                } else if self.mode == Mode::FilterInput {
1695                    // Only allow backspace when focus is on the input field
1696                    if self.current_service == Service::CloudFormationStacks {
1697                        if self.cfn_state.current_stack.is_some()
1698                            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1699                        {
1700                            if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1701                                self.cfn_state.parameters.filter.pop();
1702                                self.cfn_state.parameters.selected = 0;
1703                            }
1704                        } else if self.cfn_state.current_stack.is_some()
1705                            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1706                        {
1707                            if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1708                                self.cfn_state.outputs.filter.pop();
1709                                self.cfn_state.outputs.selected = 0;
1710                            }
1711                        } else if self.cfn_state.current_stack.is_some()
1712                            && self.cfn_state.detail_tab == CfnDetailTab::Resources
1713                        {
1714                            if self.cfn_state.resources_input_focus == InputFocus::Filter {
1715                                self.cfn_state.resources.filter.pop();
1716                                self.cfn_state.resources.selected = 0;
1717                            }
1718                        } else if self.cfn_state.input_focus == InputFocus::Filter {
1719                            self.apply_filter_operation(|f| {
1720                                f.pop();
1721                            });
1722                        }
1723                    } else if self.current_service == Service::Ec2Instances
1724                        && self.ec2_state.current_instance.is_some()
1725                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1726                    {
1727                        if self.ec2_state.input_focus == InputFocus::Filter {
1728                            self.ec2_state.tags.filter.pop();
1729                            self.ec2_state.tags.selected = 0;
1730                        }
1731                    } else if self.current_service == Service::CloudWatchLogGroups {
1732                        if self.log_groups_state.input_focus == InputFocus::Filter {
1733                            self.apply_filter_operation(|f| {
1734                                f.pop();
1735                            });
1736                        }
1737                    } else {
1738                        self.apply_filter_operation(|f| {
1739                            f.pop();
1740                        });
1741                    }
1742                } else if self.mode == Mode::EventFilterInput {
1743                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1744                        self.log_groups_state.event_filter.pop();
1745                    } else {
1746                        self.log_groups_state.relative_amount.pop();
1747                    }
1748                }
1749            }
1750            Action::DeleteWord => {
1751                let text = if self.mode == Mode::ServicePicker {
1752                    &mut self.service_picker.filter
1753                } else if self.mode == Mode::InsightsInput {
1754                    use crate::app::InsightsFocus;
1755                    match self.insights_state.insights.insights_focus {
1756                        InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1757                        InsightsFocus::LogGroupSearch => {
1758                            &mut self.insights_state.insights.log_group_search
1759                        }
1760                        _ => return,
1761                    }
1762                } else if self.mode == Mode::FilterInput {
1763                    if let Some(filter) = self.get_active_filter_mut() {
1764                        filter
1765                    } else {
1766                        return;
1767                    }
1768                } else if self.mode == Mode::EventFilterInput {
1769                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1770                        &mut self.log_groups_state.event_filter
1771                    } else {
1772                        &mut self.log_groups_state.relative_amount
1773                    }
1774                } else {
1775                    return;
1776                };
1777
1778                if text.is_empty() {
1779                    return;
1780                }
1781
1782                let mut chars: Vec<char> = text.chars().collect();
1783                while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1784                    chars.pop();
1785                }
1786                while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1787                    chars.pop();
1788                }
1789                *text = chars.into_iter().collect();
1790            }
1791            Action::WordLeft => {
1792                // Not implemented - would need cursor position tracking
1793            }
1794            Action::WordRight => {
1795                // Not implemented - would need cursor position tracking
1796            }
1797            Action::OpenColumnSelector => {
1798                // Don't allow opening preferences in Template or GitSync tabs
1799                if self.current_service == Service::CloudFormationStacks
1800                    && self.cfn_state.current_stack.is_some()
1801                    && (self.cfn_state.detail_tab == CfnDetailTab::Template
1802                        || self.cfn_state.detail_tab == CfnDetailTab::GitSync)
1803                {
1804                    return;
1805                }
1806
1807                // Don't allow opening preferences for IAM user tabs without preferences
1808                if self.current_service == Service::IamUsers
1809                    && self.iam_state.current_user.is_some()
1810                    && self.iam_state.user_tab == UserTab::SecurityCredentials
1811                {
1812                    return;
1813                }
1814
1815                // Don't allow opening preferences for IAM role tabs without preferences
1816                if self.current_service == Service::IamRoles
1817                    && self.iam_state.current_role.is_some()
1818                    && (self.iam_state.role_tab == RoleTab::TrustRelationships
1819                        || self.iam_state.role_tab == RoleTab::RevokeSessions)
1820                {
1821                    return;
1822                }
1823
1824                // Don't allow opening preferences for certain SQS tabs
1825                if self.current_service == Service::SqsQueues
1826                    && self.sqs_state.current_queue.is_some()
1827                    && matches!(
1828                        self.sqs_state.detail_tab,
1829                        SqsQueueDetailTab::QueuePolicies
1830                            | SqsQueueDetailTab::Monitoring
1831                            | SqsQueueDetailTab::DeadLetterQueue
1832                            | SqsQueueDetailTab::Encryption
1833                            | SqsQueueDetailTab::DeadLetterQueueRedriveTasks
1834                    )
1835                {
1836                    return;
1837                }
1838
1839                // Don't allow opening preferences for EC2 instance detail tabs except Tags
1840                if self.current_service == Service::Ec2Instances
1841                    && self.ec2_state.table.expanded_item.is_some()
1842                    && self.ec2_state.detail_tab != Ec2DetailTab::Tags
1843                {
1844                    return;
1845                }
1846
1847                // Don't allow opening preferences for CloudTrail Event JSON
1848                if self.current_service == Service::CloudTrailEvents
1849                    && self.cloudtrail_state.current_event.is_some()
1850                    && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
1851                {
1852                    return;
1853                }
1854
1855                // If we have page input, apply it instead of opening column selector
1856                if !self.page_input.is_empty() {
1857                    if let Ok(page) = self.page_input.parse::<usize>() {
1858                        self.go_to_page(page);
1859                    }
1860                    self.page_input.clear();
1861                } else {
1862                    self.mode = Mode::ColumnSelector;
1863                    self.column_selector_index = 0;
1864                }
1865            }
1866            Action::ToggleColumn => {
1867                if self.current_service == Service::S3Buckets
1868                    && self.s3_state.current_bucket.is_none()
1869                {
1870                    let idx = self.column_selector_index;
1871                    if idx > 0 && idx <= self.s3_bucket_column_ids.len() {
1872                        if let Some(col) = self.s3_bucket_column_ids.get(idx - 1) {
1873                            if let Some(pos) = self
1874                                .s3_bucket_visible_column_ids
1875                                .iter()
1876                                .position(|c| c == col)
1877                            {
1878                                self.s3_bucket_visible_column_ids.remove(pos);
1879                            } else {
1880                                self.s3_bucket_visible_column_ids.push(*col);
1881                            }
1882                        }
1883                    } else if idx == self.s3_bucket_column_ids.len() + 3 {
1884                        self.s3_state.buckets.page_size = PageSize::Ten;
1885                    } else if idx == self.s3_bucket_column_ids.len() + 4 {
1886                        self.s3_state.buckets.page_size = PageSize::TwentyFive;
1887                    } else if idx == self.s3_bucket_column_ids.len() + 5 {
1888                        self.s3_state.buckets.page_size = PageSize::Fifty;
1889                    } else if idx == self.s3_bucket_column_ids.len() + 6 {
1890                        self.s3_state.buckets.page_size = PageSize::OneHundred;
1891                    }
1892                } else if self.current_service == Service::CloudWatchAlarms {
1893                    // Map flat list index to actual item
1894                    // 0: Columns header, 1-16: columns, 17: empty, 18: ViewAs header, 19-20: view options
1895                    // 21: empty, 22: PageSize header, 23-25: page sizes, 26: empty, 27: WrapLines header, 28: wrap option
1896                    let idx = self.column_selector_index;
1897                    if (1..=16).contains(&idx) {
1898                        // Column toggle
1899                        if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1900                            if let Some(pos) = self
1901                                .cw_alarm_visible_column_ids
1902                                .iter()
1903                                .position(|c| c == col)
1904                            {
1905                                self.cw_alarm_visible_column_ids.remove(pos);
1906                            } else {
1907                                self.cw_alarm_visible_column_ids.push(*col);
1908                            }
1909                        }
1910                    } else if idx == 19 {
1911                        self.alarms_state.view_as = AlarmViewMode::Table;
1912                    } else if idx == 20 {
1913                        self.alarms_state.view_as = AlarmViewMode::Cards;
1914                    } else if idx == 23 {
1915                        self.alarms_state.table.page_size = PageSize::Ten;
1916                    } else if idx == 24 {
1917                        self.alarms_state.table.page_size = PageSize::TwentyFive;
1918                    } else if idx == 25 {
1919                        self.alarms_state.table.page_size = PageSize::Fifty;
1920                    } else if idx == 26 {
1921                        self.alarms_state.table.page_size = PageSize::OneHundred;
1922                    } else if idx == 29 {
1923                        self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1924                    }
1925                } else if self.current_service == Service::CloudTrailEvents {
1926                    if self.cloudtrail_state.current_event.is_some()
1927                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
1928                    {
1929                        // Resources table in detail view
1930                        let idx = self.column_selector_index;
1931                        if idx > 0 && idx <= self.cloudtrail_resource_column_ids.len() {
1932                            if let Some(col) = self.cloudtrail_resource_column_ids.get(idx - 1) {
1933                                if let Some(pos) = self
1934                                    .cloudtrail_resource_visible_column_ids
1935                                    .iter()
1936                                    .position(|c| c == col)
1937                                {
1938                                    // Don't allow toggling off if it's the last visible column
1939                                    if self.cloudtrail_resource_visible_column_ids.len() > 1 {
1940                                        self.cloudtrail_resource_visible_column_ids.remove(pos);
1941                                    }
1942                                } else {
1943                                    self.cloudtrail_resource_visible_column_ids.push(*col);
1944                                }
1945                            }
1946                        }
1947                    } else {
1948                        // Main events table
1949                        let idx = self.column_selector_index;
1950                        if (1..=14).contains(&idx) {
1951                            if let Some(col) = self.cloudtrail_event_column_ids.get(idx - 1) {
1952                                if let Some(pos) = self
1953                                    .cloudtrail_event_visible_column_ids
1954                                    .iter()
1955                                    .position(|c| c == col)
1956                                {
1957                                    self.cloudtrail_event_visible_column_ids.remove(pos);
1958                                } else {
1959                                    self.cloudtrail_event_visible_column_ids.push(*col);
1960                                }
1961                            }
1962                        } else if idx == 17 {
1963                            self.cloudtrail_state.table.page_size = PageSize::Ten;
1964                            self.cloudtrail_state.table.snap_to_page();
1965                        } else if idx == 18 {
1966                            self.cloudtrail_state.table.page_size = PageSize::TwentyFive;
1967                            self.cloudtrail_state.table.snap_to_page();
1968                        } else if idx == 19 {
1969                            self.cloudtrail_state.table.page_size = PageSize::Fifty;
1970                            self.cloudtrail_state.table.snap_to_page();
1971                        } else if idx == 20 {
1972                            self.cloudtrail_state.table.page_size = PageSize::OneHundred;
1973                            self.cloudtrail_state.table.snap_to_page();
1974                        }
1975                    }
1976                } else if self.current_service == Service::ApiGatewayApis {
1977                    if let Some(api) = &self.apig_state.current_api {
1978                        use crate::ui::apig::ApiDetailTab;
1979                        if self.apig_state.detail_tab == ApiDetailTab::Routes {
1980                            // Check if REST API (shows resources) or HTTP/WebSocket (shows routes)
1981                            if api.protocol_type.to_uppercase() == "REST" {
1982                                // REST API - toggle resource columns (skip first column - it's locked)
1983                                let idx = self.column_selector_index;
1984                                if idx > 1 && idx <= self.apig_resource_column_ids.len() {
1985                                    if let Some(col) = self.apig_resource_column_ids.get(idx - 1) {
1986                                        if let Some(pos) = self
1987                                            .apig_resource_visible_column_ids
1988                                            .iter()
1989                                            .position(|c| c == col)
1990                                        {
1991                                            self.apig_resource_visible_column_ids.remove(pos);
1992                                        } else {
1993                                            self.apig_resource_visible_column_ids.push(*col);
1994                                        }
1995                                    }
1996                                }
1997                            } else {
1998                                // HTTP/WebSocket API - toggle route columns (skip first column - it's locked)
1999                                let idx = self.column_selector_index;
2000                                if idx > 1 && idx <= self.apig_route_column_ids.len() {
2001                                    if let Some(col) = self.apig_route_column_ids.get(idx - 1) {
2002                                        if let Some(pos) = self
2003                                            .apig_route_visible_column_ids
2004                                            .iter()
2005                                            .position(|c| c == col)
2006                                        {
2007                                            self.apig_route_visible_column_ids.remove(pos);
2008                                        } else {
2009                                            self.apig_route_visible_column_ids.push(*col);
2010                                        }
2011                                    }
2012                                }
2013                            }
2014                            // idx == 1 is the locked first column, do nothing
2015                        }
2016                    } else {
2017                        // API list view - toggle API columns
2018                        let idx = self.column_selector_index;
2019                        if idx > 0 && idx <= self.apig_api_column_ids.len() {
2020                            if let Some(col) = self.apig_api_column_ids.get(idx - 1) {
2021                                if let Some(pos) = self
2022                                    .apig_api_visible_column_ids
2023                                    .iter()
2024                                    .position(|c| c == col)
2025                                {
2026                                    self.apig_api_visible_column_ids.remove(pos);
2027                                } else {
2028                                    self.apig_api_visible_column_ids.push(*col);
2029                                }
2030                            }
2031                        } else if idx == self.apig_api_column_ids.len() + 3 {
2032                            self.apig_state.apis.page_size = PageSize::Ten;
2033                        } else if idx == self.apig_api_column_ids.len() + 4 {
2034                            self.apig_state.apis.page_size = PageSize::TwentyFive;
2035                        } else if idx == self.apig_api_column_ids.len() + 5 {
2036                            self.apig_state.apis.page_size = PageSize::Fifty;
2037                        } else if idx == self.apig_api_column_ids.len() + 6 {
2038                            self.apig_state.apis.page_size = PageSize::OneHundred;
2039                        }
2040                    }
2041                } else if self.current_service == Service::EcrRepositories {
2042                    if self.ecr_state.current_repository.is_some() {
2043                        // Images view - columns + page size
2044                        let idx = self.column_selector_index;
2045                        if let Some(col) = self.ecr_image_column_ids.get(idx) {
2046                            if let Some(pos) = self
2047                                .ecr_image_visible_column_ids
2048                                .iter()
2049                                .position(|c| c == col)
2050                            {
2051                                self.ecr_image_visible_column_ids.remove(pos);
2052                            } else {
2053                                self.ecr_image_visible_column_ids.push(*col);
2054                            }
2055                        }
2056                    } else {
2057                        // Repositories view - columns + page size
2058                        let idx = self.column_selector_index;
2059                        if idx > 0 && idx <= self.ecr_repo_column_ids.len() {
2060                            if let Some(col) = self.ecr_repo_column_ids.get(idx - 1) {
2061                                if let Some(pos) = self
2062                                    .ecr_repo_visible_column_ids
2063                                    .iter()
2064                                    .position(|c| c == col)
2065                                {
2066                                    self.ecr_repo_visible_column_ids.remove(pos);
2067                                } else {
2068                                    self.ecr_repo_visible_column_ids.push(*col);
2069                                }
2070                            }
2071                        } else if idx == self.ecr_repo_column_ids.len() + 3 {
2072                            self.ecr_state.repositories.page_size = PageSize::Ten;
2073                        } else if idx == self.ecr_repo_column_ids.len() + 4 {
2074                            self.ecr_state.repositories.page_size = PageSize::TwentyFive;
2075                        } else if idx == self.ecr_repo_column_ids.len() + 5 {
2076                            self.ecr_state.repositories.page_size = PageSize::Fifty;
2077                        } else if idx == self.ecr_repo_column_ids.len() + 6 {
2078                            self.ecr_state.repositories.page_size = PageSize::OneHundred;
2079                        }
2080                    }
2081                } else if self.current_service == Service::Ec2Instances {
2082                    if self.ec2_state.current_instance.is_some()
2083                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
2084                    {
2085                        let idx = self.column_selector_index;
2086                        if idx > 0 && idx <= self.ec2_state.tag_column_ids.len() {
2087                            if let Some(col) = self.ec2_state.tag_column_ids.get(idx - 1) {
2088                                if let Some(pos) = self
2089                                    .ec2_state
2090                                    .tag_visible_column_ids
2091                                    .iter()
2092                                    .position(|c| c == col)
2093                                {
2094                                    self.ec2_state.tag_visible_column_ids.remove(pos);
2095                                } else {
2096                                    self.ec2_state.tag_visible_column_ids.push(col.clone());
2097                                }
2098                            }
2099                        } else if idx == self.ec2_state.tag_column_ids.len() + 3 {
2100                            self.ec2_state.tags.page_size = PageSize::Ten;
2101                        } else if idx == self.ec2_state.tag_column_ids.len() + 4 {
2102                            self.ec2_state.tags.page_size = PageSize::TwentyFive;
2103                        } else if idx == self.ec2_state.tag_column_ids.len() + 5 {
2104                            self.ec2_state.tags.page_size = PageSize::Fifty;
2105                        } else if idx == self.ec2_state.tag_column_ids.len() + 6 {
2106                            self.ec2_state.tags.page_size = PageSize::OneHundred;
2107                        }
2108                    } else {
2109                        let idx = self.column_selector_index;
2110                        if idx > 0 && idx <= self.ec2_column_ids.len() {
2111                            if let Some(col) = self.ec2_column_ids.get(idx - 1) {
2112                                if let Some(pos) =
2113                                    self.ec2_visible_column_ids.iter().position(|c| c == col)
2114                                {
2115                                    self.ec2_visible_column_ids.remove(pos);
2116                                } else {
2117                                    self.ec2_visible_column_ids.push(*col);
2118                                }
2119                            }
2120                        } else if idx == self.ec2_column_ids.len() + 3 {
2121                            self.ec2_state.table.page_size = PageSize::Ten;
2122                        } else if idx == self.ec2_column_ids.len() + 4 {
2123                            self.ec2_state.table.page_size = PageSize::TwentyFive;
2124                        } else if idx == self.ec2_column_ids.len() + 5 {
2125                            self.ec2_state.table.page_size = PageSize::Fifty;
2126                        } else if idx == self.ec2_column_ids.len() + 6 {
2127                            self.ec2_state.table.page_size = PageSize::OneHundred;
2128                        }
2129                    }
2130                } else if self.current_service == Service::SqsQueues {
2131                    if self.sqs_state.current_queue.is_some()
2132                        && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2133                    {
2134                        // Triggers tab - columns + page size
2135                        let idx = self.column_selector_index;
2136                        if idx > 0 && idx <= self.sqs_state.trigger_column_ids.len() {
2137                            if let Some(col) = self.sqs_state.trigger_column_ids.get(idx - 1) {
2138                                if let Some(pos) = self
2139                                    .sqs_state
2140                                    .trigger_visible_column_ids
2141                                    .iter()
2142                                    .position(|c| c == col)
2143                                {
2144                                    self.sqs_state.trigger_visible_column_ids.remove(pos);
2145                                } else {
2146                                    self.sqs_state.trigger_visible_column_ids.push(col.clone());
2147                                }
2148                            }
2149                        } else if idx == self.sqs_state.trigger_column_ids.len() + 3 {
2150                            self.sqs_state.triggers.page_size = PageSize::Ten;
2151                        } else if idx == self.sqs_state.trigger_column_ids.len() + 4 {
2152                            self.sqs_state.triggers.page_size = PageSize::TwentyFive;
2153                        } else if idx == self.sqs_state.trigger_column_ids.len() + 5 {
2154                            self.sqs_state.triggers.page_size = PageSize::Fifty;
2155                        } else if idx == self.sqs_state.trigger_column_ids.len() + 6 {
2156                            self.sqs_state.triggers.page_size = PageSize::OneHundred;
2157                        }
2158                    } else if self.sqs_state.current_queue.is_some()
2159                        && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2160                    {
2161                        // Pipes tab - columns + page size
2162                        let idx = self.column_selector_index;
2163                        if idx > 0 && idx <= self.sqs_state.pipe_column_ids.len() {
2164                            if let Some(col) = self.sqs_state.pipe_column_ids.get(idx - 1) {
2165                                if let Some(pos) = self
2166                                    .sqs_state
2167                                    .pipe_visible_column_ids
2168                                    .iter()
2169                                    .position(|c| c == col)
2170                                {
2171                                    self.sqs_state.pipe_visible_column_ids.remove(pos);
2172                                } else {
2173                                    self.sqs_state.pipe_visible_column_ids.push(col.clone());
2174                                }
2175                            }
2176                        } else if idx == self.sqs_state.pipe_column_ids.len() + 3 {
2177                            self.sqs_state.pipes.page_size = PageSize::Ten;
2178                        } else if idx == self.sqs_state.pipe_column_ids.len() + 4 {
2179                            self.sqs_state.pipes.page_size = PageSize::TwentyFive;
2180                        } else if idx == self.sqs_state.pipe_column_ids.len() + 5 {
2181                            self.sqs_state.pipes.page_size = PageSize::Fifty;
2182                        } else if idx == self.sqs_state.pipe_column_ids.len() + 6 {
2183                            self.sqs_state.pipes.page_size = PageSize::OneHundred;
2184                        }
2185                    } else if self.sqs_state.current_queue.is_some()
2186                        && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2187                    {
2188                        // Tags tab - columns + page size
2189                        let idx = self.column_selector_index;
2190                        if idx > 0 && idx <= self.sqs_state.tag_column_ids.len() {
2191                            if let Some(col) = self.sqs_state.tag_column_ids.get(idx - 1) {
2192                                if let Some(pos) = self
2193                                    .sqs_state
2194                                    .tag_visible_column_ids
2195                                    .iter()
2196                                    .position(|c| c == col)
2197                                {
2198                                    self.sqs_state.tag_visible_column_ids.remove(pos);
2199                                } else {
2200                                    self.sqs_state.tag_visible_column_ids.push(col.clone());
2201                                }
2202                            }
2203                        } else if idx == self.sqs_state.tag_column_ids.len() + 3 {
2204                            self.sqs_state.tags.page_size = PageSize::Ten;
2205                        } else if idx == self.sqs_state.tag_column_ids.len() + 4 {
2206                            self.sqs_state.tags.page_size = PageSize::TwentyFive;
2207                        } else if idx == self.sqs_state.tag_column_ids.len() + 5 {
2208                            self.sqs_state.tags.page_size = PageSize::Fifty;
2209                        } else if idx == self.sqs_state.tag_column_ids.len() + 6 {
2210                            self.sqs_state.tags.page_size = PageSize::OneHundred;
2211                        }
2212                    } else if self.sqs_state.current_queue.is_some()
2213                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2214                    {
2215                        // Subscriptions tab - columns + page size
2216                        let idx = self.column_selector_index;
2217                        if idx > 0 && idx <= self.sqs_state.subscription_column_ids.len() {
2218                            if let Some(col) = self.sqs_state.subscription_column_ids.get(idx - 1) {
2219                                if let Some(pos) = self
2220                                    .sqs_state
2221                                    .subscription_visible_column_ids
2222                                    .iter()
2223                                    .position(|c| c == col)
2224                                {
2225                                    self.sqs_state.subscription_visible_column_ids.remove(pos);
2226                                } else {
2227                                    self.sqs_state
2228                                        .subscription_visible_column_ids
2229                                        .push(col.clone());
2230                                }
2231                            }
2232                        } else if idx == self.sqs_state.subscription_column_ids.len() + 3 {
2233                            self.sqs_state.subscriptions.page_size = PageSize::Ten;
2234                        } else if idx == self.sqs_state.subscription_column_ids.len() + 4 {
2235                            self.sqs_state.subscriptions.page_size = PageSize::TwentyFive;
2236                        } else if idx == self.sqs_state.subscription_column_ids.len() + 5 {
2237                            self.sqs_state.subscriptions.page_size = PageSize::Fifty;
2238                        } else if idx == self.sqs_state.subscription_column_ids.len() + 6 {
2239                            self.sqs_state.subscriptions.page_size = PageSize::OneHundred;
2240                        }
2241                    } else {
2242                        // Queues list - columns + page size
2243                        let idx = self.column_selector_index;
2244                        if let Some(col) = self.sqs_column_ids.get(idx) {
2245                            if let Some(pos) =
2246                                self.sqs_visible_column_ids.iter().position(|c| c == col)
2247                            {
2248                                self.sqs_visible_column_ids.remove(pos);
2249                            } else {
2250                                self.sqs_visible_column_ids.push(*col);
2251                            }
2252                        } else if idx == self.sqs_column_ids.len() + 2 {
2253                            self.sqs_state.queues.page_size = PageSize::Ten;
2254                        } else if idx == self.sqs_column_ids.len() + 3 {
2255                            self.sqs_state.queues.page_size = PageSize::TwentyFive;
2256                        } else if idx == self.sqs_column_ids.len() + 4 {
2257                            self.sqs_state.queues.page_size = PageSize::Fifty;
2258                        } else if idx == self.sqs_column_ids.len() + 5 {
2259                            self.sqs_state.queues.page_size = PageSize::OneHundred;
2260                        }
2261                    }
2262                } else if self.current_service == Service::LambdaFunctions {
2263                    let idx = self.column_selector_index;
2264                    // Check if we're in Versions tab
2265                    if self.lambda_state.current_function.is_some()
2266                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
2267                    {
2268                        // Version columns
2269                        if idx > 0 && idx <= self.lambda_state.version_column_ids.len() {
2270                            if let Some(col) = self.lambda_state.version_column_ids.get(idx - 1) {
2271                                if let Some(pos) = self
2272                                    .lambda_state
2273                                    .version_visible_column_ids
2274                                    .iter()
2275                                    .position(|c| *c == *col)
2276                                {
2277                                    self.lambda_state.version_visible_column_ids.remove(pos);
2278                                } else {
2279                                    self.lambda_state
2280                                        .version_visible_column_ids
2281                                        .push(col.clone());
2282                                }
2283                            }
2284                        } else if idx == self.lambda_state.version_column_ids.len() + 3 {
2285                            self.lambda_state.version_table.page_size = PageSize::Ten;
2286                        } else if idx == self.lambda_state.version_column_ids.len() + 4 {
2287                            self.lambda_state.version_table.page_size = PageSize::TwentyFive;
2288                        } else if idx == self.lambda_state.version_column_ids.len() + 5 {
2289                            self.lambda_state.version_table.page_size = PageSize::Fifty;
2290                        } else if idx == self.lambda_state.version_column_ids.len() + 6 {
2291                            self.lambda_state.version_table.page_size = PageSize::OneHundred;
2292                        }
2293                    } else if (self.lambda_state.current_function.is_some()
2294                        && self.lambda_state.detail_tab == LambdaDetailTab::Aliases)
2295                        || (self.lambda_state.current_version.is_some()
2296                            && self.lambda_state.detail_tab == LambdaDetailTab::Configuration)
2297                    {
2298                        // Alias columns
2299                        if idx > 0 && idx <= self.lambda_state.alias_column_ids.len() {
2300                            if let Some(col) = self.lambda_state.alias_column_ids.get(idx - 1) {
2301                                if let Some(pos) = self
2302                                    .lambda_state
2303                                    .alias_visible_column_ids
2304                                    .iter()
2305                                    .position(|c| *c == *col)
2306                                {
2307                                    self.lambda_state.alias_visible_column_ids.remove(pos);
2308                                } else {
2309                                    self.lambda_state.alias_visible_column_ids.push(col.clone());
2310                                }
2311                            }
2312                        } else if idx == self.lambda_state.alias_column_ids.len() + 3 {
2313                            self.lambda_state.alias_table.page_size = PageSize::Ten;
2314                        } else if idx == self.lambda_state.alias_column_ids.len() + 4 {
2315                            self.lambda_state.alias_table.page_size = PageSize::TwentyFive;
2316                        } else if idx == self.lambda_state.alias_column_ids.len() + 5 {
2317                            self.lambda_state.alias_table.page_size = PageSize::Fifty;
2318                        } else if idx == self.lambda_state.alias_column_ids.len() + 6 {
2319                            self.lambda_state.alias_table.page_size = PageSize::OneHundred;
2320                        }
2321                    } else {
2322                        // Function columns
2323                        if idx > 0 && idx <= self.lambda_state.function_column_ids.len() {
2324                            if let Some(col) = self.lambda_state.function_column_ids.get(idx - 1) {
2325                                if let Some(pos) = self
2326                                    .lambda_state
2327                                    .function_visible_column_ids
2328                                    .iter()
2329                                    .position(|c| *c == *col)
2330                                {
2331                                    self.lambda_state.function_visible_column_ids.remove(pos);
2332                                } else {
2333                                    self.lambda_state.function_visible_column_ids.push(*col);
2334                                }
2335                            }
2336                        } else if idx == self.lambda_state.function_column_ids.len() + 3 {
2337                            self.lambda_state.table.page_size = PageSize::Ten;
2338                        } else if idx == self.lambda_state.function_column_ids.len() + 4 {
2339                            self.lambda_state.table.page_size = PageSize::TwentyFive;
2340                        } else if idx == self.lambda_state.function_column_ids.len() + 5 {
2341                            self.lambda_state.table.page_size = PageSize::Fifty;
2342                        } else if idx == self.lambda_state.function_column_ids.len() + 6 {
2343                            self.lambda_state.table.page_size = PageSize::OneHundred;
2344                        }
2345                    }
2346                } else if self.current_service == Service::LambdaApplications {
2347                    if self.lambda_application_state.current_application.is_some() {
2348                        // In detail view - handle resource or deployment columns
2349                        if self.lambda_application_state.detail_tab
2350                            == LambdaApplicationDetailTab::Overview
2351                        {
2352                            // Resources columns
2353                            let idx = self.column_selector_index;
2354                            if idx > 0 && idx <= self.lambda_resource_column_ids.len() {
2355                                if let Some(col) = self.lambda_resource_column_ids.get(idx - 1) {
2356                                    if let Some(pos) = self
2357                                        .lambda_resource_visible_column_ids
2358                                        .iter()
2359                                        .position(|c| c == col)
2360                                    {
2361                                        self.lambda_resource_visible_column_ids.remove(pos);
2362                                    } else {
2363                                        self.lambda_resource_visible_column_ids.push(*col);
2364                                    }
2365                                }
2366                            } else if idx == self.lambda_resource_column_ids.len() + 3 {
2367                                self.lambda_application_state.resources.page_size = PageSize::Ten;
2368                            } else if idx == self.lambda_resource_column_ids.len() + 4 {
2369                                self.lambda_application_state.resources.page_size =
2370                                    PageSize::TwentyFive;
2371                            } else if idx == self.lambda_resource_column_ids.len() + 5 {
2372                                self.lambda_application_state.resources.page_size = PageSize::Fifty;
2373                            }
2374                        } else {
2375                            // Deployments columns
2376                            let idx = self.column_selector_index;
2377                            if idx > 0 && idx <= self.lambda_deployment_column_ids.len() {
2378                                if let Some(col) = self.lambda_deployment_column_ids.get(idx - 1) {
2379                                    if let Some(pos) = self
2380                                        .lambda_deployment_visible_column_ids
2381                                        .iter()
2382                                        .position(|c| c == col)
2383                                    {
2384                                        self.lambda_deployment_visible_column_ids.remove(pos);
2385                                    } else {
2386                                        self.lambda_deployment_visible_column_ids.push(*col);
2387                                    }
2388                                }
2389                            } else if idx == self.lambda_deployment_column_ids.len() + 3 {
2390                                self.lambda_application_state.deployments.page_size = PageSize::Ten;
2391                            } else if idx == self.lambda_deployment_column_ids.len() + 4 {
2392                                self.lambda_application_state.deployments.page_size =
2393                                    PageSize::TwentyFive;
2394                            } else if idx == self.lambda_deployment_column_ids.len() + 5 {
2395                                self.lambda_application_state.deployments.page_size =
2396                                    PageSize::Fifty;
2397                            }
2398                        }
2399                    } else {
2400                        // In list view - handle application columns
2401                        let idx = self.column_selector_index;
2402                        if idx > 0 && idx <= self.lambda_application_column_ids.len() {
2403                            if let Some(col) = self.lambda_application_column_ids.get(idx - 1) {
2404                                if let Some(pos) = self
2405                                    .lambda_application_visible_column_ids
2406                                    .iter()
2407                                    .position(|c| *c == *col)
2408                                {
2409                                    self.lambda_application_visible_column_ids.remove(pos);
2410                                } else {
2411                                    self.lambda_application_visible_column_ids.push(*col);
2412                                }
2413                            }
2414                        } else if idx == self.lambda_application_column_ids.len() + 3 {
2415                            self.lambda_application_state.table.page_size = PageSize::Ten;
2416                        } else if idx == self.lambda_application_column_ids.len() + 4 {
2417                            self.lambda_application_state.table.page_size = PageSize::TwentyFive;
2418                        } else if idx == self.lambda_application_column_ids.len() + 5 {
2419                            self.lambda_application_state.table.page_size = PageSize::Fifty;
2420                        }
2421                    }
2422                } else if self.view_mode == ViewMode::Events {
2423                    if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
2424                    {
2425                        if let Some(pos) = self
2426                            .cw_log_event_visible_column_ids
2427                            .iter()
2428                            .position(|c| c == col)
2429                        {
2430                            self.cw_log_event_visible_column_ids.remove(pos);
2431                        } else {
2432                            self.cw_log_event_visible_column_ids.push(*col);
2433                        }
2434                    }
2435                } else if self.view_mode == ViewMode::Detail {
2436                    let idx = self.column_selector_index;
2437                    if self.log_groups_state.detail_tab == DetailTab::Tags {
2438                        // Tags tab
2439                        if idx > 0 && idx <= self.cw_log_tag_column_ids.len() {
2440                            if let Some(col) = self.cw_log_tag_column_ids.get(idx - 1) {
2441                                if let Some(pos) = self
2442                                    .cw_log_tag_visible_column_ids
2443                                    .iter()
2444                                    .position(|c| c == col)
2445                                {
2446                                    self.cw_log_tag_visible_column_ids.remove(pos);
2447                                } else {
2448                                    self.cw_log_tag_visible_column_ids.push(*col);
2449                                }
2450                            }
2451                        } else if idx == self.cw_log_tag_column_ids.len() + 3 {
2452                            self.log_groups_state.tags.page_size = PageSize::Ten;
2453                        } else if idx == self.cw_log_tag_column_ids.len() + 4 {
2454                            self.log_groups_state.tags.page_size = PageSize::TwentyFive;
2455                        } else if idx == self.cw_log_tag_column_ids.len() + 5 {
2456                            self.log_groups_state.tags.page_size = PageSize::Fifty;
2457                        } else if idx == self.cw_log_tag_column_ids.len() + 6 {
2458                            self.log_groups_state.tags.page_size = PageSize::OneHundred;
2459                        }
2460                    } else {
2461                        // Log streams tab
2462                        if idx > 0 && idx <= self.cw_log_stream_column_ids.len() {
2463                            if let Some(col) = self.cw_log_stream_column_ids.get(idx - 1) {
2464                                if let Some(pos) = self
2465                                    .cw_log_stream_visible_column_ids
2466                                    .iter()
2467                                    .position(|c| c == col)
2468                                {
2469                                    self.cw_log_stream_visible_column_ids.remove(pos);
2470                                } else {
2471                                    self.cw_log_stream_visible_column_ids.push(*col);
2472                                }
2473                            }
2474                        } else if idx == self.cw_log_stream_column_ids.len() + 3 {
2475                            self.log_groups_state.stream_page_size = 10;
2476                            self.log_groups_state.stream_current_page = 0;
2477                        } else if idx == self.cw_log_stream_column_ids.len() + 4 {
2478                            self.log_groups_state.stream_page_size = 25;
2479                            self.log_groups_state.stream_current_page = 0;
2480                        } else if idx == self.cw_log_stream_column_ids.len() + 5 {
2481                            self.log_groups_state.stream_page_size = 50;
2482                            self.log_groups_state.stream_current_page = 0;
2483                        } else if idx == self.cw_log_stream_column_ids.len() + 6 {
2484                            self.log_groups_state.stream_page_size = 100;
2485                            self.log_groups_state.stream_current_page = 0;
2486                        }
2487                    }
2488                } else if self.current_service == Service::CloudFormationStacks {
2489                    let idx = self.column_selector_index;
2490                    // Check if we're in StackInfo tab (tags)
2491                    if self.cfn_state.current_stack.is_some()
2492                        && self.cfn_state.detail_tab == CfnDetailTab::StackInfo
2493                    {
2494                        // Tags have 2 columns (Key, Value) - always visible, so only handle page size
2495                        if idx == 4 {
2496                            self.cfn_state.tags.page_size = PageSize::Ten;
2497                        } else if idx == 5 {
2498                            self.cfn_state.tags.page_size = PageSize::TwentyFive;
2499                        } else if idx == 6 {
2500                            self.cfn_state.tags.page_size = PageSize::Fifty;
2501                        } else if idx == 7 {
2502                            self.cfn_state.tags.page_size = PageSize::OneHundred;
2503                        }
2504                    } else if self.cfn_state.current_stack.is_some()
2505                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
2506                    {
2507                        if idx > 0 && idx <= self.cfn_parameter_column_ids.len() {
2508                            if let Some(col) = self.cfn_parameter_column_ids.get(idx - 1) {
2509                                if let Some(pos) = self
2510                                    .cfn_parameter_visible_column_ids
2511                                    .iter()
2512                                    .position(|c| c == col)
2513                                {
2514                                    self.cfn_parameter_visible_column_ids.remove(pos);
2515                                } else {
2516                                    self.cfn_parameter_visible_column_ids.push(col);
2517                                }
2518                            }
2519                        } else if idx == self.cfn_parameter_column_ids.len() + 3 {
2520                            self.cfn_state.parameters.page_size = PageSize::Ten;
2521                        } else if idx == self.cfn_parameter_column_ids.len() + 4 {
2522                            self.cfn_state.parameters.page_size = PageSize::TwentyFive;
2523                        } else if idx == self.cfn_parameter_column_ids.len() + 5 {
2524                            self.cfn_state.parameters.page_size = PageSize::Fifty;
2525                        } else if idx == self.cfn_parameter_column_ids.len() + 6 {
2526                            self.cfn_state.parameters.page_size = PageSize::OneHundred;
2527                        }
2528                    } else if self.cfn_state.current_stack.is_some()
2529                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
2530                    {
2531                        if idx > 0 && idx <= self.cfn_output_column_ids.len() {
2532                            if let Some(col) = self.cfn_output_column_ids.get(idx - 1) {
2533                                if let Some(pos) = self
2534                                    .cfn_output_visible_column_ids
2535                                    .iter()
2536                                    .position(|c| c == col)
2537                                {
2538                                    self.cfn_output_visible_column_ids.remove(pos);
2539                                } else {
2540                                    self.cfn_output_visible_column_ids.push(col);
2541                                }
2542                            }
2543                        } else if idx == self.cfn_output_column_ids.len() + 3 {
2544                            self.cfn_state.outputs.page_size = PageSize::Ten;
2545                        } else if idx == self.cfn_output_column_ids.len() + 4 {
2546                            self.cfn_state.outputs.page_size = PageSize::TwentyFive;
2547                        } else if idx == self.cfn_output_column_ids.len() + 5 {
2548                            self.cfn_state.outputs.page_size = PageSize::Fifty;
2549                        } else if idx == self.cfn_output_column_ids.len() + 6 {
2550                            self.cfn_state.outputs.page_size = PageSize::OneHundred;
2551                        }
2552                    } else if self.cfn_state.current_stack.is_some()
2553                        && self.cfn_state.detail_tab == CfnDetailTab::Resources
2554                    {
2555                        if idx > 0 && idx <= self.cfn_resource_column_ids.len() {
2556                            if let Some(col) = self.cfn_resource_column_ids.get(idx - 1) {
2557                                if let Some(pos) = self
2558                                    .cfn_resource_visible_column_ids
2559                                    .iter()
2560                                    .position(|c| c == col)
2561                                {
2562                                    self.cfn_resource_visible_column_ids.remove(pos);
2563                                } else {
2564                                    self.cfn_resource_visible_column_ids.push(col);
2565                                }
2566                            }
2567                        } else if idx == self.cfn_resource_column_ids.len() + 3 {
2568                            self.cfn_state.resources.page_size = PageSize::Ten;
2569                        } else if idx == self.cfn_resource_column_ids.len() + 4 {
2570                            self.cfn_state.resources.page_size = PageSize::TwentyFive;
2571                        } else if idx == self.cfn_resource_column_ids.len() + 5 {
2572                            self.cfn_state.resources.page_size = PageSize::Fifty;
2573                        } else if idx == self.cfn_resource_column_ids.len() + 6 {
2574                            self.cfn_state.resources.page_size = PageSize::OneHundred;
2575                        }
2576                    } else if self.cfn_state.current_stack.is_none() {
2577                        // Stack list view
2578                        if idx > 0 && idx <= self.cfn_column_ids.len() {
2579                            if let Some(col) = self.cfn_column_ids.get(idx - 1) {
2580                                if let Some(pos) =
2581                                    self.cfn_visible_column_ids.iter().position(|c| c == col)
2582                                {
2583                                    self.cfn_visible_column_ids.remove(pos);
2584                                } else {
2585                                    self.cfn_visible_column_ids.push(*col);
2586                                }
2587                            }
2588                        } else if idx == self.cfn_column_ids.len() + 3 {
2589                            self.cfn_state.table.page_size = PageSize::Ten;
2590                        } else if idx == self.cfn_column_ids.len() + 4 {
2591                            self.cfn_state.table.page_size = PageSize::TwentyFive;
2592                        } else if idx == self.cfn_column_ids.len() + 5 {
2593                            self.cfn_state.table.page_size = PageSize::Fifty;
2594                        } else if idx == self.cfn_column_ids.len() + 6 {
2595                            self.cfn_state.table.page_size = PageSize::OneHundred;
2596                        }
2597                    }
2598                    // Template tab: no column toggle
2599                } else if self.current_service == Service::IamUsers {
2600                    let idx = self.column_selector_index;
2601                    if self.iam_state.current_user.is_some() {
2602                        match self.iam_state.user_tab {
2603                            UserTab::Permissions => {
2604                                // Policy columns
2605                                if idx > 0 && idx <= self.iam_policy_column_ids.len() {
2606                                    if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
2607                                        if let Some(pos) = self
2608                                            .iam_policy_visible_column_ids
2609                                            .iter()
2610                                            .position(|c| c == col)
2611                                        {
2612                                            self.iam_policy_visible_column_ids.remove(pos);
2613                                        } else {
2614                                            self.iam_policy_visible_column_ids.push(col.clone());
2615                                        }
2616                                    }
2617                                } else if idx == self.iam_policy_column_ids.len() + 3 {
2618                                    self.iam_state.policies.page_size = PageSize::Ten;
2619                                } else if idx == self.iam_policy_column_ids.len() + 4 {
2620                                    self.iam_state.policies.page_size = PageSize::TwentyFive;
2621                                } else if idx == self.iam_policy_column_ids.len() + 5 {
2622                                    self.iam_state.policies.page_size = PageSize::Fifty;
2623                                }
2624                            }
2625                            UserTab::Groups => {
2626                                toggle_iam_page_size_only(
2627                                    idx,
2628                                    5,
2629                                    &mut self.iam_state.user_group_memberships.page_size,
2630                                );
2631                            }
2632                            UserTab::Tags => {
2633                                toggle_iam_page_size_only(
2634                                    idx,
2635                                    5,
2636                                    &mut self.iam_state.user_tags.page_size,
2637                                );
2638                            }
2639                            UserTab::LastAccessed => {
2640                                toggle_iam_page_size_only(
2641                                    idx,
2642                                    6,
2643                                    &mut self.iam_state.last_accessed_services.page_size,
2644                                );
2645                            }
2646                            _ => {}
2647                        }
2648                    } else {
2649                        // User list columns
2650                        toggle_iam_preference_static(
2651                            idx,
2652                            &self.iam_user_column_ids,
2653                            &mut self.iam_user_visible_column_ids,
2654                            &mut self.iam_state.users.page_size,
2655                        );
2656                    }
2657                } else if self.current_service == Service::IamRoles {
2658                    let idx = self.column_selector_index;
2659                    if self.iam_state.current_role.is_some() {
2660                        match self.iam_state.role_tab {
2661                            RoleTab::Permissions => {
2662                                // Policy columns for role detail
2663                                toggle_iam_preference(
2664                                    idx,
2665                                    &self.iam_policy_column_ids,
2666                                    &mut self.iam_policy_visible_column_ids,
2667                                    &mut self.iam_state.policies.page_size,
2668                                );
2669                            }
2670                            RoleTab::LastAccessed => {
2671                                // 0: header, 1-3: columns, 4: empty, 5: PageSize header, 6-8: page sizes
2672                                toggle_iam_page_size_only(
2673                                    idx,
2674                                    6,
2675                                    &mut self.iam_state.last_accessed_services.page_size,
2676                                );
2677                            }
2678                            _ => {}
2679                        }
2680                    } else {
2681                        // Role list columns
2682                        toggle_iam_preference_static(
2683                            idx,
2684                            &self.iam_role_column_ids,
2685                            &mut self.iam_role_visible_column_ids,
2686                            &mut self.iam_state.roles.page_size,
2687                        );
2688                    }
2689                } else if self.current_service == Service::IamUserGroups {
2690                    toggle_iam_preference(
2691                        self.column_selector_index,
2692                        &self.iam_group_column_ids,
2693                        &mut self.iam_group_visible_column_ids,
2694                        &mut self.iam_state.groups.page_size,
2695                    );
2696                } else {
2697                    let idx = self.column_selector_index;
2698                    if idx > 0 && idx <= self.cw_log_group_column_ids.len() {
2699                        if let Some(col) = self.cw_log_group_column_ids.get(idx - 1) {
2700                            if let Some(pos) = self
2701                                .cw_log_group_visible_column_ids
2702                                .iter()
2703                                .position(|c| c == col)
2704                            {
2705                                self.cw_log_group_visible_column_ids.remove(pos);
2706                            } else {
2707                                self.cw_log_group_visible_column_ids.push(*col);
2708                            }
2709                        }
2710                    } else if idx == self.cw_log_group_column_ids.len() + 3 {
2711                        self.log_groups_state.log_groups.page_size = PageSize::Ten;
2712                    } else if idx == self.cw_log_group_column_ids.len() + 4 {
2713                        self.log_groups_state.log_groups.page_size = PageSize::TwentyFive;
2714                    } else if idx == self.cw_log_group_column_ids.len() + 5 {
2715                        self.log_groups_state.log_groups.page_size = PageSize::Fifty;
2716                    } else if idx == self.cw_log_group_column_ids.len() + 6 {
2717                        self.log_groups_state.log_groups.page_size = PageSize::OneHundred;
2718                    }
2719                }
2720            }
2721            Action::NextPreferences => {
2722                if self.current_service == Service::ApiGatewayApis {
2723                    cycle_preference_next(
2724                        &mut self.column_selector_index,
2725                        self.apig_api_column_ids.len(),
2726                    );
2727                } else if self.current_service == Service::CloudWatchAlarms {
2728                    // Jump to next section: Columns(0), ViewAs(18), PageSize(22), WrapLines(28)
2729                    if self.column_selector_index < 18 {
2730                        self.column_selector_index = 18; // ViewAs header
2731                    } else if self.column_selector_index < 22 {
2732                        self.column_selector_index = 22; // PageSize header
2733                    } else if self.column_selector_index < 28 {
2734                        self.column_selector_index = 28; // WrapLines header
2735                    } else {
2736                        self.column_selector_index = 0; // Back to Columns header
2737                    }
2738                } else if self.current_service == Service::EcrRepositories
2739                    && self.ecr_state.current_repository.is_some()
2740                {
2741                    // Images view: Columns(0), PageSize(columns.len() + 2)
2742                    let page_size_idx = self.ecr_image_column_ids.len() + 2;
2743                    if self.column_selector_index < page_size_idx {
2744                        self.column_selector_index = page_size_idx;
2745                    } else {
2746                        self.column_selector_index = 0;
2747                    }
2748                } else if self.current_service == Service::LambdaFunctions {
2749                    // Lambda: Columns(0), PageSize(columns.len() + 2)
2750                    let page_size_idx = self.lambda_state.function_column_ids.len() + 2;
2751                    if self.column_selector_index < page_size_idx {
2752                        self.column_selector_index = page_size_idx;
2753                    } else {
2754                        self.column_selector_index = 0;
2755                    }
2756                } else if self.current_service == Service::LambdaApplications {
2757                    // Lambda Applications: Columns(0), PageSize(columns.len() + 2)
2758                    let page_size_idx = self.lambda_application_column_ids.len() + 2;
2759                    if self.column_selector_index < page_size_idx {
2760                        self.column_selector_index = page_size_idx;
2761                    } else {
2762                        self.column_selector_index = 0;
2763                    }
2764                } else if self.current_service == Service::CloudFormationStacks {
2765                    // CloudFormation: Columns(0), PageSize(columns.len() + 2)
2766                    let page_size_idx = self.cfn_column_ids.len() + 2;
2767                    if self.column_selector_index < page_size_idx {
2768                        self.column_selector_index = page_size_idx;
2769                    } else {
2770                        self.column_selector_index = 0;
2771                    }
2772                } else if self.current_service == Service::CloudTrailEvents {
2773                    if self.cloudtrail_state.current_event.is_some()
2774                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2775                    {
2776                        // Resources table has no page size, just columns
2777                        self.column_selector_index = 0;
2778                    } else {
2779                        let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2780                        if self.column_selector_index < page_size_idx {
2781                            self.column_selector_index = page_size_idx;
2782                        } else {
2783                            self.column_selector_index = 0;
2784                        }
2785                    }
2786                } else if self.current_service == Service::Ec2Instances {
2787                    let page_size_idx = self.ec2_column_ids.len() + 2;
2788                    if self.column_selector_index < page_size_idx {
2789                        self.column_selector_index = page_size_idx;
2790                    } else {
2791                        self.column_selector_index = 0;
2792                    }
2793                } else if self.current_service == Service::IamUsers {
2794                    if self.iam_state.current_user.is_some() {
2795                        match self.iam_state.user_tab {
2796                            UserTab::Permissions => {
2797                                // Columns(0), PageSize(columns.len() + 2)
2798                                let page_size_idx = self.iam_policy_column_ids.len() + 2;
2799                                if self.column_selector_index < page_size_idx {
2800                                    self.column_selector_index = page_size_idx;
2801                                } else {
2802                                    self.column_selector_index = 0;
2803                                }
2804                            }
2805                            UserTab::Groups | UserTab::Tags => {
2806                                // Columns(0), PageSize(4)
2807                                if self.column_selector_index < 4 {
2808                                    self.column_selector_index = 4;
2809                                } else {
2810                                    self.column_selector_index = 0;
2811                                }
2812                            }
2813                            UserTab::LastAccessed => {
2814                                // Columns(0), PageSize(5)
2815                                if self.column_selector_index < 5 {
2816                                    self.column_selector_index = 5;
2817                                } else {
2818                                    self.column_selector_index = 0;
2819                                }
2820                            }
2821                            _ => {}
2822                        }
2823                    } else {
2824                        // User columns: Columns(0), PageSize(columns.len() + 2)
2825                        let page_size_idx = self.iam_user_column_ids.len() + 2;
2826                        if self.column_selector_index < page_size_idx {
2827                            self.column_selector_index = page_size_idx;
2828                        } else {
2829                            self.column_selector_index = 0;
2830                        }
2831                    }
2832                } else if self.current_service == Service::IamRoles {
2833                    if self.iam_state.current_role.is_some() {
2834                        match self.iam_state.role_tab {
2835                            RoleTab::Permissions => {
2836                                // Columns(0), PageSize(columns.len() + 2)
2837                                let page_size_idx = self.iam_policy_column_ids.len() + 2;
2838                                if self.column_selector_index < page_size_idx {
2839                                    self.column_selector_index = page_size_idx;
2840                                } else {
2841                                    self.column_selector_index = 0;
2842                                }
2843                            }
2844                            RoleTab::Tags => {
2845                                // Columns(0), PageSize(4)
2846                                if self.column_selector_index < 4 {
2847                                    self.column_selector_index = 4;
2848                                } else {
2849                                    self.column_selector_index = 0;
2850                                }
2851                            }
2852                            RoleTab::LastAccessed => {
2853                                // Columns(0), PageSize(5)
2854                                if self.column_selector_index < 5 {
2855                                    self.column_selector_index = 5;
2856                                } else {
2857                                    self.column_selector_index = 0;
2858                                }
2859                            }
2860                            _ => {}
2861                        }
2862                    } else {
2863                        // Role columns: Columns(0), PageSize(columns.len() + 2)
2864                        let page_size_idx = self.iam_role_column_ids.len() + 2;
2865                        if self.column_selector_index < page_size_idx {
2866                            self.column_selector_index = page_size_idx;
2867                        } else {
2868                            self.column_selector_index = 0;
2869                        }
2870                    }
2871                } else if self.current_service == Service::IamUserGroups {
2872                    // Group columns: Columns(0), PageSize(columns.len() + 2)
2873                    let page_size_idx = self.iam_group_column_ids.len() + 2;
2874                    if self.column_selector_index < page_size_idx {
2875                        self.column_selector_index = page_size_idx;
2876                    } else {
2877                        self.column_selector_index = 0;
2878                    }
2879                } else if self.current_service == Service::SqsQueues
2880                    && self.sqs_state.current_queue.is_some()
2881                    && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2882                {
2883                    // Triggers tab: Columns(0), PageSize(columns.len() + 2)
2884                    let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
2885                    if self.column_selector_index < page_size_idx {
2886                        self.column_selector_index = page_size_idx;
2887                    } else {
2888                        self.column_selector_index = 0;
2889                    }
2890                } else if self.current_service == Service::SqsQueues
2891                    && self.sqs_state.current_queue.is_some()
2892                    && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2893                {
2894                    // Pipes tab: Columns(0), PageSize(columns.len() + 2)
2895                    let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
2896                    if self.column_selector_index < page_size_idx {
2897                        self.column_selector_index = page_size_idx;
2898                    } else {
2899                        self.column_selector_index = 0;
2900                    }
2901                } else if self.current_service == Service::SqsQueues
2902                    && self.sqs_state.current_queue.is_some()
2903                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2904                {
2905                    // Tags tab: Columns(0), PageSize(columns.len() + 2)
2906                    let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
2907                    if self.column_selector_index < page_size_idx {
2908                        self.column_selector_index = page_size_idx;
2909                    } else {
2910                        self.column_selector_index = 0;
2911                    }
2912                } else if self.current_service == Service::SqsQueues
2913                    && self.sqs_state.current_queue.is_some()
2914                    && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2915                {
2916                    // Subscriptions tab: Columns(0), PageSize(columns.len() + 2)
2917                    let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
2918                    if self.column_selector_index < page_size_idx {
2919                        self.column_selector_index = page_size_idx;
2920                    } else {
2921                        self.column_selector_index = 0;
2922                    }
2923                } else if self.current_service == Service::S3Buckets
2924                    && self.s3_state.current_bucket.is_none()
2925                {
2926                    let page_size_idx = self.s3_bucket_column_ids.len() + 2;
2927                    if self.column_selector_index < page_size_idx {
2928                        self.column_selector_index = page_size_idx;
2929                    } else {
2930                        self.column_selector_index = 0;
2931                    }
2932                } else if self.current_service == Service::CloudWatchLogGroups {
2933                    if self.view_mode == ViewMode::Events {
2934                        // Events view: only columns, no sections to cycle
2935                    } else if self.view_mode == ViewMode::Detail {
2936                        if self.log_groups_state.detail_tab == DetailTab::Tags {
2937                            // Tags tab: Columns(0), PageSize(columns.len() + 2)
2938                            cycle_preference_next(
2939                                &mut self.column_selector_index,
2940                                self.cw_log_tag_column_ids.len(),
2941                            );
2942                        } else {
2943                            // Streams view: Columns(0), PageSize(columns.len() + 2)
2944                            cycle_preference_next(
2945                                &mut self.column_selector_index,
2946                                self.cw_log_stream_column_ids.len(),
2947                            );
2948                        }
2949                    } else {
2950                        // Log groups view: Columns(0), PageSize(columns.len() + 2)
2951                        cycle_preference_next(
2952                            &mut self.column_selector_index,
2953                            self.cw_log_group_column_ids.len(),
2954                        );
2955                    }
2956                }
2957            }
2958            Action::PrevPreferences => {
2959                if self.current_service == Service::ApiGatewayApis {
2960                    cycle_preference_prev(
2961                        &mut self.column_selector_index,
2962                        self.apig_api_column_ids.len(),
2963                    );
2964                } else if self.current_service == Service::CloudWatchAlarms {
2965                    // Jump to prev section: Columns(0), ViewAs(18), PageSize(22), WrapLines(28)
2966                    if self.column_selector_index >= 28 {
2967                        self.column_selector_index = 22;
2968                    } else if self.column_selector_index >= 22 {
2969                        self.column_selector_index = 18;
2970                    } else if self.column_selector_index >= 18 {
2971                        self.column_selector_index = 0;
2972                    } else {
2973                        self.column_selector_index = 28;
2974                    }
2975                } else if self.current_service == Service::EcrRepositories
2976                    && self.ecr_state.current_repository.is_some()
2977                {
2978                    let page_size_idx = self.ecr_image_column_ids.len() + 2;
2979                    if self.column_selector_index >= page_size_idx {
2980                        self.column_selector_index = 0;
2981                    } else {
2982                        self.column_selector_index = page_size_idx;
2983                    }
2984                } else if self.current_service == Service::LambdaFunctions {
2985                    let page_size_idx = self.lambda_state.function_column_ids.len() + 2;
2986                    if self.column_selector_index >= page_size_idx {
2987                        self.column_selector_index = 0;
2988                    } else {
2989                        self.column_selector_index = page_size_idx;
2990                    }
2991                } else if self.current_service == Service::LambdaApplications {
2992                    let page_size_idx = self.lambda_application_column_ids.len() + 2;
2993                    if self.column_selector_index >= page_size_idx {
2994                        self.column_selector_index = 0;
2995                    } else {
2996                        self.column_selector_index = page_size_idx;
2997                    }
2998                } else if self.current_service == Service::CloudFormationStacks {
2999                    let page_size_idx = self.cfn_column_ids.len() + 2;
3000                    if self.column_selector_index >= page_size_idx {
3001                        self.column_selector_index = 0;
3002                    } else {
3003                        self.column_selector_index = page_size_idx;
3004                    }
3005                } else if self.current_service == Service::CloudTrailEvents {
3006                    if self.cloudtrail_state.current_event.is_some()
3007                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
3008                    {
3009                        // Resources table has no page size, just columns
3010                        self.column_selector_index = 0;
3011                    } else {
3012                        let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
3013                        if self.column_selector_index >= page_size_idx {
3014                            self.column_selector_index = 0;
3015                        } else {
3016                            self.column_selector_index = page_size_idx;
3017                        }
3018                    }
3019                } else if self.current_service == Service::Ec2Instances {
3020                    let page_size_idx = self.ec2_column_ids.len() + 2;
3021                    if self.column_selector_index >= page_size_idx {
3022                        self.column_selector_index = 0;
3023                    } else {
3024                        self.column_selector_index = page_size_idx;
3025                    }
3026                } else if self.current_service == Service::IamUsers {
3027                    if self.iam_state.current_user.is_some() {
3028                        match self.iam_state.user_tab {
3029                            UserTab::Permissions => {
3030                                let page_size_idx = self.iam_policy_column_ids.len() + 2;
3031                                if self.column_selector_index >= page_size_idx {
3032                                    self.column_selector_index = 0;
3033                                } else {
3034                                    self.column_selector_index = page_size_idx;
3035                                }
3036                            }
3037                            UserTab::Groups | UserTab::Tags => {
3038                                if self.column_selector_index >= 4 {
3039                                    self.column_selector_index = 0;
3040                                } else {
3041                                    self.column_selector_index = 4;
3042                                }
3043                            }
3044                            UserTab::LastAccessed => {
3045                                if self.column_selector_index >= 5 {
3046                                    self.column_selector_index = 0;
3047                                } else {
3048                                    self.column_selector_index = 5;
3049                                }
3050                            }
3051                            _ => {}
3052                        }
3053                    } else {
3054                        let page_size_idx = self.iam_user_column_ids.len() + 2;
3055                        if self.column_selector_index >= page_size_idx {
3056                            self.column_selector_index = 0;
3057                        } else {
3058                            self.column_selector_index = page_size_idx;
3059                        }
3060                    }
3061                } else if self.current_service == Service::IamRoles {
3062                    if self.iam_state.current_role.is_some() {
3063                        match self.iam_state.role_tab {
3064                            RoleTab::Permissions => {
3065                                let page_size_idx = self.iam_policy_column_ids.len() + 2;
3066                                if self.column_selector_index >= page_size_idx {
3067                                    self.column_selector_index = 0;
3068                                } else {
3069                                    self.column_selector_index = page_size_idx;
3070                                }
3071                            }
3072                            RoleTab::Tags => {
3073                                if self.column_selector_index >= 4 {
3074                                    self.column_selector_index = 0;
3075                                } else {
3076                                    self.column_selector_index = 4;
3077                                }
3078                            }
3079                            RoleTab::LastAccessed => {
3080                                if self.column_selector_index >= 5 {
3081                                    self.column_selector_index = 0;
3082                                } else {
3083                                    self.column_selector_index = 5;
3084                                }
3085                            }
3086                            _ => {}
3087                        }
3088                    } else {
3089                        let page_size_idx = self.iam_role_column_ids.len() + 2;
3090                        if self.column_selector_index >= page_size_idx {
3091                            self.column_selector_index = 0;
3092                        } else {
3093                            self.column_selector_index = page_size_idx;
3094                        }
3095                    }
3096                } else if self.current_service == Service::IamUserGroups {
3097                    let page_size_idx = self.iam_group_column_ids.len() + 2;
3098                    if self.column_selector_index >= page_size_idx {
3099                        self.column_selector_index = 0;
3100                    } else {
3101                        self.column_selector_index = page_size_idx;
3102                    }
3103                } else if self.current_service == Service::SqsQueues
3104                    && self.sqs_state.current_queue.is_some()
3105                {
3106                    let page_size_idx = match self.sqs_state.detail_tab {
3107                        SqsQueueDetailTab::LambdaTriggers => {
3108                            self.sqs_state.trigger_column_ids.len() + 2
3109                        }
3110                        SqsQueueDetailTab::EventBridgePipes => {
3111                            self.sqs_state.pipe_column_ids.len() + 2
3112                        }
3113                        SqsQueueDetailTab::Tagging => self.sqs_state.tag_column_ids.len() + 2,
3114                        SqsQueueDetailTab::SnsSubscriptions => {
3115                            self.sqs_state.subscription_column_ids.len() + 2
3116                        }
3117                        _ => 0,
3118                    };
3119                    if page_size_idx > 0 {
3120                        if self.column_selector_index >= page_size_idx {
3121                            self.column_selector_index = 0;
3122                        } else {
3123                            self.column_selector_index = page_size_idx;
3124                        }
3125                    }
3126                } else if self.current_service == Service::S3Buckets
3127                    && self.s3_state.current_bucket.is_none()
3128                {
3129                    let page_size_idx = self.s3_bucket_column_ids.len() + 2;
3130                    if self.column_selector_index >= page_size_idx {
3131                        self.column_selector_index = 0;
3132                    } else {
3133                        self.column_selector_index = page_size_idx;
3134                    }
3135                } else if self.current_service == Service::CloudWatchLogGroups {
3136                    if self.view_mode == ViewMode::Events {
3137                        // Events view: only columns, no sections to cycle
3138                    } else if self.view_mode == ViewMode::Detail {
3139                        if self.log_groups_state.detail_tab == DetailTab::Tags {
3140                            // Tags tab: Columns(0), PageSize(columns.len() + 2)
3141                            cycle_preference_prev(
3142                                &mut self.column_selector_index,
3143                                self.cw_log_tag_column_ids.len(),
3144                            );
3145                        } else {
3146                            // Streams view: Columns(0), PageSize(columns.len() + 2)
3147                            cycle_preference_prev(
3148                                &mut self.column_selector_index,
3149                                self.cw_log_stream_column_ids.len(),
3150                            );
3151                        }
3152                    } else {
3153                        // Log groups view: Columns(0), PageSize(columns.len() + 2)
3154                        cycle_preference_prev(
3155                            &mut self.column_selector_index,
3156                            self.cw_log_group_column_ids.len(),
3157                        );
3158                    }
3159                }
3160            }
3161            Action::CloseColumnSelector => {
3162                self.mode = Mode::Normal;
3163                self.preference_section = Preferences::Columns;
3164            }
3165            Action::NextDetailTab => {
3166                if self.current_service == Service::CloudTrailEvents
3167                    && self.cloudtrail_state.current_event.is_some()
3168                {
3169                    self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
3170                } else if self.current_service == Service::ApiGatewayApis
3171                    && self.apig_state.current_api.is_some()
3172                {
3173                    self.apig_state.detail_tab = self.apig_state.detail_tab.next();
3174                } else if self.current_service == Service::SqsQueues
3175                    && self.sqs_state.current_queue.is_some()
3176                {
3177                    self.sqs_state.detail_tab = self.sqs_state.detail_tab.next();
3178                    if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
3179                        self.sqs_state.set_metrics_loading(true);
3180                        self.sqs_state.set_monitoring_scroll(0);
3181                        self.sqs_state.clear_metrics();
3182                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
3183                        self.sqs_state.triggers.loading = true;
3184                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
3185                        self.sqs_state.pipes.loading = true;
3186                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
3187                        self.sqs_state.tags.loading = true;
3188                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
3189                        self.sqs_state.subscriptions.loading = true;
3190                    }
3191                } else if self.current_service == Service::Ec2Instances
3192                    && self.ec2_state.current_instance.is_some()
3193                {
3194                    self.ec2_state.detail_tab = self.ec2_state.detail_tab.next();
3195                    if self.ec2_state.detail_tab == Ec2DetailTab::Tags {
3196                        self.ec2_state.tags.loading = true;
3197                    } else if self.ec2_state.detail_tab == Ec2DetailTab::Monitoring {
3198                        self.ec2_state.set_metrics_loading(true);
3199                        self.ec2_state.set_monitoring_scroll(0);
3200                        self.ec2_state.clear_metrics();
3201                    }
3202                } else if self.current_service == Service::LambdaApplications
3203                    && self.lambda_application_state.current_application.is_some()
3204                {
3205                    self.lambda_application_state.detail_tab =
3206                        self.lambda_application_state.detail_tab.next();
3207                } else if self.current_service == Service::IamRoles
3208                    && self.iam_state.current_role.is_some()
3209                {
3210                    self.iam_state.role_tab = self.iam_state.role_tab.next();
3211                    if self.iam_state.role_tab == RoleTab::Tags {
3212                        self.iam_state.tags.loading = true;
3213                    }
3214                } else if self.current_service == Service::IamUsers
3215                    && self.iam_state.current_user.is_some()
3216                {
3217                    self.iam_state.user_tab = self.iam_state.user_tab.next();
3218                    if self.iam_state.user_tab == UserTab::Tags {
3219                        self.iam_state.user_tags.loading = true;
3220                    }
3221                } else if self.current_service == Service::IamUserGroups
3222                    && self.iam_state.current_group.is_some()
3223                {
3224                    self.iam_state.group_tab = self.iam_state.group_tab.next();
3225                } else if self.view_mode == ViewMode::Detail {
3226                    self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
3227                    if self.log_groups_state.detail_tab == DetailTab::Tags {
3228                        self.log_groups_state.tags.loading = true;
3229                    }
3230                } else if self.current_service == Service::S3Buckets {
3231                    if self.s3_state.current_bucket.is_some() {
3232                        self.s3_state.object_tab = self.s3_state.object_tab.next();
3233                    } else {
3234                        self.s3_state.bucket_type = match self.s3_state.bucket_type {
3235                            S3BucketType::GeneralPurpose => S3BucketType::Directory,
3236                            S3BucketType::Directory => S3BucketType::GeneralPurpose,
3237                        };
3238                        self.s3_state.buckets.reset();
3239                    }
3240                } else if self.current_service == Service::CloudWatchAlarms {
3241                    self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
3242                        AlarmTab::AllAlarms => AlarmTab::InAlarm,
3243                        AlarmTab::InAlarm => AlarmTab::AllAlarms,
3244                    };
3245                    self.alarms_state.table.reset();
3246                } else if self.current_service == Service::EcrRepositories
3247                    && self.ecr_state.current_repository.is_none()
3248                {
3249                    self.ecr_state.tab = self.ecr_state.tab.next();
3250                    self.ecr_state.repositories.reset();
3251                    self.ecr_state.repositories.loading = true;
3252                } else if self.current_service == Service::LambdaFunctions
3253                    && self.lambda_state.current_function.is_some()
3254                {
3255                    if self.lambda_state.current_version.is_some() {
3256                        // Version view: use VersionDetailTab enum
3257                        self.lambda_state.version_detail_tab =
3258                            self.lambda_state.version_detail_tab.next();
3259                        self.lambda_state.detail_tab =
3260                            self.lambda_state.version_detail_tab.to_detail_tab();
3261                        if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
3262                            self.lambda_state.set_metrics_loading(true);
3263                            self.lambda_state.set_monitoring_scroll(0);
3264                            self.lambda_state.clear_metrics();
3265                        }
3266                    } else {
3267                        self.lambda_state.detail_tab = self.lambda_state.detail_tab.next();
3268                        if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
3269                            self.lambda_state.set_metrics_loading(true);
3270                            self.lambda_state.set_monitoring_scroll(0);
3271                            self.lambda_state.clear_metrics();
3272                        }
3273                    }
3274                } else if self.current_service == Service::CloudFormationStacks
3275                    && self.cfn_state.current_stack.is_some()
3276                {
3277                    self.cfn_state.detail_tab = self.cfn_state.detail_tab.next();
3278                }
3279            }
3280            Action::PrevDetailTab => {
3281                if self.current_service == Service::CloudTrailEvents
3282                    && self.cloudtrail_state.current_event.is_some()
3283                {
3284                    self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
3285                } else if self.current_service == Service::ApiGatewayApis
3286                    && self.apig_state.current_api.is_some()
3287                {
3288                    self.apig_state.detail_tab = self.apig_state.detail_tab.prev();
3289                } else if self.current_service == Service::SqsQueues
3290                    && self.sqs_state.current_queue.is_some()
3291                {
3292                    self.sqs_state.detail_tab = self.sqs_state.detail_tab.prev();
3293                    if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
3294                        self.sqs_state.set_metrics_loading(true);
3295                        self.sqs_state.set_monitoring_scroll(0);
3296                        self.sqs_state.clear_metrics();
3297                    }
3298                } else if self.current_service == Service::Ec2Instances
3299                    && self.ec2_state.current_instance.is_some()
3300                {
3301                    self.ec2_state.detail_tab = self.ec2_state.detail_tab.prev();
3302                    if self.ec2_state.detail_tab == Ec2DetailTab::Tags {
3303                        self.ec2_state.tags.loading = true;
3304                    } else if self.ec2_state.detail_tab == Ec2DetailTab::Monitoring {
3305                        self.ec2_state.set_metrics_loading(true);
3306                        self.ec2_state.set_monitoring_scroll(0);
3307                        self.ec2_state.clear_metrics();
3308                    }
3309                } else if self.current_service == Service::LambdaApplications
3310                    && self.lambda_application_state.current_application.is_some()
3311                {
3312                    self.lambda_application_state.detail_tab =
3313                        self.lambda_application_state.detail_tab.prev();
3314                } else if self.current_service == Service::IamRoles
3315                    && self.iam_state.current_role.is_some()
3316                {
3317                    self.iam_state.role_tab = self.iam_state.role_tab.prev();
3318                } else if self.current_service == Service::IamUsers
3319                    && self.iam_state.current_user.is_some()
3320                {
3321                    self.iam_state.user_tab = self.iam_state.user_tab.prev();
3322                } else if self.current_service == Service::IamUserGroups
3323                    && self.iam_state.current_group.is_some()
3324                {
3325                    self.iam_state.group_tab = self.iam_state.group_tab.prev();
3326                } else if self.view_mode == ViewMode::Detail {
3327                    self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
3328                } else if self.current_service == Service::S3Buckets {
3329                    if self.s3_state.current_bucket.is_some() {
3330                        self.s3_state.object_tab = self.s3_state.object_tab.prev();
3331                    }
3332                } else if self.current_service == Service::CloudWatchAlarms {
3333                    self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
3334                        AlarmTab::AllAlarms => AlarmTab::InAlarm,
3335                        AlarmTab::InAlarm => AlarmTab::AllAlarms,
3336                    };
3337                } else if self.current_service == Service::EcrRepositories
3338                    && self.ecr_state.current_repository.is_none()
3339                {
3340                    self.ecr_state.tab = self.ecr_state.tab.prev();
3341                    self.ecr_state.repositories.reset();
3342                    self.ecr_state.repositories.loading = true;
3343                } else if self.current_service == Service::LambdaFunctions
3344                    && self.lambda_state.current_function.is_some()
3345                {
3346                    if self.lambda_state.current_version.is_some() {
3347                        // Version view: use VersionDetailTab enum
3348                        self.lambda_state.version_detail_tab =
3349                            self.lambda_state.version_detail_tab.prev();
3350                        self.lambda_state.detail_tab =
3351                            self.lambda_state.version_detail_tab.to_detail_tab();
3352                        if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
3353                            self.lambda_state.set_metrics_loading(true);
3354                            self.lambda_state.set_monitoring_scroll(0);
3355                            self.lambda_state.clear_metrics();
3356                        }
3357                    } else {
3358                        self.lambda_state.detail_tab = self.lambda_state.detail_tab.prev();
3359                        if self.lambda_state.detail_tab == LambdaDetailTab::Monitor {
3360                            self.lambda_state.set_metrics_loading(true);
3361                            self.lambda_state.set_monitoring_scroll(0);
3362                            self.lambda_state.clear_metrics();
3363                        }
3364                    }
3365                } else if self.current_service == Service::CloudFormationStacks
3366                    && self.cfn_state.current_stack.is_some()
3367                {
3368                    self.cfn_state.detail_tab = self.cfn_state.detail_tab.prev();
3369                }
3370            }
3371            Action::StartFilter => {
3372                // Don't allow filter mode when no service is selected and no tabs open
3373                if !self.service_selected && self.tabs.is_empty() {
3374                    return;
3375                }
3376
3377                if self.current_service == Service::CloudWatchInsights {
3378                    self.mode = Mode::InsightsInput;
3379                } else if self.current_service == Service::CloudWatchAlarms {
3380                    self.mode = Mode::FilterInput;
3381                } else if self.current_service == Service::CloudTrailEvents {
3382                    self.mode = Mode::FilterInput;
3383                    self.cloudtrail_state.input_focus = InputFocus::Filter;
3384                } else if self.current_service == Service::S3Buckets {
3385                    self.mode = Mode::FilterInput;
3386                    self.log_groups_state.filter_mode = true;
3387                } else if self.current_service == Service::ApiGatewayApis
3388                    || self.current_service == Service::EcrRepositories
3389                    || self.current_service == Service::IamUsers
3390                    || self.current_service == Service::IamUserGroups
3391                {
3392                    self.mode = Mode::FilterInput;
3393                    if self.current_service == Service::ApiGatewayApis {
3394                        self.apig_state.input_focus = InputFocus::Filter;
3395                    } else if self.current_service == Service::EcrRepositories
3396                        && self.ecr_state.current_repository.is_none()
3397                    {
3398                        self.ecr_state.input_focus = InputFocus::Filter;
3399                    }
3400                } else if self.current_service == Service::LambdaFunctions {
3401                    self.mode = Mode::FilterInput;
3402                    if self.lambda_state.current_version.is_some()
3403                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
3404                    {
3405                        self.lambda_state.alias_input_focus = InputFocus::Filter;
3406                    } else if self.lambda_state.current_function.is_some()
3407                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3408                    {
3409                        self.lambda_state.version_input_focus = InputFocus::Filter;
3410                    } else if self.lambda_state.current_function.is_none() {
3411                        self.lambda_state.input_focus = InputFocus::Filter;
3412                    }
3413                } else if self.current_service == Service::LambdaApplications {
3414                    self.mode = Mode::FilterInput;
3415                    if self.lambda_application_state.current_application.is_some() {
3416                        // In detail view - check which tab
3417                        if self.lambda_application_state.detail_tab
3418                            == LambdaApplicationDetailTab::Overview
3419                        {
3420                            self.lambda_application_state.resource_input_focus = InputFocus::Filter;
3421                        } else {
3422                            self.lambda_application_state.deployment_input_focus =
3423                                InputFocus::Filter;
3424                        }
3425                    } else {
3426                        self.lambda_application_state.input_focus = InputFocus::Filter;
3427                    }
3428                } else if self.current_service == Service::IamRoles {
3429                    self.mode = Mode::FilterInput;
3430                } else if self.current_service == Service::CloudFormationStacks {
3431                    self.mode = Mode::FilterInput;
3432                    if self.cfn_state.current_stack.is_some()
3433                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3434                    {
3435                        self.cfn_state.parameters_input_focus = InputFocus::Filter;
3436                    } else if self.cfn_state.current_stack.is_some()
3437                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3438                    {
3439                        self.cfn_state.outputs_input_focus = InputFocus::Filter;
3440                    } else {
3441                        self.cfn_state.input_focus = InputFocus::Filter;
3442                    }
3443                } else if self.current_service == Service::SqsQueues {
3444                    self.mode = Mode::FilterInput;
3445                    self.sqs_state.input_focus = InputFocus::Filter;
3446                } else if self.view_mode == ViewMode::List
3447                    || (self.view_mode == ViewMode::Detail
3448                        && (self.log_groups_state.detail_tab == DetailTab::LogStreams
3449                            || self.log_groups_state.detail_tab == DetailTab::Tags))
3450                {
3451                    self.mode = Mode::FilterInput;
3452                    self.log_groups_state.filter_mode = true;
3453                    self.log_groups_state.input_focus = InputFocus::Filter;
3454                } else if self.view_mode == ViewMode::Events {
3455                    self.mode = Mode::EventFilterInput;
3456                }
3457            }
3458            Action::StartEventFilter => {
3459                if self.current_service == Service::CloudWatchInsights {
3460                    self.mode = Mode::InsightsInput;
3461                } else if self.view_mode == ViewMode::List {
3462                    self.mode = Mode::FilterInput;
3463                    self.log_groups_state.filter_mode = true;
3464                    self.log_groups_state.input_focus = InputFocus::Filter;
3465                } else if self.view_mode == ViewMode::Events {
3466                    self.mode = Mode::EventFilterInput;
3467                } else if self.view_mode == ViewMode::Detail
3468                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3469                {
3470                    self.mode = Mode::FilterInput;
3471                    self.log_groups_state.filter_mode = true;
3472                    self.log_groups_state.input_focus = InputFocus::Filter;
3473                }
3474            }
3475            Action::NextFilterFocus => {
3476                if self.current_service == Service::CloudTrailEvents
3477                    && self.cloudtrail_state.current_event.is_some()
3478                {
3479                    self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
3480                } else if self.mode == Mode::FilterInput
3481                    && self.current_service == Service::S3Buckets
3482                {
3483                    const S3_FILTER_CONTROLS: [InputFocus; 2] =
3484                        [InputFocus::Filter, InputFocus::Pagination];
3485                    self.s3_state.input_focus = self.s3_state.input_focus.next(&S3_FILTER_CONTROLS);
3486                } else if self.mode == Mode::FilterInput
3487                    && self.current_service == Service::Ec2Instances
3488                {
3489                    self.ec2_state.input_focus =
3490                        self.ec2_state.input_focus.next(&ec2::FILTER_CONTROLS);
3491                } else if self.mode == Mode::FilterInput
3492                    && self.current_service == Service::LambdaApplications
3493                {
3494                    use crate::ui::lambda::FILTER_CONTROLS;
3495                    if self.lambda_application_state.current_application.is_some() {
3496                        if self.lambda_application_state.detail_tab
3497                            == LambdaApplicationDetailTab::Deployments
3498                        {
3499                            self.lambda_application_state.deployment_input_focus = self
3500                                .lambda_application_state
3501                                .deployment_input_focus
3502                                .next(&FILTER_CONTROLS);
3503                        } else {
3504                            self.lambda_application_state.resource_input_focus = self
3505                                .lambda_application_state
3506                                .resource_input_focus
3507                                .next(&FILTER_CONTROLS);
3508                        }
3509                    } else {
3510                        self.lambda_application_state.input_focus = self
3511                            .lambda_application_state
3512                            .input_focus
3513                            .next(&FILTER_CONTROLS);
3514                    }
3515                } else if self.mode == Mode::FilterInput
3516                    && self.current_service == Service::IamRoles
3517                    && self.iam_state.current_role.is_some()
3518                {
3519                    use crate::ui::iam::POLICY_FILTER_CONTROLS;
3520                    self.iam_state.policy_input_focus = self
3521                        .iam_state
3522                        .policy_input_focus
3523                        .next(&POLICY_FILTER_CONTROLS);
3524                } else if self.mode == Mode::FilterInput
3525                    && self.current_service == Service::IamRoles
3526                    && self.iam_state.current_role.is_none()
3527                {
3528                    use crate::ui::iam::ROLE_FILTER_CONTROLS;
3529                    self.iam_state.role_input_focus =
3530                        self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
3531                } else if self.mode == Mode::FilterInput
3532                    && self.current_service == Service::IamUsers
3533                    && self.iam_state.current_user.is_some()
3534                {
3535                    use crate::ui::iam::{
3536                        POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3537                        USER_SIMPLE_FILTER_CONTROLS,
3538                    };
3539                    if self.iam_state.user_tab == UserTab::Permissions {
3540                        self.iam_state.policy_input_focus = self
3541                            .iam_state
3542                            .policy_input_focus
3543                            .next(&POLICY_FILTER_CONTROLS);
3544                    } else if self.iam_state.user_tab == UserTab::LastAccessed {
3545                        self.iam_state.last_accessed_input_focus = self
3546                            .iam_state
3547                            .last_accessed_input_focus
3548                            .next(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3549                    } else {
3550                        self.iam_state.user_input_focus = self
3551                            .iam_state
3552                            .user_input_focus
3553                            .next(&USER_SIMPLE_FILTER_CONTROLS);
3554                    }
3555                } else if self.mode == Mode::FilterInput
3556                    && self.current_service == Service::IamUserGroups
3557                {
3558                    use crate::ui::iam::GROUP_FILTER_CONTROLS;
3559                    self.iam_state.group_input_focus = self
3560                        .iam_state
3561                        .group_input_focus
3562                        .next(&GROUP_FILTER_CONTROLS);
3563                } else if self.mode == Mode::InsightsInput {
3564                    use crate::app::InsightsFocus;
3565                    self.insights_state.insights.insights_focus =
3566                        match self.insights_state.insights.insights_focus {
3567                            InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
3568                            InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
3569                            InsightsFocus::LogGroupSearch => InsightsFocus::Query,
3570                            InsightsFocus::Query => InsightsFocus::QueryLanguage,
3571                        };
3572                } else if self.mode == Mode::FilterInput
3573                    && self.current_service == Service::CloudFormationStacks
3574                {
3575                    if self.cfn_state.current_stack.is_some()
3576                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3577                    {
3578                        self.cfn_state.parameters_input_focus = self
3579                            .cfn_state
3580                            .parameters_input_focus
3581                            .next(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
3582                    } else if self.cfn_state.current_stack.is_some()
3583                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3584                    {
3585                        self.cfn_state.outputs_input_focus = self
3586                            .cfn_state
3587                            .outputs_input_focus
3588                            .next(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
3589                    } else if self.cfn_state.current_stack.is_some()
3590                        && self.cfn_state.detail_tab == CfnDetailTab::Resources
3591                    {
3592                        self.cfn_state.resources_input_focus = self
3593                            .cfn_state
3594                            .resources_input_focus
3595                            .next(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
3596                    } else {
3597                        self.cfn_state.input_focus = self
3598                            .cfn_state
3599                            .input_focus
3600                            .next(&CfnStateConstants::FILTER_CONTROLS);
3601                    }
3602                } else if self.mode == Mode::FilterInput
3603                    && self.current_service == Service::SqsQueues
3604                {
3605                    if self.sqs_state.current_queue.is_some()
3606                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3607                    {
3608                        use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
3609                        self.sqs_state.input_focus = self
3610                            .sqs_state
3611                            .input_focus
3612                            .next(SUBSCRIPTION_FILTER_CONTROLS);
3613                    } else {
3614                        use crate::ui::sqs::FILTER_CONTROLS;
3615                        self.sqs_state.input_focus =
3616                            self.sqs_state.input_focus.next(FILTER_CONTROLS);
3617                    }
3618                } else if self.mode == Mode::FilterInput
3619                    && self.current_service == Service::CloudWatchLogGroups
3620                {
3621                    use crate::ui::cw::logs::FILTER_CONTROLS;
3622                    self.log_groups_state.input_focus =
3623                        self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
3624                } else if self.mode == Mode::EventFilterInput {
3625                    self.log_groups_state.event_input_focus =
3626                        self.log_groups_state.event_input_focus.next();
3627                } else if self.mode == Mode::FilterInput
3628                    && self.current_service == Service::CloudWatchAlarms
3629                {
3630                    use crate::ui::cw::alarms::FILTER_CONTROLS;
3631                    self.alarms_state.input_focus =
3632                        self.alarms_state.input_focus.next(&FILTER_CONTROLS);
3633                } else if self.mode == Mode::FilterInput
3634                    && self.current_service == Service::CloudTrailEvents
3635                {
3636                    const FILTER_CONTROLS: [InputFocus; 2] =
3637                        [InputFocus::Filter, InputFocus::Pagination];
3638                    self.cloudtrail_state.input_focus =
3639                        self.cloudtrail_state.input_focus.next(&FILTER_CONTROLS);
3640                } else if self.mode == Mode::FilterInput
3641                    && self.current_service == Service::ApiGatewayApis
3642                {
3643                    use crate::ui::apig::FILTER_CONTROLS;
3644                    self.apig_state.input_focus =
3645                        self.apig_state.input_focus.next(&FILTER_CONTROLS);
3646                } else if self.mode == Mode::FilterInput
3647                    && self.current_service == Service::EcrRepositories
3648                    && self.ecr_state.current_repository.is_none()
3649                {
3650                    use crate::ui::ecr::FILTER_CONTROLS;
3651                    self.ecr_state.input_focus = self.ecr_state.input_focus.next(&FILTER_CONTROLS);
3652                } else if self.mode == Mode::FilterInput
3653                    && self.current_service == Service::LambdaFunctions
3654                {
3655                    use crate::ui::lambda::FILTER_CONTROLS;
3656                    if self.lambda_state.current_version.is_some()
3657                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
3658                    {
3659                        self.lambda_state.alias_input_focus =
3660                            self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
3661                    } else if self.lambda_state.current_function.is_some()
3662                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3663                    {
3664                        self.lambda_state.version_input_focus =
3665                            self.lambda_state.version_input_focus.next(&FILTER_CONTROLS);
3666                    } else if self.lambda_state.current_function.is_some()
3667                        && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3668                    {
3669                        self.lambda_state.alias_input_focus =
3670                            self.lambda_state.alias_input_focus.next(&FILTER_CONTROLS);
3671                    } else if self.lambda_state.current_function.is_none() {
3672                        self.lambda_state.input_focus =
3673                            self.lambda_state.input_focus.next(&FILTER_CONTROLS);
3674                    }
3675                }
3676            }
3677            Action::PrevFilterFocus => {
3678                if self.current_service == Service::CloudTrailEvents
3679                    && self.cloudtrail_state.current_event.is_some()
3680                {
3681                    self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
3682                } else if self.mode == Mode::FilterInput
3683                    && self.current_service == Service::ApiGatewayApis
3684                {
3685                    use crate::ui::apig::FILTER_CONTROLS;
3686                    self.apig_state.input_focus =
3687                        self.apig_state.input_focus.prev(&FILTER_CONTROLS);
3688                } else if self.mode == Mode::FilterInput
3689                    && self.current_service == Service::Ec2Instances
3690                {
3691                    self.ec2_state.input_focus =
3692                        self.ec2_state.input_focus.prev(&ec2::FILTER_CONTROLS);
3693                } else if self.mode == Mode::FilterInput
3694                    && self.current_service == Service::LambdaApplications
3695                {
3696                    use crate::ui::lambda::FILTER_CONTROLS;
3697                    if self.lambda_application_state.current_application.is_some() {
3698                        if self.lambda_application_state.detail_tab
3699                            == LambdaApplicationDetailTab::Deployments
3700                        {
3701                            self.lambda_application_state.deployment_input_focus = self
3702                                .lambda_application_state
3703                                .deployment_input_focus
3704                                .prev(&FILTER_CONTROLS);
3705                        } else {
3706                            self.lambda_application_state.resource_input_focus = self
3707                                .lambda_application_state
3708                                .resource_input_focus
3709                                .prev(&FILTER_CONTROLS);
3710                        }
3711                    } else {
3712                        self.lambda_application_state.input_focus = self
3713                            .lambda_application_state
3714                            .input_focus
3715                            .prev(&FILTER_CONTROLS);
3716                    }
3717                } else if self.mode == Mode::FilterInput
3718                    && self.current_service == Service::CloudFormationStacks
3719                {
3720                    if self.cfn_state.current_stack.is_some()
3721                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3722                    {
3723                        self.cfn_state.parameters_input_focus = self
3724                            .cfn_state
3725                            .parameters_input_focus
3726                            .prev(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
3727                    } else if self.cfn_state.current_stack.is_some()
3728                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3729                    {
3730                        self.cfn_state.outputs_input_focus = self
3731                            .cfn_state
3732                            .outputs_input_focus
3733                            .prev(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
3734                    } else if self.cfn_state.current_stack.is_some()
3735                        && self.cfn_state.detail_tab == CfnDetailTab::Resources
3736                    {
3737                        self.cfn_state.resources_input_focus = self
3738                            .cfn_state
3739                            .resources_input_focus
3740                            .prev(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
3741                    } else {
3742                        self.cfn_state.input_focus = self
3743                            .cfn_state
3744                            .input_focus
3745                            .prev(&CfnStateConstants::FILTER_CONTROLS);
3746                    }
3747                } else if self.mode == Mode::FilterInput
3748                    && self.current_service == Service::SqsQueues
3749                {
3750                    if self.sqs_state.current_queue.is_some()
3751                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3752                    {
3753                        use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
3754                        self.sqs_state.input_focus = self
3755                            .sqs_state
3756                            .input_focus
3757                            .prev(SUBSCRIPTION_FILTER_CONTROLS);
3758                    } else {
3759                        use crate::ui::sqs::FILTER_CONTROLS;
3760                        self.sqs_state.input_focus =
3761                            self.sqs_state.input_focus.prev(FILTER_CONTROLS);
3762                    }
3763                } else if self.mode == Mode::FilterInput
3764                    && self.current_service == Service::IamRoles
3765                    && self.iam_state.current_role.is_none()
3766                {
3767                    use crate::ui::iam::ROLE_FILTER_CONTROLS;
3768                    self.iam_state.role_input_focus =
3769                        self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
3770                } else if self.mode == Mode::FilterInput
3771                    && self.current_service == Service::IamUsers
3772                    && self.iam_state.current_user.is_some()
3773                {
3774                    use crate::ui::iam::{
3775                        POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3776                        USER_SIMPLE_FILTER_CONTROLS,
3777                    };
3778                    if self.iam_state.user_tab == UserTab::Permissions {
3779                        self.iam_state.policy_input_focus = self
3780                            .iam_state
3781                            .policy_input_focus
3782                            .prev(&POLICY_FILTER_CONTROLS);
3783                    } else if self.iam_state.user_tab == UserTab::LastAccessed {
3784                        self.iam_state.last_accessed_input_focus = self
3785                            .iam_state
3786                            .last_accessed_input_focus
3787                            .prev(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3788                    } else {
3789                        self.iam_state.user_input_focus = self
3790                            .iam_state
3791                            .user_input_focus
3792                            .prev(&USER_SIMPLE_FILTER_CONTROLS);
3793                    }
3794                } else if self.mode == Mode::FilterInput
3795                    && self.current_service == Service::IamUserGroups
3796                {
3797                    use crate::ui::iam::GROUP_FILTER_CONTROLS;
3798                    self.iam_state.group_input_focus = self
3799                        .iam_state
3800                        .group_input_focus
3801                        .prev(&GROUP_FILTER_CONTROLS);
3802                } else if self.mode == Mode::FilterInput
3803                    && self.current_service == Service::CloudWatchLogGroups
3804                {
3805                    use crate::ui::cw::logs::FILTER_CONTROLS;
3806                    self.log_groups_state.input_focus =
3807                        self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
3808                } else if self.mode == Mode::EventFilterInput {
3809                    self.log_groups_state.event_input_focus =
3810                        self.log_groups_state.event_input_focus.prev();
3811                } else if self.mode == Mode::FilterInput
3812                    && self.current_service == Service::IamRoles
3813                    && self.iam_state.current_role.is_some()
3814                {
3815                    use crate::ui::iam::POLICY_FILTER_CONTROLS;
3816                    self.iam_state.policy_input_focus = self
3817                        .iam_state
3818                        .policy_input_focus
3819                        .prev(&POLICY_FILTER_CONTROLS);
3820                } else if self.mode == Mode::FilterInput
3821                    && self.current_service == Service::CloudWatchAlarms
3822                {
3823                    use crate::ui::cw::alarms::FILTER_CONTROLS;
3824                    self.alarms_state.input_focus =
3825                        self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
3826                } else if self.mode == Mode::FilterInput
3827                    && self.current_service == Service::CloudTrailEvents
3828                {
3829                    const FILTER_CONTROLS: [InputFocus; 2] =
3830                        [InputFocus::Filter, InputFocus::Pagination];
3831                    self.cloudtrail_state.input_focus =
3832                        self.cloudtrail_state.input_focus.prev(&FILTER_CONTROLS);
3833                } else if self.mode == Mode::FilterInput
3834                    && self.current_service == Service::EcrRepositories
3835                    && self.ecr_state.current_repository.is_none()
3836                {
3837                    use crate::ui::ecr::FILTER_CONTROLS;
3838                    self.ecr_state.input_focus = self.ecr_state.input_focus.prev(&FILTER_CONTROLS);
3839                } else if self.mode == Mode::FilterInput
3840                    && self.current_service == Service::LambdaFunctions
3841                {
3842                    use crate::ui::lambda::FILTER_CONTROLS;
3843                    if self.lambda_state.current_version.is_some()
3844                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
3845                    {
3846                        self.lambda_state.alias_input_focus =
3847                            self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
3848                    } else if self.lambda_state.current_function.is_some()
3849                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
3850                    {
3851                        self.lambda_state.version_input_focus =
3852                            self.lambda_state.version_input_focus.prev(&FILTER_CONTROLS);
3853                    } else if self.lambda_state.current_function.is_some()
3854                        && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
3855                    {
3856                        self.lambda_state.alias_input_focus =
3857                            self.lambda_state.alias_input_focus.prev(&FILTER_CONTROLS);
3858                    } else if self.lambda_state.current_function.is_none() {
3859                        self.lambda_state.input_focus =
3860                            self.lambda_state.input_focus.prev(&FILTER_CONTROLS);
3861                    }
3862                }
3863            }
3864            Action::ToggleFilterCheckbox => {
3865                if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
3866                    if self.ec2_state.input_focus == EC2_STATE_FILTER {
3867                        self.ec2_state.state_filter = self.ec2_state.state_filter.next();
3868                        self.ec2_state.table.reset();
3869                    }
3870                } else if self.mode == Mode::InsightsInput {
3871                    use crate::app::InsightsFocus;
3872                    if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3873                        && self.insights_state.insights.show_dropdown
3874                        && !self.insights_state.insights.log_group_matches.is_empty()
3875                    {
3876                        let selected_idx = self.insights_state.insights.dropdown_selected;
3877                        if let Some(group_name) = self
3878                            .insights_state
3879                            .insights
3880                            .log_group_matches
3881                            .get(selected_idx)
3882                        {
3883                            let group_name = group_name.clone();
3884                            if let Some(pos) = self
3885                                .insights_state
3886                                .insights
3887                                .selected_log_groups
3888                                .iter()
3889                                .position(|g| g == &group_name)
3890                            {
3891                                self.insights_state.insights.selected_log_groups.remove(pos);
3892                            } else if self.insights_state.insights.selected_log_groups.len() < 50 {
3893                                self.insights_state
3894                                    .insights
3895                                    .selected_log_groups
3896                                    .push(group_name);
3897                            }
3898                        }
3899                    }
3900                } else if self.mode == Mode::FilterInput
3901                    && self.current_service == Service::CloudFormationStacks
3902                {
3903                    use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
3904                    match self.cfn_state.input_focus {
3905                        STATUS_FILTER => {
3906                            self.cfn_state.status_filter = self.cfn_state.status_filter.next();
3907                            self.cfn_state.table.reset();
3908                        }
3909                        VIEW_NESTED => {
3910                            self.cfn_state.view_nested = !self.cfn_state.view_nested;
3911                            self.cfn_state.table.reset();
3912                        }
3913                        _ => {}
3914                    }
3915                } else if self.mode == Mode::FilterInput
3916                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3917                {
3918                    match self.log_groups_state.input_focus {
3919                        InputFocus::Checkbox("ExactMatch") => {
3920                            self.log_groups_state.exact_match = !self.log_groups_state.exact_match
3921                        }
3922                        InputFocus::Checkbox("ShowExpired") => {
3923                            self.log_groups_state.show_expired = !self.log_groups_state.show_expired
3924                        }
3925                        _ => {}
3926                    }
3927                } else if self.mode == Mode::EventFilterInput
3928                    && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
3929                {
3930                    self.log_groups_state.relative_unit =
3931                        self.log_groups_state.relative_unit.next();
3932                }
3933            }
3934            Action::CycleSortColumn => {
3935                if self.view_mode == ViewMode::Detail
3936                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3937                {
3938                    self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
3939                        StreamSort::Name => StreamSort::CreationTime,
3940                        StreamSort::CreationTime => StreamSort::LastEventTime,
3941                        StreamSort::LastEventTime => StreamSort::Name,
3942                    };
3943                }
3944            }
3945            Action::ToggleSortDirection => {
3946                if self.view_mode == ViewMode::Detail
3947                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3948                {
3949                    self.log_groups_state.stream_sort_desc =
3950                        !self.log_groups_state.stream_sort_desc;
3951                }
3952            }
3953            Action::ScrollUp => {
3954                if self.mode == Mode::ErrorModal {
3955                    self.error_scroll = self.error_scroll.saturating_sub(1);
3956                } else if self.current_service == Service::CloudTrailEvents
3957                    && self.cloudtrail_state.current_event.is_some()
3958                {
3959                    self.cloudtrail_state.event_json_scroll =
3960                        self.cloudtrail_state.event_json_scroll.saturating_sub(10);
3961                } else if self.current_service == Service::LambdaFunctions
3962                    && self.lambda_state.current_function.is_some()
3963                    && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3964                    && !self.lambda_state.is_metrics_loading()
3965                {
3966                    self.lambda_state.set_monitoring_scroll(
3967                        self.lambda_state.monitoring_scroll().saturating_sub(1),
3968                    );
3969                } else if self.current_service == Service::Ec2Instances
3970                    && self.ec2_state.current_instance.is_some()
3971                    && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
3972                    && !self.ec2_state.is_metrics_loading()
3973                {
3974                    self.ec2_state.set_monitoring_scroll(
3975                        self.ec2_state.monitoring_scroll().saturating_sub(1),
3976                    );
3977                } else if self.current_service == Service::SqsQueues
3978                    && self.sqs_state.current_queue.is_some()
3979                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3980                    && !self.sqs_state.is_metrics_loading()
3981                {
3982                    self.sqs_state.set_monitoring_scroll(
3983                        self.sqs_state.monitoring_scroll().saturating_sub(1),
3984                    );
3985                } else if self.view_mode == ViewMode::PolicyView {
3986                    self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
3987                } else if self.current_service == Service::IamRoles
3988                    && self.iam_state.current_role.is_some()
3989                    && self.iam_state.role_tab == RoleTab::TrustRelationships
3990                {
3991                    self.iam_state.trust_policy_scroll =
3992                        self.iam_state.trust_policy_scroll.saturating_sub(10);
3993                } else if self.view_mode == ViewMode::Events {
3994                    if self.log_groups_state.event_scroll_offset == 0
3995                        && self.log_groups_state.has_older_events
3996                    {
3997                        self.log_groups_state.loading = true;
3998                    } else {
3999                        self.log_groups_state.event_scroll_offset =
4000                            self.log_groups_state.event_scroll_offset.saturating_sub(1);
4001                    }
4002                } else if self.view_mode == ViewMode::InsightsResults {
4003                    self.insights_state.insights.results_selected = self
4004                        .insights_state
4005                        .insights
4006                        .results_selected
4007                        .saturating_sub(1);
4008                } else if self.view_mode == ViewMode::Detail {
4009                    self.log_groups_state.selected_stream =
4010                        self.log_groups_state.selected_stream.saturating_sub(1);
4011                    self.log_groups_state.expanded_stream = None;
4012                } else if self.view_mode == ViewMode::List
4013                    && self.current_service == Service::CloudWatchLogGroups
4014                {
4015                    self.log_groups_state.log_groups.selected =
4016                        self.log_groups_state.log_groups.selected.saturating_sub(1);
4017                    self.log_groups_state.log_groups.snap_to_page();
4018                } else if self.current_service == Service::EcrRepositories {
4019                    if self.ecr_state.current_repository.is_some() {
4020                        self.ecr_state.images.page_up();
4021                    } else {
4022                        self.ecr_state.repositories.page_up();
4023                    }
4024                }
4025            }
4026            Action::ScrollDown => {
4027                if self.mode == Mode::ErrorModal {
4028                    if let Some(error_msg) = &self.error_message {
4029                        let lines = error_msg.lines().count();
4030                        let max_scroll = lines.saturating_sub(1);
4031                        self.error_scroll = (self.error_scroll + 1).min(max_scroll);
4032                    }
4033                } else if self.current_service == Service::CloudTrailEvents
4034                    && self.cloudtrail_state.current_event.is_some()
4035                {
4036                    if let Some(event) = &self.cloudtrail_state.current_event {
4037                        let lines = event.cloud_trail_event_json.lines().count();
4038                        let max_scroll = lines.saturating_sub(1);
4039                        self.cloudtrail_state.event_json_scroll =
4040                            (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
4041                    }
4042                } else if self.current_service == Service::SqsQueues
4043                    && self.sqs_state.current_queue.is_some()
4044                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
4045                {
4046                    self.sqs_state
4047                        .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
4048                } else if self.view_mode == ViewMode::PolicyView {
4049                    let lines = self.iam_state.policy_document.lines().count();
4050                    let max_scroll = lines.saturating_sub(1);
4051                    self.iam_state.policy_scroll =
4052                        (self.iam_state.policy_scroll + 10).min(max_scroll);
4053                } else if self.current_service == Service::IamRoles
4054                    && self.iam_state.current_role.is_some()
4055                    && self.iam_state.role_tab == RoleTab::TrustRelationships
4056                {
4057                    let lines = self.iam_state.trust_policy_document.lines().count();
4058                    let max_scroll = lines.saturating_sub(1);
4059                    self.iam_state.trust_policy_scroll =
4060                        (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
4061                } else if self.view_mode == ViewMode::Events {
4062                    let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
4063                    if self.log_groups_state.event_scroll_offset >= max_scroll {
4064                        // At the end, do nothing
4065                    } else {
4066                        self.log_groups_state.event_scroll_offset =
4067                            (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
4068                    }
4069                } else if self.view_mode == ViewMode::InsightsResults {
4070                    let max = self
4071                        .insights_state
4072                        .insights
4073                        .query_results
4074                        .len()
4075                        .saturating_sub(1);
4076                    self.insights_state.insights.results_selected =
4077                        (self.insights_state.insights.results_selected + 1).min(max);
4078                } else if self.view_mode == ViewMode::Detail {
4079                    let filtered_streams = filtered_log_streams(self);
4080                    let max = filtered_streams.len().saturating_sub(1);
4081                    self.log_groups_state.selected_stream =
4082                        (self.log_groups_state.selected_stream + 1).min(max);
4083                } else if self.view_mode == ViewMode::List
4084                    && self.current_service == Service::CloudWatchLogGroups
4085                {
4086                    let filtered_groups = filtered_log_groups(self);
4087                    self.log_groups_state
4088                        .log_groups
4089                        .next_item(filtered_groups.len());
4090                } else if self.current_service == Service::EcrRepositories {
4091                    if self.ecr_state.current_repository.is_some() {
4092                        let filtered_images = filtered_ecr_images(self);
4093                        self.ecr_state.images.page_down(filtered_images.len());
4094                    } else {
4095                        let filtered_repos = filtered_ecr_repositories(self);
4096                        self.ecr_state.repositories.page_down(filtered_repos.len());
4097                    }
4098                }
4099            }
4100
4101            Action::Refresh => {
4102                if self.mode == Mode::ProfilePicker {
4103                    self.log_groups_state.loading = true;
4104                    self.log_groups_state.loading_message = "Refreshing...".to_string();
4105                } else if self.mode == Mode::RegionPicker {
4106                    self.measure_region_latencies();
4107                } else if self.mode == Mode::SessionPicker {
4108                    self.sessions = Session::list_all().unwrap_or_default();
4109                } else if self.current_service == Service::CloudWatchInsights
4110                    && !self.insights_state.insights.selected_log_groups.is_empty()
4111                {
4112                    self.log_groups_state.loading = true;
4113                    self.insights_state.insights.query_completed = true;
4114                } else if self.current_service == Service::LambdaFunctions {
4115                    self.lambda_state.table.loading = true;
4116                } else if self.current_service == Service::LambdaApplications {
4117                    self.lambda_application_state.table.loading = true;
4118                } else if matches!(
4119                    self.view_mode,
4120                    ViewMode::Events | ViewMode::Detail | ViewMode::List
4121                ) {
4122                    self.log_groups_state.loading = true;
4123                }
4124            }
4125            Action::Yank => {
4126                if self.mode == Mode::ErrorModal {
4127                    // Copy error message
4128                    if let Some(error) = &self.error_message {
4129                        copy_to_clipboard(error);
4130                    }
4131                } else if self.view_mode == ViewMode::Events {
4132                    if let Some(event) = self
4133                        .log_groups_state
4134                        .log_events
4135                        .get(self.log_groups_state.event_scroll_offset)
4136                    {
4137                        copy_to_clipboard(&event.message);
4138                    }
4139                } else if self.current_service == Service::EcrRepositories {
4140                    if self.ecr_state.current_repository.is_some() {
4141                        let filtered_images = filtered_ecr_images(self);
4142                        if let Some(image) = self.ecr_state.images.get_selected(&filtered_images) {
4143                            copy_to_clipboard(&image.uri);
4144                        }
4145                    } else {
4146                        let filtered_repos = filtered_ecr_repositories(self);
4147                        if let Some(repo) =
4148                            self.ecr_state.repositories.get_selected(&filtered_repos)
4149                        {
4150                            copy_to_clipboard(&repo.uri);
4151                        }
4152                    }
4153                } else if self.current_service == Service::LambdaFunctions {
4154                    let filtered_functions = filtered_lambda_functions(self);
4155                    if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
4156                        copy_to_clipboard(&func.arn);
4157                    }
4158                } else if self.current_service == Service::CloudFormationStacks {
4159                    if let Some(stack_name) = &self.cfn_state.current_stack {
4160                        // In detail view - copy current stack ARN
4161                        if let Some(stack) = self
4162                            .cfn_state
4163                            .table
4164                            .items
4165                            .iter()
4166                            .find(|s| &s.name == stack_name)
4167                        {
4168                            copy_to_clipboard(&stack.stack_id);
4169                        }
4170                    } else {
4171                        // In list view - copy selected stack ARN
4172                        let filtered_stacks = filtered_cloudformation_stacks(self);
4173                        if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
4174                            copy_to_clipboard(&stack.stack_id);
4175                        }
4176                    }
4177                } else if self.current_service == Service::IamUsers {
4178                    if self.iam_state.current_user.is_some() {
4179                        if let Some(user_name) = &self.iam_state.current_user {
4180                            if let Some(user) = self
4181                                .iam_state
4182                                .users
4183                                .items
4184                                .iter()
4185                                .find(|u| u.user_name == *user_name)
4186                            {
4187                                copy_to_clipboard(&user.arn);
4188                            }
4189                        }
4190                    } else {
4191                        let filtered_users = filtered_iam_users(self);
4192                        if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
4193                            copy_to_clipboard(&user.arn);
4194                        }
4195                    }
4196                } else if self.current_service == Service::IamRoles {
4197                    if self.iam_state.current_role.is_some() {
4198                        if let Some(role_name) = &self.iam_state.current_role {
4199                            if let Some(role) = self
4200                                .iam_state
4201                                .roles
4202                                .items
4203                                .iter()
4204                                .find(|r| r.role_name == *role_name)
4205                            {
4206                                copy_to_clipboard(&role.arn);
4207                            }
4208                        }
4209                    } else {
4210                        let filtered_roles = filtered_iam_roles(self);
4211                        if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
4212                            copy_to_clipboard(&role.arn);
4213                        }
4214                    }
4215                } else if self.current_service == Service::IamUserGroups {
4216                    if self.iam_state.current_group.is_some() {
4217                        if let Some(group_name) = &self.iam_state.current_group {
4218                            let arn = iam::format_arn(&self.config.account_id, "group", group_name);
4219                            copy_to_clipboard(&arn);
4220                        }
4221                    } else {
4222                        let filtered_groups: Vec<_> = self
4223                            .iam_state
4224                            .groups
4225                            .items
4226                            .iter()
4227                            .filter(|g| {
4228                                if self.iam_state.groups.filter.is_empty() {
4229                                    true
4230                                } else {
4231                                    g.group_name
4232                                        .to_lowercase()
4233                                        .contains(&self.iam_state.groups.filter.to_lowercase())
4234                                }
4235                            })
4236                            .collect();
4237                        if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
4238                            let arn = iam::format_arn(
4239                                &self.config.account_id,
4240                                "group",
4241                                &group.group_name,
4242                            );
4243                            copy_to_clipboard(&arn);
4244                        }
4245                    }
4246                } else if self.current_service == Service::SqsQueues {
4247                    if self.sqs_state.current_queue.is_some() {
4248                        // In queue detail view - copy queue ARN
4249                        if let Some(queue) = self
4250                            .sqs_state
4251                            .queues
4252                            .items
4253                            .iter()
4254                            .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
4255                        {
4256                            let arn = format!(
4257                                "arn:aws:sqs:{}:{}:{}",
4258                                extract_region(&queue.url),
4259                                extract_account_id(&queue.url),
4260                                queue.name
4261                            );
4262                            copy_to_clipboard(&arn);
4263                        }
4264                    } else {
4265                        // In list view - copy selected queue ARN
4266                        let filtered_queues = filtered_queues(
4267                            &self.sqs_state.queues.items,
4268                            &self.sqs_state.queues.filter,
4269                        );
4270                        if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
4271                            let arn = format!(
4272                                "arn:aws:sqs:{}:{}:{}",
4273                                extract_region(&queue.url),
4274                                extract_account_id(&queue.url),
4275                                queue.name
4276                            );
4277                            copy_to_clipboard(&arn);
4278                        }
4279                    }
4280                } else if self.current_service == Service::ApiGatewayApis {
4281                    if let Some(_api) = &self.apig_state.current_api {
4282                        use crate::ui::apig::ApiDetailTab;
4283                        if self.apig_state.detail_tab == ApiDetailTab::Routes {
4284                            // In routes view - copy selected route ARN
4285                            let (filtered_routes, _) = crate::ui::apig::filter_tree_items(
4286                                &self.apig_state.routes.items,
4287                                &self.apig_state.route_children,
4288                                &self.apig_state.route_filter,
4289                            );
4290                            let filtered_refs: Vec<&Route> = filtered_routes.iter().collect();
4291                            if let Some(route) = self.apig_state.routes.get_selected(&filtered_refs)
4292                            {
4293                                if !route.arn.is_empty() {
4294                                    copy_to_clipboard(&route.arn);
4295                                }
4296                            }
4297                        }
4298                    }
4299                }
4300            }
4301            Action::CopyToClipboard => {
4302                // Request snapshot - will be captured after next render
4303                self.snapshot_requested = true;
4304            }
4305            Action::RetryLoad => {
4306                self.error_message = None;
4307                self.mode = Mode::Normal;
4308                self.log_groups_state.loading = true;
4309            }
4310            Action::ApplyFilter => {
4311                if self.mode == Mode::FilterInput
4312                    && self.current_service == Service::SqsQueues
4313                    && self.sqs_state.input_focus == InputFocus::Dropdown("SubscriptionRegion")
4314                {
4315                    let regions = AwsRegion::all();
4316                    if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
4317                        self.sqs_state.subscription_region_filter = region.code.to_string();
4318                    }
4319                    self.mode = Mode::Normal;
4320                } else if self.mode == Mode::InsightsInput {
4321                    use crate::app::InsightsFocus;
4322                    if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
4323                        && self.insights_state.insights.show_dropdown
4324                    {
4325                        // Close dropdown, exit input mode, and execute query
4326                        self.insights_state.insights.show_dropdown = false;
4327                        self.mode = Mode::Normal;
4328                        if !self.insights_state.insights.selected_log_groups.is_empty() {
4329                            self.log_groups_state.loading = true;
4330                            self.insights_state.insights.query_completed = true;
4331                        }
4332                    }
4333                } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
4334                    if let Ok(page) = self.page_input.parse::<usize>() {
4335                        self.go_to_page(page);
4336                    }
4337                    self.page_input.clear();
4338                } else {
4339                    self.mode = Mode::Normal;
4340                    self.log_groups_state.filter_mode = false;
4341                }
4342            }
4343            Action::ToggleExactMatch => {
4344                if self.view_mode == ViewMode::Detail
4345                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
4346                {
4347                    self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
4348                }
4349            }
4350            Action::ToggleShowExpired => {
4351                if self.view_mode == ViewMode::Detail
4352                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
4353                {
4354                    self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
4355                }
4356            }
4357            Action::GoBack => {
4358                // ServicePicker: close if we have tabs
4359                if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
4360                    self.mode = Mode::Normal;
4361                    self.service_picker.filter.clear();
4362                }
4363                // API Gateway: go back from API detail to list
4364                else if self.current_service == Service::ApiGatewayApis
4365                    && self.apig_state.current_api.is_some()
4366                {
4367                    self.apig_state.current_api = None;
4368                    self.apig_state.routes.items.clear();
4369                    self.apig_state.detail_tab = crate::ui::apig::ApiDetailTab::Routes;
4370                    self.update_current_tab_breadcrumb();
4371                }
4372                // S3: pop navigation stack first, then exit bucket
4373                else if self.current_service == Service::S3Buckets
4374                    && self.s3_state.current_bucket.is_some()
4375                {
4376                    if !self.s3_state.prefix_stack.is_empty() {
4377                        self.s3_state.prefix_stack.pop();
4378                        self.s3_state.buckets.loading = true;
4379                    } else {
4380                        self.s3_state.current_bucket = None;
4381                        self.s3_state.objects.clear();
4382                    }
4383                }
4384                // ECR: go back from images to repositories
4385                else if self.current_service == Service::EcrRepositories
4386                    && self.ecr_state.current_repository.is_some()
4387                {
4388                    if self.ecr_state.images.has_expanded_item() {
4389                        self.ecr_state.images.collapse();
4390                    } else {
4391                        self.ecr_state.current_repository = None;
4392                        self.ecr_state.current_repository_uri = None;
4393                        self.ecr_state.images.items.clear();
4394                        self.ecr_state.images.reset();
4395                    }
4396                }
4397                // EC2: go back from instance detail to list
4398                else if self.current_service == Service::Ec2Instances
4399                    && self.ec2_state.current_instance.is_some()
4400                {
4401                    self.ec2_state.current_instance = None;
4402                    self.view_mode = ViewMode::List;
4403                    self.update_current_tab_breadcrumb();
4404                }
4405                // SQS: go back from queue detail to list
4406                else if self.current_service == Service::SqsQueues
4407                    && self.sqs_state.current_queue.is_some()
4408                {
4409                    self.sqs_state.current_queue = None;
4410                }
4411                // IAM: go back from user detail to list
4412                else if self.current_service == Service::IamUsers
4413                    && self.iam_state.current_user.is_some()
4414                {
4415                    self.iam_state.current_user = None;
4416                    self.iam_state.policies.items.clear();
4417                    self.iam_state.policies.reset();
4418                    self.update_current_tab_breadcrumb();
4419                }
4420                // IAM: go back from group detail to list
4421                else if self.current_service == Service::IamUserGroups
4422                    && self.iam_state.current_group.is_some()
4423                {
4424                    self.iam_state.current_group = None;
4425                    self.update_current_tab_breadcrumb();
4426                }
4427                // IAM: go back from role detail to list
4428                else if self.current_service == Service::IamRoles {
4429                    if self.view_mode == ViewMode::PolicyView {
4430                        // Go back from policy view to role detail
4431                        self.view_mode = ViewMode::Detail;
4432                        self.iam_state.current_policy = None;
4433                        self.iam_state.policy_document.clear();
4434                        self.iam_state.policy_scroll = 0;
4435                        self.update_current_tab_breadcrumb();
4436                    } else if self.iam_state.current_role.is_some() {
4437                        self.iam_state.current_role = None;
4438                        self.iam_state.policies.items.clear();
4439                        self.iam_state.policies.reset();
4440                        self.update_current_tab_breadcrumb();
4441                    }
4442                }
4443                // Lambda: go back from version detail to function detail
4444                else if self.current_service == Service::LambdaFunctions
4445                    && self.lambda_state.current_version.is_some()
4446                {
4447                    self.lambda_state.current_version = None;
4448                    self.lambda_state.detail_tab = LambdaDetailTab::Versions;
4449                }
4450                // Lambda: go back from alias detail to function detail
4451                else if self.current_service == Service::LambdaFunctions
4452                    && self.lambda_state.current_alias.is_some()
4453                {
4454                    self.lambda_state.current_alias = None;
4455                    self.lambda_state.detail_tab = LambdaDetailTab::Aliases;
4456                }
4457                // Lambda: go back from function detail to list
4458                else if self.current_service == Service::LambdaFunctions
4459                    && self.lambda_state.current_function.is_some()
4460                {
4461                    self.lambda_state.current_function = None;
4462                    self.update_current_tab_breadcrumb();
4463                }
4464                // Lambda Applications: go back from application detail to list
4465                else if self.current_service == Service::LambdaApplications
4466                    && self.lambda_application_state.current_application.is_some()
4467                {
4468                    self.lambda_application_state.current_application = None;
4469                    self.update_current_tab_breadcrumb();
4470                }
4471                // CloudFormation: go back from stack detail to list
4472                else if self.current_service == Service::CloudFormationStacks
4473                    && self.cfn_state.current_stack.is_some()
4474                {
4475                    self.cfn_state.current_stack = None;
4476                    self.update_current_tab_breadcrumb();
4477                }
4478                // CloudTrail: go back from event detail to list
4479                else if self.current_service == Service::CloudTrailEvents
4480                    && self.cloudtrail_state.current_event.is_some()
4481                {
4482                    self.cloudtrail_state.current_event = None;
4483                    self.update_current_tab_breadcrumb();
4484                }
4485                // From insights results -> collapse if expanded, otherwise back to sidebar
4486                else if self.view_mode == ViewMode::InsightsResults {
4487                    if self.insights_state.insights.expanded_result.is_some() {
4488                        self.insights_state.insights.expanded_result = None;
4489                    }
4490                }
4491                // From alarms view -> collapse if expanded
4492                else if self.current_service == Service::CloudWatchAlarms {
4493                    if self.alarms_state.table.has_expanded_item() {
4494                        self.alarms_state.table.collapse();
4495                    }
4496                }
4497                // From EC2 instances view -> always collapse
4498                else if self.current_service == Service::Ec2Instances {
4499                    if self.ec2_state.current_instance.is_some()
4500                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4501                    {
4502                        self.ec2_state.tags.collapse();
4503                    } else {
4504                        self.ec2_state.table.collapse();
4505                    }
4506                }
4507                // From events view -> collapse if expanded, otherwise back to detail view
4508                else if self.view_mode == ViewMode::Events {
4509                    if self.log_groups_state.expanded_event.is_some() {
4510                        self.log_groups_state.expanded_event = None;
4511                    } else {
4512                        self.view_mode = ViewMode::Detail;
4513                        self.log_groups_state.event_filter.clear();
4514                    }
4515                }
4516                // From detail view -> back to list view
4517                else if self.view_mode == ViewMode::Detail {
4518                    self.view_mode = ViewMode::List;
4519                    self.log_groups_state.stream_filter.clear();
4520                    self.log_groups_state.exact_match = false;
4521                    self.log_groups_state.show_expired = false;
4522                }
4523            }
4524            Action::OpenInConsole | Action::OpenInBrowser => {
4525                let url = self.get_console_url();
4526                let _ = webbrowser::open(&url);
4527            }
4528            Action::ShowHelp => {
4529                self.mode = Mode::HelpModal;
4530            }
4531            Action::OpenRegionPicker => {
4532                self.region_filter.clear();
4533                self.region_picker_selected = 0;
4534                self.mode = Mode::RegionPicker;
4535            }
4536            Action::OpenProfilePicker => {
4537                self.profile_filter.clear();
4538                self.profile_picker_selected = 0;
4539                self.available_profiles = Self::load_aws_profiles();
4540                self.mode = Mode::ProfilePicker;
4541            }
4542            Action::OpenCalendar => {
4543                self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
4544                self.calendar_selecting = CalendarField::StartDate;
4545                self.mode = Mode::CalendarPicker;
4546            }
4547            Action::CloseCalendar => {
4548                self.mode = Mode::Normal;
4549                self.calendar_date = None;
4550            }
4551            Action::CalendarPrevDay => {
4552                if let Some(date) = self.calendar_date {
4553                    self.calendar_date = date.checked_sub(time::Duration::days(1));
4554                }
4555            }
4556            Action::CalendarNextDay => {
4557                if let Some(date) = self.calendar_date {
4558                    self.calendar_date = date.checked_add(time::Duration::days(1));
4559                }
4560            }
4561            Action::CalendarPrevWeek => {
4562                if let Some(date) = self.calendar_date {
4563                    self.calendar_date = date.checked_sub(time::Duration::weeks(1));
4564                }
4565            }
4566            Action::CalendarNextWeek => {
4567                if let Some(date) = self.calendar_date {
4568                    self.calendar_date = date.checked_add(time::Duration::weeks(1));
4569                }
4570            }
4571            Action::CalendarPrevMonth => {
4572                if let Some(date) = self.calendar_date {
4573                    self.calendar_date = Some(if date.month() == time::Month::January {
4574                        date.replace_month(time::Month::December)
4575                            .unwrap()
4576                            .replace_year(date.year() - 1)
4577                            .unwrap()
4578                    } else {
4579                        date.replace_month(date.month().previous()).unwrap()
4580                    });
4581                }
4582            }
4583            Action::CalendarNextMonth => {
4584                if let Some(date) = self.calendar_date {
4585                    self.calendar_date = Some(if date.month() == time::Month::December {
4586                        date.replace_month(time::Month::January)
4587                            .unwrap()
4588                            .replace_year(date.year() + 1)
4589                            .unwrap()
4590                    } else {
4591                        date.replace_month(date.month().next()).unwrap()
4592                    });
4593                }
4594            }
4595            Action::CalendarSelect => {
4596                if let Some(date) = self.calendar_date {
4597                    let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
4598                        .unix_timestamp()
4599                        * 1000;
4600                    match self.calendar_selecting {
4601                        CalendarField::StartDate => {
4602                            self.log_groups_state.start_time = Some(timestamp);
4603                            self.calendar_selecting = CalendarField::EndDate;
4604                        }
4605                        CalendarField::EndDate => {
4606                            self.log_groups_state.end_time = Some(timestamp);
4607                            self.mode = Mode::Normal;
4608                            self.calendar_date = None;
4609                        }
4610                    }
4611                }
4612            }
4613        }
4614    }
4615
4616    pub fn filtered_services(&self) -> Vec<&'static str> {
4617        let mut services = if self.service_picker.filter.is_empty() {
4618            self.service_picker.services.clone()
4619        } else {
4620            self.service_picker
4621                .services
4622                .iter()
4623                .filter(|s| {
4624                    s.to_lowercase()
4625                        .contains(&self.service_picker.filter.to_lowercase())
4626                })
4627                .copied()
4628                .collect()
4629        };
4630        services.sort();
4631        services
4632    }
4633
4634    pub fn breadcrumbs(&self) -> String {
4635        if !self.service_selected {
4636            return String::new();
4637        }
4638
4639        let mut parts = vec![];
4640
4641        match self.current_service {
4642            Service::CloudWatchLogGroups => {
4643                parts.push("CloudWatch".to_string());
4644                parts.push("Log groups".to_string());
4645
4646                if self.view_mode != ViewMode::List {
4647                    if let Some(group) = selected_log_group(self) {
4648                        parts.push(group.name.clone());
4649                    }
4650                }
4651
4652                if self.view_mode == ViewMode::Events {
4653                    if let Some(stream) = self
4654                        .log_groups_state
4655                        .log_streams
4656                        .get(self.log_groups_state.selected_stream)
4657                    {
4658                        parts.push(stream.name.clone());
4659                    }
4660                }
4661            }
4662            Service::CloudWatchInsights => {
4663                parts.push("CloudWatch".to_string());
4664                parts.push("Insights".to_string());
4665            }
4666            Service::CloudWatchAlarms => {
4667                parts.push("CloudWatch".to_string());
4668                parts.push("Alarms".to_string());
4669            }
4670            Service::CloudTrailEvents => {
4671                parts.push("CloudTrail".to_string());
4672                parts.push("Event History".to_string());
4673            }
4674            Service::S3Buckets => {
4675                parts.push("S3".to_string());
4676                if let Some(bucket) = &self.s3_state.current_bucket {
4677                    parts.push(bucket.clone());
4678                    if let Some(prefix) = self.s3_state.prefix_stack.last() {
4679                        parts.push(prefix.trim_end_matches('/').to_string());
4680                    }
4681                } else {
4682                    parts.push("Buckets".to_string());
4683                }
4684            }
4685            Service::SqsQueues => {
4686                parts.push("SQS".to_string());
4687                parts.push("Queues".to_string());
4688            }
4689            Service::EcrRepositories => {
4690                parts.push("ECR".to_string());
4691                if let Some(repo) = &self.ecr_state.current_repository {
4692                    parts.push(repo.clone());
4693                } else {
4694                    parts.push("Repositories".to_string());
4695                }
4696            }
4697            Service::LambdaFunctions => {
4698                parts.push("Lambda".to_string());
4699                if let Some(func) = &self.lambda_state.current_function {
4700                    parts.push(func.clone());
4701                } else {
4702                    parts.push("Functions".to_string());
4703                }
4704            }
4705            Service::LambdaApplications => {
4706                parts.push("Lambda".to_string());
4707                parts.push("Applications".to_string());
4708            }
4709            Service::CloudFormationStacks => {
4710                parts.push("CloudFormation".to_string());
4711                if let Some(stack_name) = &self.cfn_state.current_stack {
4712                    parts.push(stack_name.clone());
4713                } else {
4714                    parts.push("Stacks".to_string());
4715                }
4716            }
4717            Service::IamUsers => {
4718                parts.push("IAM".to_string());
4719                parts.push("Users".to_string());
4720            }
4721            Service::IamRoles => {
4722                parts.push("IAM".to_string());
4723                parts.push("Roles".to_string());
4724                if let Some(role_name) = &self.iam_state.current_role {
4725                    parts.push(role_name.clone());
4726                    if let Some(policy_name) = &self.iam_state.current_policy {
4727                        parts.push(policy_name.clone());
4728                    }
4729                }
4730            }
4731            Service::IamUserGroups => {
4732                parts.push("IAM".to_string());
4733                parts.push("User Groups".to_string());
4734                if let Some(group_name) = &self.iam_state.current_group {
4735                    parts.push(group_name.clone());
4736                }
4737            }
4738            Service::Ec2Instances => {
4739                parts.push("EC2".to_string());
4740                parts.push("Instances".to_string());
4741            }
4742            Service::ApiGatewayApis => {
4743                parts.push("API Gateway".to_string());
4744                if let Some(api) = &self.apig_state.current_api {
4745                    parts.push(api.name.clone());
4746                } else {
4747                    parts.push("APIs".to_string());
4748                }
4749            }
4750        }
4751
4752        parts.join(" > ")
4753    }
4754
4755    pub fn update_current_tab_breadcrumb(&mut self) {
4756        if !self.tabs.is_empty() {
4757            self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
4758        }
4759    }
4760
4761    pub fn get_console_url(&self) -> String {
4762        use crate::{cfn, cw, ecr, iam, lambda, s3};
4763
4764        match self.current_service {
4765            Service::CloudWatchLogGroups => {
4766                if self.view_mode == ViewMode::Events {
4767                    if let Some(group) = selected_log_group(self) {
4768                        if let Some(stream) = self
4769                            .log_groups_state
4770                            .log_streams
4771                            .get(self.log_groups_state.selected_stream)
4772                        {
4773                            return cw::logs::console_url_stream(
4774                                &self.config.region,
4775                                &group.name,
4776                                &stream.name,
4777                            );
4778                        }
4779                    }
4780                } else if self.view_mode == ViewMode::Detail {
4781                    if let Some(group) = selected_log_group(self) {
4782                        return cw::logs::console_url_detail(&self.config.region, &group.name);
4783                    }
4784                }
4785                cw::logs::console_url_list(&self.config.region)
4786            }
4787            Service::CloudWatchInsights => cw::insights::console_url(
4788                &self.config.region,
4789                &self.config.account_id,
4790                &self.insights_state.insights.query_text,
4791                &self.insights_state.insights.selected_log_groups,
4792            ),
4793            Service::CloudWatchAlarms => {
4794                let view_type = match self.alarms_state.view_as {
4795                    AlarmViewMode::Table | AlarmViewMode::Detail => "table",
4796                    AlarmViewMode::Cards => "card",
4797                };
4798                cw::alarms::console_url(
4799                    &self.config.region,
4800                    view_type,
4801                    self.alarms_state.table.page_size.value(),
4802                    &self.alarms_state.sort_column,
4803                    self.alarms_state.sort_direction.as_str(),
4804                )
4805            }
4806            Service::CloudTrailEvents => {
4807                if let Some(event) = &self.cloudtrail_state.current_event {
4808                    format!(
4809                        "https://{}.console.aws.amazon.com/cloudtrailv2/home?region={}#/events/{}",
4810                        self.config.region, self.config.region, event.event_id
4811                    )
4812                } else {
4813                    format!(
4814                        "https://{}.console.aws.amazon.com/cloudtrail/home?region={}#/events",
4815                        self.config.region, self.config.region
4816                    )
4817                }
4818            }
4819            Service::S3Buckets => {
4820                if let Some(bucket_name) = &self.s3_state.current_bucket {
4821                    let prefix = self.s3_state.prefix_stack.join("");
4822                    s3::console_url_bucket(&self.config.region, bucket_name, &prefix)
4823                } else {
4824                    s3::console_url_buckets(&self.config.region)
4825                }
4826            }
4827            Service::SqsQueues => {
4828                if let Some(queue_url) = &self.sqs_state.current_queue {
4829                    console_url_queue_detail(&self.config.region, queue_url)
4830                } else {
4831                    console_url_queues(&self.config.region)
4832                }
4833            }
4834            Service::EcrRepositories => {
4835                if let Some(repo_name) = &self.ecr_state.current_repository {
4836                    ecr::console_url_private_repository(
4837                        &self.config.region,
4838                        &self.config.account_id,
4839                        repo_name,
4840                    )
4841                } else {
4842                    ecr::console_url_repositories(&self.config.region)
4843                }
4844            }
4845            Service::LambdaFunctions => {
4846                if let Some(func_name) = &self.lambda_state.current_function {
4847                    if let Some(version) = &self.lambda_state.current_version {
4848                        lambda::console_url_function_version(
4849                            &self.config.region,
4850                            func_name,
4851                            version,
4852                            &self.lambda_state.detail_tab,
4853                        )
4854                    } else {
4855                        lambda::console_url_function_detail(&self.config.region, func_name)
4856                    }
4857                } else {
4858                    lambda::console_url_functions(&self.config.region)
4859                }
4860            }
4861            Service::LambdaApplications => {
4862                if let Some(app_name) = &self.lambda_application_state.current_application {
4863                    lambda::console_url_application_detail(
4864                        &self.config.region,
4865                        app_name,
4866                        &self.lambda_application_state.detail_tab,
4867                    )
4868                } else {
4869                    lambda::console_url_applications(&self.config.region)
4870                }
4871            }
4872            Service::CloudFormationStacks => {
4873                if let Some(stack_name) = &self.cfn_state.current_stack {
4874                    if let Some(stack) = self
4875                        .cfn_state
4876                        .table
4877                        .items
4878                        .iter()
4879                        .find(|s| &s.name == stack_name)
4880                    {
4881                        return cfn::console_url_stack_detail_with_tab(
4882                            &self.config.region,
4883                            &stack.stack_id,
4884                            &self.cfn_state.detail_tab,
4885                        );
4886                    }
4887                }
4888                cfn::console_url_stacks(&self.config.region)
4889            }
4890            Service::IamUsers => {
4891                if let Some(user_name) = &self.iam_state.current_user {
4892                    let section = match self.iam_state.user_tab {
4893                        UserTab::Permissions => "permissions",
4894                        UserTab::Groups => "groups",
4895                        UserTab::Tags => "tags",
4896                        UserTab::SecurityCredentials => "security_credentials",
4897                        UserTab::LastAccessed => "access_advisor",
4898                    };
4899                    iam::console_url_user_detail(&self.config.region, user_name, section)
4900                } else {
4901                    iam::console_url_users(&self.config.region)
4902                }
4903            }
4904            Service::IamRoles => {
4905                if let Some(policy_name) = &self.iam_state.current_policy {
4906                    if let Some(role_name) = &self.iam_state.current_role {
4907                        return iam::console_url_role_policy(
4908                            &self.config.region,
4909                            role_name,
4910                            policy_name,
4911                        );
4912                    }
4913                }
4914                if let Some(role_name) = &self.iam_state.current_role {
4915                    let section = match self.iam_state.role_tab {
4916                        RoleTab::Permissions => "permissions",
4917                        RoleTab::TrustRelationships => "trust_relationships",
4918                        RoleTab::Tags => "tags",
4919                        RoleTab::LastAccessed => "access_advisor",
4920                        RoleTab::RevokeSessions => "revoke_sessions",
4921                    };
4922                    iam::console_url_role_detail(&self.config.region, role_name, section)
4923                } else {
4924                    iam::console_url_roles(&self.config.region)
4925                }
4926            }
4927            Service::IamUserGroups => iam::console_url_groups(&self.config.region),
4928            Service::Ec2Instances => {
4929                if let Some(instance_id) = &self.ec2_state.current_instance {
4930                    format!(
4931                        "https://{}.console.aws.amazon.com/ec2/home?region={}#InstanceDetails:instanceId={}",
4932                        self.config.region, self.config.region, instance_id
4933                    )
4934                } else {
4935                    format!(
4936                        "https://{}.console.aws.amazon.com/ec2/home?region={}#Instances:",
4937                        self.config.region, self.config.region
4938                    )
4939                }
4940            }
4941            Service::ApiGatewayApis => {
4942                use crate::apig;
4943                if let Some(api) = &self.apig_state.current_api {
4944                    if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
4945                        let protocol = api.protocol_type.to_uppercase();
4946                        if protocol == "REST" {
4947                            // Resources view - use filtered items to get correct resource_id
4948                            let (filtered_items, _) = crate::ui::apig::filter_tree_items(
4949                                &self.apig_state.resources.items,
4950                                &self.apig_state.resource_children,
4951                                &self.apig_state.route_filter,
4952                            );
4953
4954                            let resource_id =
4955                                if self.apig_state.resources.selected < filtered_items.len() {
4956                                    // Find the actual resource at this row in the filtered tree
4957                                    let mut current_row = 0;
4958                                    self.find_resource_at_row(
4959                                        &filtered_items,
4960                                        self.apig_state.resources.selected,
4961                                        &mut current_row,
4962                                    )
4963                                } else {
4964                                    None
4965                                };
4966                            apig::console_url_resources(
4967                                &self.config.region,
4968                                &api.id,
4969                                resource_id.as_deref(),
4970                            )
4971                        } else {
4972                            // Routes view - use filtered items to get correct route_id
4973                            let (filtered_items, filtered_children) =
4974                                crate::ui::apig::filter_tree_items(
4975                                    &self.apig_state.routes.items,
4976                                    &self.apig_state.route_children,
4977                                    &self.apig_state.route_filter,
4978                                );
4979
4980                            let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
4981                                &filtered_items,
4982                                &self.apig_state.expanded_routes,
4983                                &filtered_children,
4984                            );
4985
4986                            let route_id = if self.apig_state.routes.selected < total_rows {
4987                                // Find the actual route_id at this row in the filtered tree
4988                                let mut current_row = 0;
4989                                self.find_route_id_at_row_with_children(
4990                                    &filtered_items,
4991                                    &filtered_children,
4992                                    self.apig_state.routes.selected,
4993                                    &mut current_row,
4994                                )
4995                            } else {
4996                                None
4997                            };
4998                            apig::console_url_routes(
4999                                &self.config.region,
5000                                &api.id,
5001                                route_id.as_deref(),
5002                            )
5003                        }
5004                    } else {
5005                        apig::console_url_api(&self.config.region, &api.id)
5006                    }
5007                } else {
5008                    apig::console_url_apis(&self.config.region)
5009                }
5010            }
5011        }
5012    }
5013
5014    pub fn calculate_total_bucket_rows(&self) -> usize {
5015        calculate_total_bucket_rows(self)
5016    }
5017
5018    fn calculate_total_object_rows(&self) -> usize {
5019        calculate_total_object_rows(self)
5020    }
5021
5022    fn get_column_selector_max(&self) -> usize {
5023        if self.current_service == Service::ApiGatewayApis {
5024            self.apig_api_column_ids.len() + 6
5025        } else if self.current_service == Service::S3Buckets
5026            && self.s3_state.current_bucket.is_none()
5027        {
5028            self.s3_bucket_column_ids.len() + 6
5029        } else if self.view_mode == ViewMode::Events {
5030            self.cw_log_event_column_ids.len() - 1
5031        } else if self.view_mode == ViewMode::Detail {
5032            self.cw_log_stream_column_ids.len() + 6
5033        } else if self.current_service == Service::CloudWatchAlarms {
5034            29
5035        } else if self.current_service == Service::CloudTrailEvents {
5036            if self.cloudtrail_state.current_event.is_some()
5037                && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
5038            {
5039                self.cloudtrail_resource_column_ids.len()
5040            } else {
5041                self.cloudtrail_event_column_ids.len() + 6
5042            }
5043        } else if self.current_service == Service::Ec2Instances {
5044            if self.ec2_state.current_instance.is_some()
5045                && self.ec2_state.detail_tab == Ec2DetailTab::Tags
5046            {
5047                self.ec2_state.tag_column_ids.len() + 6
5048            } else {
5049                self.ec2_column_ids.len() + 6
5050            }
5051        } else if self.current_service == Service::EcrRepositories {
5052            if self.ecr_state.current_repository.is_some() {
5053                self.ecr_image_column_ids.len() + 6
5054            } else {
5055                self.ecr_repo_column_ids.len() + 6
5056            }
5057        } else if self.current_service == Service::SqsQueues {
5058            self.sqs_column_ids.len() - 1
5059        } else if self.current_service == Service::LambdaFunctions {
5060            self.lambda_state.function_column_ids.len() + 6
5061        } else if self.current_service == Service::LambdaApplications {
5062            self.lambda_application_column_ids.len() + 5
5063        } else if self.current_service == Service::CloudFormationStacks {
5064            self.cfn_column_ids.len() + 6
5065        } else if self.current_service == Service::IamUsers {
5066            if self.iam_state.current_user.is_some() {
5067                self.iam_policy_column_ids.len() + 5
5068            } else {
5069                self.iam_user_column_ids.len() + 5
5070            }
5071        } else if self.current_service == Service::IamRoles {
5072            if self.iam_state.current_role.is_some() {
5073                self.iam_policy_column_ids.len() + 5
5074            } else {
5075                self.iam_role_column_ids.len() + 5
5076            }
5077        } else {
5078            self.cw_log_group_column_ids.len() + 6
5079        }
5080    }
5081
5082    fn get_column_count(&self) -> usize {
5083        if self.current_service == Service::ApiGatewayApis {
5084            self.apig_api_column_ids.len()
5085        } else if self.current_service == Service::S3Buckets
5086            && self.s3_state.current_bucket.is_none()
5087        {
5088            self.s3_bucket_column_ids.len()
5089        } else if self.view_mode == ViewMode::Events {
5090            self.cw_log_event_column_ids.len()
5091        } else if self.view_mode == ViewMode::Detail {
5092            self.cw_log_stream_column_ids.len()
5093        } else if self.current_service == Service::CloudWatchAlarms {
5094            14
5095        } else if self.current_service == Service::CloudTrailEvents {
5096            if self.cloudtrail_state.current_event.is_some()
5097                && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
5098            {
5099                self.cloudtrail_resource_column_ids.len()
5100            } else {
5101                self.cloudtrail_event_column_ids.len()
5102            }
5103        } else if self.current_service == Service::Ec2Instances {
5104            if self.ec2_state.current_instance.is_some()
5105                && self.ec2_state.detail_tab == Ec2DetailTab::Tags
5106            {
5107                self.ec2_state.tag_column_ids.len()
5108            } else {
5109                self.ec2_column_ids.len()
5110            }
5111        } else if self.current_service == Service::EcrRepositories {
5112            if self.ecr_state.current_repository.is_some() {
5113                self.ecr_image_column_ids.len()
5114            } else {
5115                self.ecr_repo_column_ids.len()
5116            }
5117        } else if self.current_service == Service::SqsQueues {
5118            self.sqs_column_ids.len()
5119        } else if self.current_service == Service::LambdaFunctions {
5120            self.lambda_state.function_column_ids.len()
5121        } else if self.current_service == Service::LambdaApplications {
5122            self.lambda_application_column_ids.len()
5123        } else if self.current_service == Service::CloudFormationStacks {
5124            self.cfn_column_ids.len()
5125        } else if self.current_service == Service::IamUsers {
5126            if self.iam_state.current_user.is_some() {
5127                self.iam_policy_column_ids.len()
5128            } else {
5129                self.iam_user_column_ids.len()
5130            }
5131        } else if self.current_service == Service::IamRoles {
5132            if self.iam_state.current_role.is_some() {
5133                self.iam_policy_column_ids.len()
5134            } else {
5135                self.iam_role_column_ids.len()
5136            }
5137        } else {
5138            self.cw_log_group_column_ids.len()
5139        }
5140    }
5141
5142    fn is_blank_row_index(&self, idx: usize) -> bool {
5143        let column_count = self.get_column_count();
5144        // Blank row is at: header(0) + columns(1..=column_count) + blank(column_count+1)
5145        idx == column_count + 1
5146    }
5147
5148    fn next_item(&mut self) {
5149        match self.mode {
5150            Mode::FilterInput => {
5151                if self.current_service == Service::S3Buckets
5152                    && self.s3_state.input_focus == InputFocus::Pagination
5153                {
5154                    // Navigate to next page
5155                    let page_size = self.s3_state.buckets.page_size.value();
5156                    let total_rows = crate::ui::s3::calculate_filtered_bucket_rows(self);
5157                    let max_offset = total_rows.saturating_sub(page_size);
5158                    self.s3_state.selected_row =
5159                        (self.s3_state.selected_row + page_size).min(max_offset);
5160                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5161                } else if self.current_service == Service::CloudTrailEvents
5162                    && self.cloudtrail_state.input_focus == InputFocus::Pagination
5163                {
5164                    // Navigate to next page
5165                    let page_size = self.cloudtrail_state.table.page_size.value();
5166                    let total_items = self.cloudtrail_state.table.items.len();
5167                    let current_page = self.cloudtrail_state.table.selected / page_size;
5168                    let total_pages = total_items.div_ceil(page_size);
5169                    if current_page + 1 < total_pages {
5170                        self.cloudtrail_state.table.selected = (current_page + 1) * page_size;
5171                    }
5172                } else if self.current_service == Service::CloudFormationStacks {
5173                    use crate::ui::cfn::STATUS_FILTER;
5174                    if self.cfn_state.input_focus == STATUS_FILTER {
5175                        self.cfn_state.status_filter = self.cfn_state.status_filter.next();
5176                        self.cfn_state.table.reset();
5177                    }
5178                } else if self.current_service == Service::IamUsers
5179                    && self.iam_state.current_user.is_some()
5180                {
5181                    use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
5182                    if self.iam_state.user_tab == UserTab::Permissions
5183                        && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
5184                    {
5185                        self.cycle_policy_type_next();
5186                    } else if self.iam_state.user_tab == UserTab::LastAccessed
5187                        && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
5188                    {
5189                        self.iam_state.last_accessed_history_filter =
5190                            self.iam_state.last_accessed_history_filter.next();
5191                        self.iam_state.last_accessed_services.reset();
5192                    }
5193                } else if self.current_service == Service::IamRoles
5194                    && self.iam_state.current_role.is_some()
5195                    && self.iam_state.role_tab == RoleTab::Permissions
5196                {
5197                    use crate::ui::iam::POLICY_TYPE_DROPDOWN;
5198                    if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
5199                        self.cycle_policy_type_next();
5200                    }
5201                } else if self.current_service == Service::Ec2Instances {
5202                    if self.ec2_state.input_focus == EC2_STATE_FILTER {
5203                        self.ec2_state.state_filter = self.ec2_state.state_filter.next();
5204                        self.ec2_state.table.reset();
5205                    }
5206                } else if self.current_service == Service::SqsQueues {
5207                    use crate::ui::sqs::SUBSCRIPTION_REGION;
5208                    if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
5209                        let regions = AwsRegion::all();
5210                        self.sqs_state.subscription_region_selected =
5211                            (self.sqs_state.subscription_region_selected + 1)
5212                                .min(regions.len() - 1);
5213                        self.sqs_state.subscriptions.reset();
5214                    }
5215                }
5216            }
5217            Mode::RegionPicker => {
5218                let filtered = self.get_filtered_regions();
5219                if !filtered.is_empty() {
5220                    self.region_picker_selected =
5221                        (self.region_picker_selected + 1).min(filtered.len() - 1);
5222                }
5223            }
5224            Mode::ProfilePicker => {
5225                let filtered = self.get_filtered_profiles();
5226                if !filtered.is_empty() {
5227                    self.profile_picker_selected =
5228                        (self.profile_picker_selected + 1).min(filtered.len() - 1);
5229                }
5230            }
5231            Mode::SessionPicker => {
5232                let filtered = self.get_filtered_sessions();
5233                if !filtered.is_empty() {
5234                    self.session_picker_selected =
5235                        (self.session_picker_selected + 1).min(filtered.len() - 1);
5236                }
5237            }
5238            Mode::InsightsInput => {
5239                use crate::app::InsightsFocus;
5240                if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
5241                    && self.insights_state.insights.show_dropdown
5242                    && !self.insights_state.insights.log_group_matches.is_empty()
5243                {
5244                    let max = self.insights_state.insights.log_group_matches.len() - 1;
5245                    self.insights_state.insights.dropdown_selected =
5246                        (self.insights_state.insights.dropdown_selected + 1).min(max);
5247                }
5248            }
5249            Mode::ColumnSelector => {
5250                let max = self.get_column_selector_max();
5251                let mut next_idx = (self.column_selector_index + 1).min(max);
5252                // Skip blank row
5253                if self.is_blank_row_index(next_idx) {
5254                    next_idx = (next_idx + 1).min(max);
5255                }
5256                self.column_selector_index = next_idx;
5257            }
5258            Mode::ServicePicker => {
5259                let filtered = self.filtered_services();
5260                if !filtered.is_empty() {
5261                    self.service_picker.selected =
5262                        (self.service_picker.selected + 1).min(filtered.len() - 1);
5263                }
5264            }
5265            Mode::TabPicker => {
5266                let filtered = self.get_filtered_tabs();
5267                if !filtered.is_empty() {
5268                    self.tab_picker_selected =
5269                        (self.tab_picker_selected + 1).min(filtered.len() - 1);
5270                }
5271            }
5272            Mode::Normal => {
5273                if !self.service_selected {
5274                    let filtered = self.filtered_services();
5275                    if !filtered.is_empty() {
5276                        self.service_picker.selected =
5277                            (self.service_picker.selected + 1).min(filtered.len() - 1);
5278                    }
5279                } else if self.current_service == Service::S3Buckets {
5280                    if self.s3_state.current_bucket.is_some() {
5281                        if self.s3_state.object_tab == S3ObjectTab::Properties {
5282                            // Scroll properties view
5283                            self.s3_state.properties_scroll =
5284                                self.s3_state.properties_scroll.saturating_add(1);
5285                        } else {
5286                            // Calculate total rows including all nested preview items
5287                            let total_rows = self.calculate_total_object_rows();
5288                            let max = total_rows.saturating_sub(1);
5289                            self.s3_state.selected_object =
5290                                (self.s3_state.selected_object + 1).min(max);
5291
5292                            // Adjust scroll offset if selection goes below viewport
5293                            let visible_rows = self.s3_state.object_visible_rows.get();
5294                            if self.s3_state.selected_object
5295                                >= self.s3_state.object_scroll_offset + visible_rows
5296                            {
5297                                self.s3_state.object_scroll_offset =
5298                                    self.s3_state.selected_object - visible_rows + 1;
5299                            }
5300                        }
5301                    } else {
5302                        // Navigate rows in bucket list
5303                        let total_rows = crate::ui::s3::calculate_filtered_bucket_rows(self);
5304                        if total_rows > 0 {
5305                            self.s3_state.selected_row =
5306                                (self.s3_state.selected_row + 1).min(total_rows - 1);
5307
5308                            // Adjust scroll offset if selection goes below viewport
5309                            let visible_rows = self.s3_state.bucket_visible_rows.get();
5310                            if self.s3_state.selected_row
5311                                >= self.s3_state.bucket_scroll_offset + visible_rows
5312                            {
5313                                self.s3_state.bucket_scroll_offset =
5314                                    self.s3_state.selected_row - visible_rows + 1;
5315                            }
5316                        }
5317                    }
5318                } else if self.view_mode == ViewMode::InsightsResults {
5319                    let max = self
5320                        .insights_state
5321                        .insights
5322                        .query_results
5323                        .len()
5324                        .saturating_sub(1);
5325                    if self.insights_state.insights.results_selected < max {
5326                        self.insights_state.insights.results_selected += 1;
5327                    }
5328                } else if self.current_service == Service::ApiGatewayApis {
5329                    if let Some(api) = &self.apig_state.current_api {
5330                        if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
5331                            let protocol = api.protocol_type.to_uppercase();
5332                            if protocol == "REST" {
5333                                // Navigate resources tree
5334                                let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
5335                                    &self.apig_state.resources.items,
5336                                    &self.apig_state.expanded_resources,
5337                                    &self.apig_state.resource_children,
5338                                );
5339                                if total_rows > 0 {
5340                                    self.apig_state.resources.selected =
5341                                        (self.apig_state.resources.selected + 1)
5342                                            .min(total_rows - 1);
5343                                }
5344                            } else {
5345                                // Navigate routes tree
5346                                let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
5347                                    &self.apig_state.routes.items,
5348                                    &self.apig_state.expanded_routes,
5349                                    &self.apig_state.route_children,
5350                                );
5351                                if total_rows > 0 {
5352                                    self.apig_state.routes.selected =
5353                                        (self.apig_state.routes.selected + 1).min(total_rows - 1);
5354                                }
5355                            }
5356                        }
5357                    } else {
5358                        let filtered = crate::ui::apig::filtered_apis(self);
5359                        if !filtered.is_empty() {
5360                            self.apig_state.apis.next_item(filtered.len());
5361                        }
5362                    }
5363                } else if self.view_mode == ViewMode::PolicyView {
5364                    let lines = self.iam_state.policy_document.lines().count();
5365                    let max_scroll = lines.saturating_sub(1);
5366                    self.iam_state.policy_scroll =
5367                        (self.iam_state.policy_scroll + 1).min(max_scroll);
5368                } else if self.current_service == Service::CloudFormationStacks
5369                    && self.cfn_state.current_stack.is_some()
5370                    && self.cfn_state.detail_tab == CfnDetailTab::Template
5371                {
5372                    let lines = self.cfn_state.template_body.lines().count();
5373                    let max_scroll = lines.saturating_sub(1);
5374                    self.cfn_state.template_scroll =
5375                        (self.cfn_state.template_scroll + 1).min(max_scroll);
5376                } else if self.current_service == Service::SqsQueues
5377                    && self.sqs_state.current_queue.is_some()
5378                    && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
5379                {
5380                    let lines = self.sqs_state.policy_document.lines().count();
5381                    let max_scroll = lines.saturating_sub(1);
5382                    self.sqs_state.policy_scroll =
5383                        (self.sqs_state.policy_scroll + 1).min(max_scroll);
5384                } else if self.current_service == Service::LambdaFunctions
5385                    && self.lambda_state.current_function.is_some()
5386                    && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
5387                    && !self.lambda_state.is_metrics_loading()
5388                {
5389                    self.lambda_state
5390                        .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
5391                } else if self.current_service == Service::Ec2Instances
5392                    && self.ec2_state.current_instance.is_some()
5393                    && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
5394                    && !self.ec2_state.is_metrics_loading()
5395                {
5396                    self.ec2_state
5397                        .set_monitoring_scroll((self.ec2_state.monitoring_scroll() + 1).min(5));
5398                } else if self.current_service == Service::SqsQueues
5399                    && self.sqs_state.current_queue.is_some()
5400                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
5401                    && !self.sqs_state.is_metrics_loading()
5402                {
5403                    self.sqs_state
5404                        .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
5405                } else if self.view_mode == ViewMode::Events {
5406                    let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
5407                    if self.log_groups_state.event_scroll_offset >= max_scroll {
5408                        // At the end, do nothing
5409                    } else {
5410                        self.log_groups_state.event_scroll_offset =
5411                            (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
5412                    }
5413                } else if self.current_service == Service::CloudWatchLogGroups {
5414                    if self.view_mode == ViewMode::List {
5415                        let filtered_groups = filtered_log_groups(self);
5416                        self.log_groups_state
5417                            .log_groups
5418                            .next_item(filtered_groups.len());
5419                    } else if self.view_mode == ViewMode::Detail {
5420                        let filtered_streams = filtered_log_streams(self);
5421                        if !filtered_streams.is_empty() {
5422                            let max = filtered_streams.len() - 1;
5423                            if self.log_groups_state.selected_stream >= max {
5424                                // At the end, do nothing
5425                            } else {
5426                                self.log_groups_state.selected_stream =
5427                                    (self.log_groups_state.selected_stream + 1).min(max);
5428                                self.log_groups_state.expanded_stream = None;
5429                            }
5430                        }
5431                    }
5432                } else if self.current_service == Service::CloudWatchAlarms {
5433                    let filtered_alarms = match self.alarms_state.alarm_tab {
5434                        AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
5435                        AlarmTab::InAlarm => self
5436                            .alarms_state
5437                            .table
5438                            .items
5439                            .iter()
5440                            .filter(|a| a.state.to_uppercase() == "ALARM")
5441                            .count(),
5442                    };
5443                    if filtered_alarms > 0 {
5444                        self.alarms_state.table.next_item(filtered_alarms);
5445                    }
5446                } else if self.current_service == Service::CloudTrailEvents {
5447                    if self.cloudtrail_state.current_event.is_some()
5448                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
5449                    {
5450                        // Scroll JSON down
5451                        if let Some(event) = &self.cloudtrail_state.current_event {
5452                            let line_count = event.cloud_trail_event_json.lines().count();
5453                            let max_scroll = line_count.saturating_sub(1);
5454                            self.cloudtrail_state.event_json_scroll =
5455                                (self.cloudtrail_state.event_json_scroll + 1).min(max_scroll);
5456                        }
5457                    } else {
5458                        // For CloudTrail with infinite pagination, navigate within current page
5459                        let page_size = self.cloudtrail_state.table.page_size.value();
5460                        let current_page = self.cloudtrail_state.table.selected / page_size;
5461                        let page_start = current_page * page_size;
5462                        let page_end = page_start + page_size;
5463                        let filtered_count = self.cloudtrail_state.table.items.len();
5464
5465                        // Only navigate if within loaded items
5466                        if self.cloudtrail_state.table.selected < filtered_count.saturating_sub(1) {
5467                            self.cloudtrail_state.table.selected =
5468                                (self.cloudtrail_state.table.selected + 1)
5469                                    .min(page_end - 1)
5470                                    .min(filtered_count - 1);
5471                        }
5472                    }
5473                } else if self.current_service == Service::Ec2Instances {
5474                    if self.ec2_state.current_instance.is_some()
5475                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
5476                    {
5477                        let filtered = crate::ui::ec2::filtered_tags(self);
5478                        if !filtered.is_empty() {
5479                            self.ec2_state.tags.next_item(filtered.len());
5480                        }
5481                    } else {
5482                        let filtered: Vec<_> = self
5483                            .ec2_state
5484                            .table
5485                            .items
5486                            .iter()
5487                            .filter(|i| self.ec2_state.state_filter.matches(&i.state))
5488                            .filter(|i| {
5489                                if self.ec2_state.table.filter.is_empty() {
5490                                    return true;
5491                                }
5492                                i.name.contains(&self.ec2_state.table.filter)
5493                                    || i.instance_id.contains(&self.ec2_state.table.filter)
5494                                    || i.state.contains(&self.ec2_state.table.filter)
5495                                    || i.instance_type.contains(&self.ec2_state.table.filter)
5496                                    || i.availability_zone.contains(&self.ec2_state.table.filter)
5497                                    || i.security_groups.contains(&self.ec2_state.table.filter)
5498                                    || i.key_name.contains(&self.ec2_state.table.filter)
5499                            })
5500                            .collect();
5501                        if !filtered.is_empty() {
5502                            self.ec2_state.table.next_item(filtered.len());
5503                        }
5504                    }
5505                } else if self.current_service == Service::EcrRepositories {
5506                    if self.ecr_state.current_repository.is_some() {
5507                        let filtered_images = filtered_ecr_images(self);
5508                        if !filtered_images.is_empty() {
5509                            self.ecr_state.images.next_item(filtered_images.len());
5510                        }
5511                    } else {
5512                        let filtered_repos = filtered_ecr_repositories(self);
5513                        if !filtered_repos.is_empty() {
5514                            self.ecr_state.repositories.selected =
5515                                (self.ecr_state.repositories.selected + 1)
5516                                    .min(filtered_repos.len() - 1);
5517                            self.ecr_state.repositories.snap_to_page();
5518                        }
5519                    }
5520                } else if self.current_service == Service::SqsQueues {
5521                    if self.sqs_state.current_queue.is_some()
5522                        && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
5523                    {
5524                        let filtered = filtered_lambda_triggers(self);
5525                        if !filtered.is_empty() {
5526                            self.sqs_state.triggers.next_item(filtered.len());
5527                        }
5528                    } else if self.sqs_state.current_queue.is_some()
5529                        && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
5530                    {
5531                        let filtered = filtered_eventbridge_pipes(self);
5532                        if !filtered.is_empty() {
5533                            self.sqs_state.pipes.next_item(filtered.len());
5534                        }
5535                    } else if self.sqs_state.current_queue.is_some()
5536                        && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
5537                    {
5538                        let filtered = filtered_tags(self);
5539                        if !filtered.is_empty() {
5540                            self.sqs_state.tags.next_item(filtered.len());
5541                        }
5542                    } else if self.sqs_state.current_queue.is_some()
5543                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
5544                    {
5545                        let filtered = filtered_subscriptions(self);
5546                        if !filtered.is_empty() {
5547                            self.sqs_state.subscriptions.next_item(filtered.len());
5548                        }
5549                    } else {
5550                        let filtered_queues = filtered_queues(
5551                            &self.sqs_state.queues.items,
5552                            &self.sqs_state.queues.filter,
5553                        );
5554                        if !filtered_queues.is_empty() {
5555                            self.sqs_state.queues.next_item(filtered_queues.len());
5556                        }
5557                    }
5558                } else if self.current_service == Service::LambdaFunctions {
5559                    if self.lambda_state.current_function.is_some()
5560                        && self.lambda_state.detail_tab == LambdaDetailTab::Code
5561                    {
5562                        // Layer table navigation in Code tab
5563                        if let Some(func_name) = &self.lambda_state.current_function {
5564                            if let Some(func) = self
5565                                .lambda_state
5566                                .table
5567                                .items
5568                                .iter()
5569                                .find(|f| f.name == *func_name)
5570                            {
5571                                let max = func.layers.len().saturating_sub(1);
5572                                if !func.layers.is_empty() {
5573                                    self.lambda_state.layer_selected =
5574                                        (self.lambda_state.layer_selected + 1).min(max);
5575                                }
5576                            }
5577                        }
5578                    } else if self.lambda_state.current_function.is_some()
5579                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
5580                    {
5581                        // Version table navigation
5582                        let filtered: Vec<_> = self
5583                            .lambda_state
5584                            .version_table
5585                            .items
5586                            .iter()
5587                            .filter(|v| {
5588                                self.lambda_state.version_table.filter.is_empty()
5589                                    || v.version.to_lowercase().contains(
5590                                        &self.lambda_state.version_table.filter.to_lowercase(),
5591                                    )
5592                                    || v.aliases.to_lowercase().contains(
5593                                        &self.lambda_state.version_table.filter.to_lowercase(),
5594                                    )
5595                                    || v.description.to_lowercase().contains(
5596                                        &self.lambda_state.version_table.filter.to_lowercase(),
5597                                    )
5598                            })
5599                            .collect();
5600                        if !filtered.is_empty() {
5601                            self.lambda_state.version_table.selected =
5602                                (self.lambda_state.version_table.selected + 1)
5603                                    .min(filtered.len() - 1);
5604                            self.lambda_state.version_table.snap_to_page();
5605                        }
5606                    } else if self.lambda_state.current_function.is_some()
5607                        && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
5608                            || (self.lambda_state.current_version.is_some()
5609                                && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
5610                    {
5611                        // Alias table navigation (both in Aliases tab and Version Configuration)
5612                        let version_filter = self.lambda_state.current_version.clone();
5613                        let filtered: Vec<_> = self
5614                            .lambda_state
5615                            .alias_table
5616                            .items
5617                            .iter()
5618                            .filter(|a| {
5619                                (version_filter.is_none()
5620                                    || a.versions.contains(version_filter.as_ref().unwrap()))
5621                                    && (self.lambda_state.alias_table.filter.is_empty()
5622                                        || a.name.to_lowercase().contains(
5623                                            &self.lambda_state.alias_table.filter.to_lowercase(),
5624                                        )
5625                                        || a.versions.to_lowercase().contains(
5626                                            &self.lambda_state.alias_table.filter.to_lowercase(),
5627                                        )
5628                                        || a.description.to_lowercase().contains(
5629                                            &self.lambda_state.alias_table.filter.to_lowercase(),
5630                                        ))
5631                            })
5632                            .collect();
5633                        if !filtered.is_empty() {
5634                            self.lambda_state.alias_table.selected =
5635                                (self.lambda_state.alias_table.selected + 1)
5636                                    .min(filtered.len() - 1);
5637                            self.lambda_state.alias_table.snap_to_page();
5638                        }
5639                    } else if self.lambda_state.current_function.is_none() {
5640                        let filtered = filtered_lambda_functions(self);
5641                        if !filtered.is_empty() {
5642                            self.lambda_state.table.next_item(filtered.len());
5643                            self.lambda_state.table.snap_to_page();
5644                        }
5645                    }
5646                } else if self.current_service == Service::LambdaApplications {
5647                    if self.lambda_application_state.current_application.is_some() {
5648                        if self.lambda_application_state.detail_tab
5649                            == LambdaApplicationDetailTab::Overview
5650                        {
5651                            let len = self.lambda_application_state.resources.items.len();
5652                            if len > 0 {
5653                                self.lambda_application_state.resources.next_item(len);
5654                            }
5655                        } else {
5656                            let len = self.lambda_application_state.deployments.items.len();
5657                            if len > 0 {
5658                                self.lambda_application_state.deployments.next_item(len);
5659                            }
5660                        }
5661                    } else {
5662                        let filtered = filtered_lambda_applications(self);
5663                        if !filtered.is_empty() {
5664                            self.lambda_application_state.table.selected =
5665                                (self.lambda_application_state.table.selected + 1)
5666                                    .min(filtered.len() - 1);
5667                            self.lambda_application_state.table.snap_to_page();
5668                        }
5669                    }
5670                } else if self.current_service == Service::CloudFormationStacks {
5671                    if self.cfn_state.current_stack.is_some()
5672                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5673                    {
5674                        let filtered = filtered_parameters(self);
5675                        self.cfn_state.parameters.next_item(filtered.len());
5676                    } else if self.cfn_state.current_stack.is_some()
5677                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5678                    {
5679                        let filtered = filtered_outputs(self);
5680                        self.cfn_state.outputs.next_item(filtered.len());
5681                    } else if self.cfn_state.current_stack.is_some()
5682                        && self.cfn_state.detail_tab == CfnDetailTab::Resources
5683                    {
5684                        let filtered = filtered_resources(self);
5685                        self.cfn_state.resources.next_item(filtered.len());
5686                    } else {
5687                        let filtered = filtered_cloudformation_stacks(self);
5688                        self.cfn_state.table.next_item(filtered.len());
5689                    }
5690                } else if self.current_service == Service::IamUsers {
5691                    if self.iam_state.current_user.is_some() {
5692                        if self.iam_state.user_tab == UserTab::Tags {
5693                            let filtered = filtered_user_tags(self);
5694                            if !filtered.is_empty() {
5695                                self.iam_state.user_tags.next_item(filtered.len());
5696                            }
5697                        } else {
5698                            let filtered = filtered_iam_policies(self);
5699                            if !filtered.is_empty() {
5700                                self.iam_state.policies.next_item(filtered.len());
5701                            }
5702                        }
5703                    } else {
5704                        let filtered = filtered_iam_users(self);
5705                        if !filtered.is_empty() {
5706                            self.iam_state.users.next_item(filtered.len());
5707                        }
5708                    }
5709                } else if self.current_service == Service::IamRoles {
5710                    if self.iam_state.current_role.is_some() {
5711                        if self.iam_state.role_tab == RoleTab::TrustRelationships {
5712                            let lines = self.iam_state.trust_policy_document.lines().count();
5713                            let max_scroll = lines.saturating_sub(1);
5714                            self.iam_state.trust_policy_scroll =
5715                                (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
5716                        } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
5717                            self.iam_state.revoke_sessions_scroll =
5718                                (self.iam_state.revoke_sessions_scroll + 1).min(19);
5719                        } else if self.iam_state.role_tab == RoleTab::Tags {
5720                            let filtered = filtered_iam_tags(self);
5721                            if !filtered.is_empty() {
5722                                self.iam_state.tags.next_item(filtered.len());
5723                            }
5724                        } else if self.iam_state.role_tab == RoleTab::LastAccessed {
5725                            let filtered = filtered_last_accessed(self);
5726                            if !filtered.is_empty() {
5727                                self.iam_state
5728                                    .last_accessed_services
5729                                    .next_item(filtered.len());
5730                            }
5731                        } else {
5732                            let filtered = filtered_iam_policies(self);
5733                            if !filtered.is_empty() {
5734                                self.iam_state.policies.next_item(filtered.len());
5735                            }
5736                        }
5737                    } else {
5738                        let filtered = filtered_iam_roles(self);
5739                        if !filtered.is_empty() {
5740                            self.iam_state.roles.next_item(filtered.len());
5741                        }
5742                    }
5743                } else if self.current_service == Service::IamUserGroups {
5744                    if self.iam_state.current_group.is_some() {
5745                        if self.iam_state.group_tab == GroupTab::Users {
5746                            let filtered: Vec<_> = self
5747                                .iam_state
5748                                .group_users
5749                                .items
5750                                .iter()
5751                                .filter(|u| {
5752                                    if self.iam_state.group_users.filter.is_empty() {
5753                                        true
5754                                    } else {
5755                                        u.user_name.to_lowercase().contains(
5756                                            &self.iam_state.group_users.filter.to_lowercase(),
5757                                        )
5758                                    }
5759                                })
5760                                .collect();
5761                            if !filtered.is_empty() {
5762                                self.iam_state.group_users.next_item(filtered.len());
5763                            }
5764                        } else if self.iam_state.group_tab == GroupTab::Permissions {
5765                            let filtered = filtered_iam_policies(self);
5766                            if !filtered.is_empty() {
5767                                self.iam_state.policies.next_item(filtered.len());
5768                            }
5769                        } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5770                            let filtered = filtered_last_accessed(self);
5771                            if !filtered.is_empty() {
5772                                self.iam_state
5773                                    .last_accessed_services
5774                                    .next_item(filtered.len());
5775                            }
5776                        }
5777                    } else {
5778                        let filtered: Vec<_> = self
5779                            .iam_state
5780                            .groups
5781                            .items
5782                            .iter()
5783                            .filter(|g| {
5784                                if self.iam_state.groups.filter.is_empty() {
5785                                    true
5786                                } else {
5787                                    g.group_name
5788                                        .to_lowercase()
5789                                        .contains(&self.iam_state.groups.filter.to_lowercase())
5790                                }
5791                            })
5792                            .collect();
5793                        if !filtered.is_empty() {
5794                            self.iam_state.groups.next_item(filtered.len());
5795                        }
5796                    }
5797                }
5798            }
5799            _ => {}
5800        }
5801    }
5802
5803    fn prev_item(&mut self) {
5804        match self.mode {
5805            Mode::FilterInput => {
5806                if self.current_service == Service::S3Buckets
5807                    && self.s3_state.input_focus == InputFocus::Pagination
5808                {
5809                    // Navigate to previous page
5810                    let page_size = self.s3_state.buckets.page_size.value();
5811                    self.s3_state.selected_row =
5812                        self.s3_state.selected_row.saturating_sub(page_size);
5813                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5814                } else if self.current_service == Service::CloudTrailEvents
5815                    && self.cloudtrail_state.input_focus == InputFocus::Pagination
5816                {
5817                    // Navigate to previous page
5818                    let page_size = self.cloudtrail_state.table.page_size.value();
5819                    let current_page = self.cloudtrail_state.table.selected / page_size;
5820                    if current_page > 0 {
5821                        self.cloudtrail_state.table.selected = (current_page - 1) * page_size;
5822                    }
5823                } else if self.current_service == Service::CloudFormationStacks {
5824                    use crate::ui::cfn::STATUS_FILTER;
5825                    if self.cfn_state.input_focus == STATUS_FILTER {
5826                        self.cfn_state.status_filter = self.cfn_state.status_filter.prev();
5827                        self.cfn_state.table.reset();
5828                    }
5829                } else if self.current_service == Service::IamUsers
5830                    && self.iam_state.current_user.is_some()
5831                {
5832                    use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
5833                    if self.iam_state.user_tab == UserTab::Permissions
5834                        && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
5835                    {
5836                        self.cycle_policy_type_prev();
5837                    } else if self.iam_state.user_tab == UserTab::LastAccessed
5838                        && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
5839                    {
5840                        self.iam_state.last_accessed_history_filter =
5841                            self.iam_state.last_accessed_history_filter.prev();
5842                        self.iam_state.last_accessed_services.reset();
5843                    }
5844                } else if self.current_service == Service::IamRoles
5845                    && self.iam_state.current_role.is_some()
5846                    && self.iam_state.role_tab == RoleTab::Permissions
5847                {
5848                    use crate::ui::iam::POLICY_TYPE_DROPDOWN;
5849                    if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
5850                        self.cycle_policy_type_prev();
5851                    }
5852                } else if self.current_service == Service::Ec2Instances {
5853                    if self.ec2_state.input_focus == EC2_STATE_FILTER {
5854                        self.ec2_state.state_filter = self.ec2_state.state_filter.prev();
5855                        self.ec2_state.table.reset();
5856                    }
5857                } else if self.current_service == Service::SqsQueues {
5858                    use crate::ui::sqs::SUBSCRIPTION_REGION;
5859                    if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
5860                        self.sqs_state.subscription_region_selected = self
5861                            .sqs_state
5862                            .subscription_region_selected
5863                            .saturating_sub(1);
5864                        self.sqs_state.subscriptions.reset();
5865                    }
5866                }
5867            }
5868            Mode::RegionPicker => {
5869                self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
5870            }
5871            Mode::ProfilePicker => {
5872                self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
5873            }
5874            Mode::SessionPicker => {
5875                self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
5876            }
5877            Mode::InsightsInput => {
5878                use crate::app::InsightsFocus;
5879                if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
5880                    && self.insights_state.insights.show_dropdown
5881                    && !self.insights_state.insights.log_group_matches.is_empty()
5882                {
5883                    self.insights_state.insights.dropdown_selected = self
5884                        .insights_state
5885                        .insights
5886                        .dropdown_selected
5887                        .saturating_sub(1);
5888                }
5889            }
5890            Mode::ColumnSelector => {
5891                let mut prev_idx = self.column_selector_index.saturating_sub(1);
5892                // Skip blank row
5893                if self.is_blank_row_index(prev_idx) {
5894                    prev_idx = prev_idx.saturating_sub(1);
5895                }
5896                self.column_selector_index = prev_idx;
5897            }
5898            Mode::ServicePicker => {
5899                self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
5900            }
5901            Mode::TabPicker => {
5902                self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
5903            }
5904            Mode::Normal => {
5905                if !self.service_selected {
5906                    self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
5907                } else if self.current_service == Service::S3Buckets {
5908                    if self.s3_state.current_bucket.is_some() {
5909                        if self.s3_state.object_tab == S3ObjectTab::Properties {
5910                            self.s3_state.properties_scroll =
5911                                self.s3_state.properties_scroll.saturating_sub(1);
5912                        } else {
5913                            self.s3_state.selected_object =
5914                                self.s3_state.selected_object.saturating_sub(1);
5915
5916                            // Adjust scroll offset if selection goes above viewport
5917                            if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5918                                self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5919                            }
5920                        }
5921                    } else {
5922                        self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
5923
5924                        // Adjust scroll offset if selection goes above viewport
5925                        if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5926                            self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5927                        }
5928                    }
5929                } else if self.current_service == Service::ApiGatewayApis {
5930                    if let Some(api) = &self.apig_state.current_api {
5931                        if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
5932                            let protocol = api.protocol_type.to_uppercase();
5933                            if protocol == "REST" {
5934                                // Navigate resources tree
5935                                if self.apig_state.resources.selected > 0 {
5936                                    self.apig_state.resources.selected -= 1;
5937                                }
5938                            } else {
5939                                // Navigate routes tree
5940                                if self.apig_state.routes.selected > 0 {
5941                                    self.apig_state.routes.selected -= 1;
5942                                }
5943                            }
5944                        }
5945                    } else {
5946                        self.apig_state.apis.prev_item();
5947                    }
5948                } else if self.view_mode == ViewMode::InsightsResults {
5949                    if self.insights_state.insights.results_selected > 0 {
5950                        self.insights_state.insights.results_selected -= 1;
5951                    }
5952                } else if self.view_mode == ViewMode::PolicyView {
5953                    self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
5954                } else if self.current_service == Service::CloudFormationStacks
5955                    && self.cfn_state.current_stack.is_some()
5956                    && self.cfn_state.detail_tab == CfnDetailTab::Template
5957                {
5958                    self.cfn_state.template_scroll =
5959                        self.cfn_state.template_scroll.saturating_sub(1);
5960                } else if self.current_service == Service::SqsQueues
5961                    && self.sqs_state.current_queue.is_some()
5962                    && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
5963                {
5964                    self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(1);
5965                } else if self.current_service == Service::LambdaFunctions
5966                    && self.lambda_state.current_function.is_some()
5967                    && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
5968                    && !self.lambda_state.is_metrics_loading()
5969                {
5970                    self.lambda_state.set_monitoring_scroll(
5971                        self.lambda_state.monitoring_scroll().saturating_sub(1),
5972                    );
5973                } else if self.current_service == Service::Ec2Instances
5974                    && self.ec2_state.current_instance.is_some()
5975                    && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
5976                    && !self.ec2_state.is_metrics_loading()
5977                {
5978                    self.ec2_state.set_monitoring_scroll(
5979                        self.ec2_state.monitoring_scroll().saturating_sub(1),
5980                    );
5981                } else if self.current_service == Service::SqsQueues
5982                    && self.sqs_state.current_queue.is_some()
5983                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
5984                    && !self.sqs_state.is_metrics_loading()
5985                {
5986                    self.sqs_state.set_monitoring_scroll(
5987                        self.sqs_state.monitoring_scroll().saturating_sub(1),
5988                    );
5989                } else if self.view_mode == ViewMode::Events {
5990                    if self.log_groups_state.event_scroll_offset == 0 {
5991                        if self.log_groups_state.has_older_events {
5992                            self.log_groups_state.loading = true;
5993                        }
5994                        // Don't move if at position 0
5995                    } else {
5996                        self.log_groups_state.event_scroll_offset =
5997                            self.log_groups_state.event_scroll_offset.saturating_sub(1);
5998                    }
5999                } else if self.current_service == Service::CloudWatchLogGroups {
6000                    if self.view_mode == ViewMode::List {
6001                        self.log_groups_state.log_groups.prev_item();
6002                    } else if self.view_mode == ViewMode::Detail
6003                        && self.log_groups_state.selected_stream > 0
6004                    {
6005                        self.log_groups_state.selected_stream =
6006                            self.log_groups_state.selected_stream.saturating_sub(1);
6007                        self.log_groups_state.expanded_stream = None;
6008                    }
6009                } else if self.current_service == Service::CloudWatchAlarms {
6010                    self.alarms_state.table.prev_item();
6011                } else if self.current_service == Service::CloudTrailEvents {
6012                    if self.cloudtrail_state.current_event.is_some()
6013                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
6014                    {
6015                        // Scroll JSON up
6016                        self.cloudtrail_state.event_json_scroll =
6017                            self.cloudtrail_state.event_json_scroll.saturating_sub(1);
6018                    } else {
6019                        // For CloudTrail with infinite pagination, navigate within current page
6020                        let page_size = self.cloudtrail_state.table.page_size.value();
6021                        let current_page = self.cloudtrail_state.table.selected / page_size;
6022                        let page_start = current_page * page_size;
6023
6024                        // Only navigate if not at page start
6025                        if self.cloudtrail_state.table.selected > page_start {
6026                            self.cloudtrail_state.table.selected -= 1;
6027                        }
6028                    }
6029                } else if self.current_service == Service::Ec2Instances {
6030                    if self.ec2_state.current_instance.is_some()
6031                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
6032                    {
6033                        self.ec2_state.tags.prev_item();
6034                    } else {
6035                        self.ec2_state.table.prev_item();
6036                    }
6037                } else if self.current_service == Service::EcrRepositories {
6038                    if self.ecr_state.current_repository.is_some() {
6039                        self.ecr_state.images.prev_item();
6040                    } else {
6041                        self.ecr_state.repositories.prev_item();
6042                    }
6043                } else if self.current_service == Service::SqsQueues {
6044                    if self.sqs_state.current_queue.is_some()
6045                        && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
6046                    {
6047                        self.sqs_state.triggers.prev_item();
6048                    } else if self.sqs_state.current_queue.is_some()
6049                        && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
6050                    {
6051                        self.sqs_state.pipes.prev_item();
6052                    } else if self.sqs_state.current_queue.is_some()
6053                        && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
6054                    {
6055                        self.sqs_state.tags.prev_item();
6056                    } else if self.sqs_state.current_queue.is_some()
6057                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
6058                    {
6059                        self.sqs_state.subscriptions.prev_item();
6060                    } else {
6061                        self.sqs_state.queues.prev_item();
6062                    }
6063                } else if self.current_service == Service::LambdaFunctions {
6064                    if self.lambda_state.current_function.is_some()
6065                        && self.lambda_state.detail_tab == LambdaDetailTab::Code
6066                    {
6067                        // Layer table navigation in Code tab
6068                        self.lambda_state.layer_selected =
6069                            self.lambda_state.layer_selected.saturating_sub(1);
6070                    } else if self.lambda_state.current_function.is_some()
6071                        && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6072                    {
6073                        self.lambda_state.version_table.prev_item();
6074                    } else if self.lambda_state.current_function.is_some()
6075                        && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
6076                            || (self.lambda_state.current_version.is_some()
6077                                && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
6078                    {
6079                        self.lambda_state.alias_table.prev_item();
6080                    } else if self.lambda_state.current_function.is_none() {
6081                        self.lambda_state.table.prev_item();
6082                    }
6083                } else if self.current_service == Service::LambdaApplications {
6084                    if self.lambda_application_state.current_application.is_some()
6085                        && self.lambda_application_state.detail_tab
6086                            == LambdaApplicationDetailTab::Overview
6087                    {
6088                        self.lambda_application_state.resources.selected = self
6089                            .lambda_application_state
6090                            .resources
6091                            .selected
6092                            .saturating_sub(1);
6093                    } else if self.lambda_application_state.current_application.is_some()
6094                        && self.lambda_application_state.detail_tab
6095                            == LambdaApplicationDetailTab::Deployments
6096                    {
6097                        self.lambda_application_state.deployments.selected = self
6098                            .lambda_application_state
6099                            .deployments
6100                            .selected
6101                            .saturating_sub(1);
6102                    } else {
6103                        self.lambda_application_state.table.selected = self
6104                            .lambda_application_state
6105                            .table
6106                            .selected
6107                            .saturating_sub(1);
6108                        self.lambda_application_state.table.snap_to_page();
6109                    }
6110                } else if self.current_service == Service::CloudFormationStacks {
6111                    if self.cfn_state.current_stack.is_some()
6112                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6113                    {
6114                        self.cfn_state.parameters.prev_item();
6115                    } else if self.cfn_state.current_stack.is_some()
6116                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6117                    {
6118                        self.cfn_state.outputs.prev_item();
6119                    } else if self.cfn_state.current_stack.is_some()
6120                        && self.cfn_state.detail_tab == CfnDetailTab::Resources
6121                    {
6122                        self.cfn_state.resources.prev_item();
6123                    } else {
6124                        self.cfn_state.table.prev_item();
6125                    }
6126                } else if self.current_service == Service::IamUsers {
6127                    self.iam_state.users.prev_item();
6128                } else if self.current_service == Service::IamRoles {
6129                    if self.iam_state.current_role.is_some() {
6130                        if self.iam_state.role_tab == RoleTab::TrustRelationships {
6131                            self.iam_state.trust_policy_scroll =
6132                                self.iam_state.trust_policy_scroll.saturating_sub(1);
6133                        } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
6134                            self.iam_state.revoke_sessions_scroll =
6135                                self.iam_state.revoke_sessions_scroll.saturating_sub(1);
6136                        } else if self.iam_state.role_tab == RoleTab::Tags {
6137                            self.iam_state.tags.prev_item();
6138                        } else if self.iam_state.role_tab == RoleTab::LastAccessed {
6139                            self.iam_state.last_accessed_services.prev_item();
6140                        } else {
6141                            self.iam_state.policies.prev_item();
6142                        }
6143                    } else {
6144                        self.iam_state.roles.prev_item();
6145                    }
6146                } else if self.current_service == Service::IamUserGroups {
6147                    if self.iam_state.current_group.is_some() {
6148                        if self.iam_state.group_tab == GroupTab::Users {
6149                            self.iam_state.group_users.prev_item();
6150                        } else if self.iam_state.group_tab == GroupTab::Permissions {
6151                            self.iam_state.policies.prev_item();
6152                        } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
6153                            self.iam_state.last_accessed_services.prev_item();
6154                        }
6155                    } else {
6156                        self.iam_state.groups.prev_item();
6157                    }
6158                }
6159            }
6160            _ => {}
6161        }
6162    }
6163
6164    fn page_down(&mut self) {
6165        if self.current_service == Service::CloudTrailEvents
6166            && self.cloudtrail_state.current_event.is_some()
6167        {
6168            if let Some(event) = &self.cloudtrail_state.current_event {
6169                let lines = event.cloud_trail_event_json.lines().count();
6170                let max_scroll = lines.saturating_sub(1);
6171                self.cloudtrail_state.event_json_scroll =
6172                    (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
6173            }
6174        } else if self.mode == Mode::ColumnSelector {
6175            let max = self.get_column_selector_max();
6176            let mut next_idx = (self.column_selector_index + 10).min(max);
6177            // Skip blank row if we land on it
6178            if self.is_blank_row_index(next_idx) {
6179                next_idx = (next_idx + 1).min(max);
6180            }
6181            self.column_selector_index = next_idx;
6182        } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
6183            if self.s3_state.input_focus == InputFocus::Pagination {
6184                // Navigate to next page
6185                let page_size = self.s3_state.buckets.page_size.value();
6186                let total_rows = self.calculate_total_bucket_rows();
6187                let max_offset = total_rows.saturating_sub(page_size);
6188                self.s3_state.selected_row =
6189                    (self.s3_state.selected_row + page_size).min(max_offset);
6190                self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6191            }
6192        } else if self.mode == Mode::FilterInput
6193            && self.current_service == Service::CloudFormationStacks
6194        {
6195            if self.cfn_state.current_stack.is_some()
6196                && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6197            {
6198                let page_size = self.cfn_state.parameters.page_size.value();
6199                let filtered_count = filtered_parameters(self).len();
6200                self.cfn_state.parameters_input_focus.handle_page_down(
6201                    &mut self.cfn_state.parameters.selected,
6202                    &mut self.cfn_state.parameters.scroll_offset,
6203                    page_size,
6204                    filtered_count,
6205                );
6206            } else if self.cfn_state.current_stack.is_some()
6207                && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6208            {
6209                let page_size = self.cfn_state.outputs.page_size.value();
6210                let filtered_count = filtered_outputs(self).len();
6211                self.cfn_state.outputs_input_focus.handle_page_down(
6212                    &mut self.cfn_state.outputs.selected,
6213                    &mut self.cfn_state.outputs.scroll_offset,
6214                    page_size,
6215                    filtered_count,
6216                );
6217            } else {
6218                use crate::ui::cfn::filtered_cloudformation_stacks;
6219                let page_size = self.cfn_state.table.page_size.value();
6220                let filtered_count = filtered_cloudformation_stacks(self).len();
6221                self.cfn_state.input_focus.handle_page_down(
6222                    &mut self.cfn_state.table.selected,
6223                    &mut self.cfn_state.table.scroll_offset,
6224                    page_size,
6225                    filtered_count,
6226                );
6227            }
6228        } else if self.mode == Mode::FilterInput
6229            && self.current_service == Service::IamRoles
6230            && self.iam_state.current_role.is_none()
6231        {
6232            let page_size = self.iam_state.roles.page_size.value();
6233            let filtered_count = filtered_iam_roles(self).len();
6234            self.iam_state.role_input_focus.handle_page_down(
6235                &mut self.iam_state.roles.selected,
6236                &mut self.iam_state.roles.scroll_offset,
6237                page_size,
6238                filtered_count,
6239            );
6240        } else if self.mode == Mode::FilterInput
6241            && self.current_service == Service::CloudWatchAlarms
6242        {
6243            let page_size = self.alarms_state.table.page_size.value();
6244            let filtered_count = self.alarms_state.table.items.len();
6245            self.alarms_state.input_focus.handle_page_down(
6246                &mut self.alarms_state.table.selected,
6247                &mut self.alarms_state.table.scroll_offset,
6248                page_size,
6249                filtered_count,
6250            );
6251        } else if self.mode == Mode::FilterInput
6252            && self.current_service == Service::CloudTrailEvents
6253        {
6254            let page_size = self.cloudtrail_state.table.page_size.value();
6255            let filtered_count = self.cloudtrail_state.table.items.len();
6256            self.cloudtrail_state.input_focus.handle_page_down(
6257                &mut self.cloudtrail_state.table.selected,
6258                &mut self.cloudtrail_state.table.scroll_offset,
6259                page_size,
6260                filtered_count,
6261            );
6262        } else if self.mode == Mode::FilterInput
6263            && self.current_service == Service::CloudWatchLogGroups
6264        {
6265            if self.view_mode == ViewMode::List {
6266                // Log groups list pagination
6267                let filtered = filtered_log_groups(self);
6268                let page_size = self.log_groups_state.log_groups.page_size.value();
6269                let filtered_count = filtered.len();
6270                self.log_groups_state.input_focus.handle_page_down(
6271                    &mut self.log_groups_state.log_groups.selected,
6272                    &mut self.log_groups_state.log_groups.scroll_offset,
6273                    page_size,
6274                    filtered_count,
6275                );
6276            } else {
6277                // Log streams pagination
6278                let filtered = filtered_log_streams(self);
6279                let page_size = self.log_groups_state.stream_page_size;
6280                let filtered_count = filtered.len();
6281                self.log_groups_state.input_focus.handle_page_down(
6282                    &mut self.log_groups_state.selected_stream,
6283                    &mut self.log_groups_state.stream_current_page,
6284                    page_size,
6285                    filtered_count,
6286                );
6287                self.log_groups_state.expanded_stream = None;
6288            }
6289        } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
6290        {
6291            if self.lambda_state.current_function.is_some()
6292                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6293                && self.lambda_state.version_input_focus == InputFocus::Pagination
6294            {
6295                let page_size = self.lambda_state.version_table.page_size.value();
6296                let filtered_count: usize = self
6297                    .lambda_state
6298                    .version_table
6299                    .items
6300                    .iter()
6301                    .filter(|v| {
6302                        self.lambda_state.version_table.filter.is_empty()
6303                            || v.version
6304                                .to_lowercase()
6305                                .contains(&self.lambda_state.version_table.filter.to_lowercase())
6306                            || v.aliases
6307                                .to_lowercase()
6308                                .contains(&self.lambda_state.version_table.filter.to_lowercase())
6309                            || v.description
6310                                .to_lowercase()
6311                                .contains(&self.lambda_state.version_table.filter.to_lowercase())
6312                    })
6313                    .count();
6314                let target = self.lambda_state.version_table.selected + page_size;
6315                self.lambda_state.version_table.selected =
6316                    target.min(filtered_count.saturating_sub(1));
6317            } else if self.lambda_state.current_function.is_some()
6318                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
6319                    || (self.lambda_state.current_version.is_some()
6320                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
6321                && self.lambda_state.alias_input_focus == InputFocus::Pagination
6322            {
6323                let page_size = self.lambda_state.alias_table.page_size.value();
6324                let version_filter = self.lambda_state.current_version.clone();
6325                let filtered_count = self
6326                    .lambda_state
6327                    .alias_table
6328                    .items
6329                    .iter()
6330                    .filter(|a| {
6331                        (version_filter.is_none()
6332                            || a.versions.contains(version_filter.as_ref().unwrap()))
6333                            && (self.lambda_state.alias_table.filter.is_empty()
6334                                || a.name
6335                                    .to_lowercase()
6336                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
6337                                || a.versions
6338                                    .to_lowercase()
6339                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
6340                                || a.description
6341                                    .to_lowercase()
6342                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase()))
6343                    })
6344                    .count();
6345                let target = self.lambda_state.alias_table.selected + page_size;
6346                self.lambda_state.alias_table.selected =
6347                    target.min(filtered_count.saturating_sub(1));
6348            } else if self.lambda_state.current_function.is_none() {
6349                let page_size = self.lambda_state.table.page_size.value();
6350                let filtered_count = filtered_lambda_functions(self).len();
6351                self.lambda_state.input_focus.handle_page_down(
6352                    &mut self.lambda_state.table.selected,
6353                    &mut self.lambda_state.table.scroll_offset,
6354                    page_size,
6355                    filtered_count,
6356                );
6357            }
6358        } else if self.mode == Mode::FilterInput
6359            && self.current_service == Service::LambdaApplications
6360        {
6361            if self.lambda_application_state.current_application.is_some() {
6362                if self.lambda_application_state.detail_tab
6363                    == LambdaApplicationDetailTab::Deployments
6364                {
6365                    let page_size = self.lambda_application_state.deployments.page_size.value();
6366                    let filtered_count = self.lambda_application_state.deployments.items.len();
6367                    self.lambda_application_state
6368                        .deployment_input_focus
6369                        .handle_page_down(
6370                            &mut self.lambda_application_state.deployments.selected,
6371                            &mut self.lambda_application_state.deployments.scroll_offset,
6372                            page_size,
6373                            filtered_count,
6374                        );
6375                } else {
6376                    let page_size = self.lambda_application_state.resources.page_size.value();
6377                    let filtered_count = self.lambda_application_state.resources.items.len();
6378                    self.lambda_application_state
6379                        .resource_input_focus
6380                        .handle_page_down(
6381                            &mut self.lambda_application_state.resources.selected,
6382                            &mut self.lambda_application_state.resources.scroll_offset,
6383                            page_size,
6384                            filtered_count,
6385                        );
6386                }
6387            } else {
6388                let page_size = self.lambda_application_state.table.page_size.value();
6389                let filtered_count = filtered_lambda_applications(self).len();
6390                self.lambda_application_state.input_focus.handle_page_down(
6391                    &mut self.lambda_application_state.table.selected,
6392                    &mut self.lambda_application_state.table.scroll_offset,
6393                    page_size,
6394                    filtered_count,
6395                );
6396            }
6397        } else if self.mode == Mode::FilterInput
6398            && self.current_service == Service::EcrRepositories
6399            && self.ecr_state.current_repository.is_none()
6400            && self.ecr_state.input_focus == InputFocus::Filter
6401        {
6402            // When input is focused, allow table scrolling
6403            let filtered = filtered_ecr_repositories(self);
6404            self.ecr_state.repositories.page_down(filtered.len());
6405        } else if self.mode == Mode::FilterInput
6406            && self.current_service == Service::EcrRepositories
6407            && self.ecr_state.current_repository.is_none()
6408        {
6409            let page_size = self.ecr_state.repositories.page_size.value();
6410            let filtered_count = filtered_ecr_repositories(self).len();
6411            self.ecr_state.input_focus.handle_page_down(
6412                &mut self.ecr_state.repositories.selected,
6413                &mut self.ecr_state.repositories.scroll_offset,
6414                page_size,
6415                filtered_count,
6416            );
6417        } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
6418            let page_size = self.iam_state.policies.page_size.value();
6419            let filtered_count = filtered_iam_policies(self).len();
6420            self.iam_state.policy_input_focus.handle_page_down(
6421                &mut self.iam_state.policies.selected,
6422                &mut self.iam_state.policies.scroll_offset,
6423                page_size,
6424                filtered_count,
6425            );
6426        } else if self.view_mode == ViewMode::PolicyView {
6427            let lines = self.iam_state.policy_document.lines().count();
6428            let max_scroll = lines.saturating_sub(1);
6429            self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
6430        } else if self.current_service == Service::CloudFormationStacks
6431            && self.cfn_state.current_stack.is_some()
6432            && self.cfn_state.detail_tab == CfnDetailTab::Template
6433        {
6434            let lines = self.cfn_state.template_body.lines().count();
6435            let max_scroll = lines.saturating_sub(1);
6436            self.cfn_state.template_scroll = (self.cfn_state.template_scroll + 10).min(max_scroll);
6437        } else if self.current_service == Service::LambdaFunctions
6438            && self.lambda_state.current_function.is_some()
6439            && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
6440            && !self.lambda_state.is_metrics_loading()
6441        {
6442            self.lambda_state
6443                .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
6444        } else if self.current_service == Service::Ec2Instances
6445            && self.ec2_state.current_instance.is_some()
6446            && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
6447            && !self.ec2_state.is_metrics_loading()
6448        {
6449            self.ec2_state
6450                .set_monitoring_scroll((self.ec2_state.monitoring_scroll() + 1).min(5));
6451        } else if self.current_service == Service::SqsQueues
6452            && self.sqs_state.current_queue.is_some()
6453        {
6454            if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
6455                self.sqs_state
6456                    .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
6457            } else {
6458                let lines = self.sqs_state.policy_document.lines().count();
6459                let max_scroll = lines.saturating_sub(1);
6460                self.sqs_state.policy_scroll = (self.sqs_state.policy_scroll + 10).min(max_scroll);
6461            }
6462        } else if self.current_service == Service::IamRoles
6463            && self.iam_state.current_role.is_some()
6464            && self.iam_state.role_tab == RoleTab::TrustRelationships
6465        {
6466            let lines = self.iam_state.trust_policy_document.lines().count();
6467            let max_scroll = lines.saturating_sub(1);
6468            self.iam_state.trust_policy_scroll =
6469                (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
6470        } else if self.current_service == Service::IamRoles
6471            && self.iam_state.current_role.is_some()
6472            && self.iam_state.role_tab == RoleTab::RevokeSessions
6473        {
6474            self.iam_state.revoke_sessions_scroll =
6475                (self.iam_state.revoke_sessions_scroll + 10).min(19);
6476        } else if self.mode == Mode::Normal {
6477            if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
6478            {
6479                let total_rows = self.calculate_total_bucket_rows();
6480                self.s3_state.selected_row = self
6481                    .s3_state
6482                    .selected_row
6483                    .saturating_add(10)
6484                    .min(total_rows.saturating_sub(1));
6485
6486                // Adjust scroll offset if selection goes below viewport
6487                let visible_rows = self.s3_state.bucket_visible_rows.get();
6488                if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
6489                    self.s3_state.bucket_scroll_offset =
6490                        self.s3_state.selected_row - visible_rows + 1;
6491                }
6492            } else if self.current_service == Service::S3Buckets
6493                && self.s3_state.current_bucket.is_some()
6494            {
6495                let total_rows = self.calculate_total_object_rows();
6496                self.s3_state.selected_object = self
6497                    .s3_state
6498                    .selected_object
6499                    .saturating_add(10)
6500                    .min(total_rows.saturating_sub(1));
6501
6502                // Adjust scroll offset if selection goes below viewport
6503                let visible_rows = self.s3_state.object_visible_rows.get();
6504                if self.s3_state.selected_object
6505                    >= self.s3_state.object_scroll_offset + visible_rows
6506                {
6507                    self.s3_state.object_scroll_offset =
6508                        self.s3_state.selected_object - visible_rows + 1;
6509                }
6510            } else if self.current_service == Service::CloudWatchLogGroups
6511                && self.view_mode == ViewMode::List
6512            {
6513                let filtered = filtered_log_groups(self);
6514                self.log_groups_state.log_groups.page_down(filtered.len());
6515            } else if self.current_service == Service::CloudWatchLogGroups
6516                && self.view_mode == ViewMode::Detail
6517            {
6518                let len = filtered_log_streams(self).len();
6519                nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
6520            } else if self.view_mode == ViewMode::Events {
6521                let max = self.log_groups_state.log_events.len();
6522                nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
6523            } else if self.view_mode == ViewMode::InsightsResults {
6524                let max = self.insights_state.insights.query_results.len();
6525                nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
6526            } else if self.current_service == Service::CloudWatchAlarms {
6527                let filtered = match self.alarms_state.alarm_tab {
6528                    AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
6529                    AlarmTab::InAlarm => self
6530                        .alarms_state
6531                        .table
6532                        .items
6533                        .iter()
6534                        .filter(|a| a.state.to_uppercase() == "ALARM")
6535                        .count(),
6536                };
6537                if filtered > 0 {
6538                    self.alarms_state.table.page_down(filtered);
6539                }
6540            } else if self.current_service == Service::CloudTrailEvents {
6541                let filtered_count = self.cloudtrail_state.table.items.len();
6542                if filtered_count > 0 {
6543                    self.cloudtrail_state.table.page_down(filtered_count);
6544                }
6545            } else if self.current_service == Service::Ec2Instances {
6546                let filtered: Vec<_> = self
6547                    .ec2_state
6548                    .table
6549                    .items
6550                    .iter()
6551                    .filter(|i| self.ec2_state.state_filter.matches(&i.state))
6552                    .filter(|i| {
6553                        if self.ec2_state.table.filter.is_empty() {
6554                            return true;
6555                        }
6556                        i.name.contains(&self.ec2_state.table.filter)
6557                            || i.instance_id.contains(&self.ec2_state.table.filter)
6558                            || i.state.contains(&self.ec2_state.table.filter)
6559                            || i.instance_type.contains(&self.ec2_state.table.filter)
6560                            || i.availability_zone.contains(&self.ec2_state.table.filter)
6561                            || i.security_groups.contains(&self.ec2_state.table.filter)
6562                            || i.key_name.contains(&self.ec2_state.table.filter)
6563                    })
6564                    .collect();
6565                if !filtered.is_empty() {
6566                    self.ec2_state.table.page_down(filtered.len());
6567                }
6568            } else if self.current_service == Service::EcrRepositories {
6569                if self.ecr_state.current_repository.is_some() {
6570                    let filtered = filtered_ecr_images(self);
6571                    self.ecr_state.images.page_down(filtered.len());
6572                } else {
6573                    let filtered = filtered_ecr_repositories(self);
6574                    self.ecr_state.repositories.page_down(filtered.len());
6575                }
6576            } else if self.current_service == Service::SqsQueues {
6577                let filtered =
6578                    filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter);
6579                self.sqs_state.queues.page_down(filtered.len());
6580            } else if self.current_service == Service::LambdaFunctions {
6581                let len = filtered_lambda_functions(self).len();
6582                self.lambda_state.table.page_down(len);
6583            } else if self.current_service == Service::LambdaApplications {
6584                let len = filtered_lambda_applications(self).len();
6585                self.lambda_application_state.table.page_down(len);
6586            } else if self.current_service == Service::CloudFormationStacks {
6587                if self.cfn_state.current_stack.is_some()
6588                    && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6589                {
6590                    let filtered = filtered_parameters(self);
6591                    self.cfn_state.parameters.page_down(filtered.len());
6592                } else if self.cfn_state.current_stack.is_some()
6593                    && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6594                {
6595                    let filtered = filtered_outputs(self);
6596                    self.cfn_state.outputs.page_down(filtered.len());
6597                } else {
6598                    let filtered = filtered_cloudformation_stacks(self);
6599                    self.cfn_state.table.page_down(filtered.len());
6600                }
6601            } else if self.current_service == Service::IamUsers {
6602                let len = filtered_iam_users(self).len();
6603                nav_page_down(&mut self.iam_state.users.selected, len, 10);
6604            } else if self.current_service == Service::IamRoles {
6605                if self.iam_state.current_role.is_some() {
6606                    let filtered = filtered_iam_policies(self);
6607                    if !filtered.is_empty() {
6608                        self.iam_state.policies.page_down(filtered.len());
6609                    }
6610                } else {
6611                    let filtered = filtered_iam_roles(self);
6612                    self.iam_state.roles.page_down(filtered.len());
6613                }
6614            } else if self.current_service == Service::IamUserGroups {
6615                if self.iam_state.current_group.is_some() {
6616                    if self.iam_state.group_tab == GroupTab::Users {
6617                        let filtered: Vec<_> = self
6618                            .iam_state
6619                            .group_users
6620                            .items
6621                            .iter()
6622                            .filter(|u| {
6623                                if self.iam_state.group_users.filter.is_empty() {
6624                                    true
6625                                } else {
6626                                    u.user_name
6627                                        .to_lowercase()
6628                                        .contains(&self.iam_state.group_users.filter.to_lowercase())
6629                                }
6630                            })
6631                            .collect();
6632                        if !filtered.is_empty() {
6633                            self.iam_state.group_users.page_down(filtered.len());
6634                        }
6635                    } else if self.iam_state.group_tab == GroupTab::Permissions {
6636                        let filtered = filtered_iam_policies(self);
6637                        if !filtered.is_empty() {
6638                            self.iam_state.policies.page_down(filtered.len());
6639                        }
6640                    } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
6641                        let filtered = filtered_last_accessed(self);
6642                        if !filtered.is_empty() {
6643                            self.iam_state
6644                                .last_accessed_services
6645                                .page_down(filtered.len());
6646                        }
6647                    }
6648                } else {
6649                    let filtered: Vec<_> = self
6650                        .iam_state
6651                        .groups
6652                        .items
6653                        .iter()
6654                        .filter(|g| {
6655                            if self.iam_state.groups.filter.is_empty() {
6656                                true
6657                            } else {
6658                                g.group_name
6659                                    .to_lowercase()
6660                                    .contains(&self.iam_state.groups.filter.to_lowercase())
6661                            }
6662                        })
6663                        .collect();
6664                    if !filtered.is_empty() {
6665                        self.iam_state.groups.page_down(filtered.len());
6666                    }
6667                }
6668            }
6669        }
6670    }
6671
6672    fn cycle_policy_type_next(&mut self) {
6673        let types = ["All types", "AWS managed", "Customer managed"];
6674        let current_idx = types
6675            .iter()
6676            .position(|&t| t == self.iam_state.policy_type_filter)
6677            .unwrap_or(0);
6678        let next_idx = (current_idx + 1) % types.len();
6679        self.iam_state.policy_type_filter = types[next_idx].to_string();
6680        self.iam_state.policies.reset();
6681    }
6682
6683    fn cycle_policy_type_prev(&mut self) {
6684        let types = ["All types", "AWS managed", "Customer managed"];
6685        let current_idx = types
6686            .iter()
6687            .position(|&t| t == self.iam_state.policy_type_filter)
6688            .unwrap_or(0);
6689        let prev_idx = if current_idx == 0 {
6690            types.len() - 1
6691        } else {
6692            current_idx - 1
6693        };
6694        self.iam_state.policy_type_filter = types[prev_idx].to_string();
6695        self.iam_state.policies.reset();
6696    }
6697
6698    fn page_up(&mut self) {
6699        if self.current_service == Service::CloudTrailEvents
6700            && self.cloudtrail_state.current_event.is_some()
6701        {
6702            self.cloudtrail_state.event_json_scroll =
6703                self.cloudtrail_state.event_json_scroll.saturating_sub(10);
6704        } else if self.mode == Mode::ColumnSelector {
6705            let mut prev_idx = self.column_selector_index.saturating_sub(10);
6706            // Skip blank row if we land on it
6707            if self.is_blank_row_index(prev_idx) {
6708                prev_idx = prev_idx.saturating_sub(1);
6709            }
6710            self.column_selector_index = prev_idx;
6711        } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
6712            if self.s3_state.input_focus == InputFocus::Pagination {
6713                // Navigate to previous page
6714                let page_size = self.s3_state.buckets.page_size.value();
6715                self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(page_size);
6716                self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6717            }
6718        } else if self.mode == Mode::FilterInput
6719            && self.current_service == Service::CloudFormationStacks
6720        {
6721            if self.cfn_state.current_stack.is_some()
6722                && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6723            {
6724                let page_size = self.cfn_state.parameters.page_size.value();
6725                self.cfn_state.parameters_input_focus.handle_page_up(
6726                    &mut self.cfn_state.parameters.selected,
6727                    &mut self.cfn_state.parameters.scroll_offset,
6728                    page_size,
6729                );
6730            } else if self.cfn_state.current_stack.is_some()
6731                && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6732            {
6733                let page_size = self.cfn_state.outputs.page_size.value();
6734                self.cfn_state.outputs_input_focus.handle_page_up(
6735                    &mut self.cfn_state.outputs.selected,
6736                    &mut self.cfn_state.outputs.scroll_offset,
6737                    page_size,
6738                );
6739            } else {
6740                let page_size = self.cfn_state.table.page_size.value();
6741                self.cfn_state.input_focus.handle_page_up(
6742                    &mut self.cfn_state.table.selected,
6743                    &mut self.cfn_state.table.scroll_offset,
6744                    page_size,
6745                );
6746            }
6747        } else if self.mode == Mode::FilterInput
6748            && self.current_service == Service::IamRoles
6749            && self.iam_state.current_role.is_none()
6750        {
6751            let page_size = self.iam_state.roles.page_size.value();
6752            self.iam_state.role_input_focus.handle_page_up(
6753                &mut self.iam_state.roles.selected,
6754                &mut self.iam_state.roles.scroll_offset,
6755                page_size,
6756            );
6757        } else if self.mode == Mode::FilterInput
6758            && self.current_service == Service::CloudWatchAlarms
6759        {
6760            let page_size = self.alarms_state.table.page_size.value();
6761            self.alarms_state.input_focus.handle_page_up(
6762                &mut self.alarms_state.table.selected,
6763                &mut self.alarms_state.table.scroll_offset,
6764                page_size,
6765            );
6766        } else if self.mode == Mode::FilterInput
6767            && self.current_service == Service::CloudTrailEvents
6768        {
6769            let page_size = self.cloudtrail_state.table.page_size.value();
6770            self.cloudtrail_state.input_focus.handle_page_up(
6771                &mut self.cloudtrail_state.table.selected,
6772                &mut self.cloudtrail_state.table.scroll_offset,
6773                page_size,
6774            );
6775        } else if self.mode == Mode::FilterInput
6776            && self.current_service == Service::CloudWatchLogGroups
6777        {
6778            if self.view_mode == ViewMode::List {
6779                // Log groups list pagination
6780                let page_size = self.log_groups_state.log_groups.page_size.value();
6781                self.log_groups_state.input_focus.handle_page_up(
6782                    &mut self.log_groups_state.log_groups.selected,
6783                    &mut self.log_groups_state.log_groups.scroll_offset,
6784                    page_size,
6785                );
6786            } else {
6787                // Log streams pagination
6788                let page_size = self.log_groups_state.stream_page_size;
6789                self.log_groups_state.input_focus.handle_page_up(
6790                    &mut self.log_groups_state.selected_stream,
6791                    &mut self.log_groups_state.stream_current_page,
6792                    page_size,
6793                );
6794                self.log_groups_state.expanded_stream = None;
6795            }
6796        } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
6797        {
6798            if self.lambda_state.current_function.is_some()
6799                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
6800                && self.lambda_state.version_input_focus == InputFocus::Pagination
6801            {
6802                let page_size = self.lambda_state.version_table.page_size.value();
6803                self.lambda_state.version_table.selected = self
6804                    .lambda_state
6805                    .version_table
6806                    .selected
6807                    .saturating_sub(page_size);
6808            } else if self.lambda_state.current_function.is_some()
6809                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
6810                    || (self.lambda_state.current_version.is_some()
6811                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
6812                && self.lambda_state.alias_input_focus == InputFocus::Pagination
6813            {
6814                let page_size = self.lambda_state.alias_table.page_size.value();
6815                self.lambda_state.alias_table.selected = self
6816                    .lambda_state
6817                    .alias_table
6818                    .selected
6819                    .saturating_sub(page_size);
6820            } else if self.lambda_state.current_function.is_none() {
6821                let page_size = self.lambda_state.table.page_size.value();
6822                self.lambda_state.input_focus.handle_page_up(
6823                    &mut self.lambda_state.table.selected,
6824                    &mut self.lambda_state.table.scroll_offset,
6825                    page_size,
6826                );
6827            }
6828        } else if self.mode == Mode::FilterInput
6829            && self.current_service == Service::LambdaApplications
6830        {
6831            if self.lambda_application_state.current_application.is_some() {
6832                if self.lambda_application_state.detail_tab
6833                    == LambdaApplicationDetailTab::Deployments
6834                {
6835                    let page_size = self.lambda_application_state.deployments.page_size.value();
6836                    self.lambda_application_state
6837                        .deployment_input_focus
6838                        .handle_page_up(
6839                            &mut self.lambda_application_state.deployments.selected,
6840                            &mut self.lambda_application_state.deployments.scroll_offset,
6841                            page_size,
6842                        );
6843                } else {
6844                    let page_size = self.lambda_application_state.resources.page_size.value();
6845                    self.lambda_application_state
6846                        .resource_input_focus
6847                        .handle_page_up(
6848                            &mut self.lambda_application_state.resources.selected,
6849                            &mut self.lambda_application_state.resources.scroll_offset,
6850                            page_size,
6851                        );
6852                }
6853            } else {
6854                let page_size = self.lambda_application_state.table.page_size.value();
6855                self.lambda_application_state.input_focus.handle_page_up(
6856                    &mut self.lambda_application_state.table.selected,
6857                    &mut self.lambda_application_state.table.scroll_offset,
6858                    page_size,
6859                );
6860            }
6861        } else if self.mode == Mode::FilterInput
6862            && self.current_service == Service::EcrRepositories
6863            && self.ecr_state.current_repository.is_none()
6864            && self.ecr_state.input_focus == InputFocus::Filter
6865        {
6866            // When input is focused, allow table scrolling
6867            self.ecr_state.repositories.page_up();
6868        } else if self.mode == Mode::FilterInput
6869            && self.current_service == Service::EcrRepositories
6870            && self.ecr_state.current_repository.is_none()
6871        {
6872            let page_size = self.ecr_state.repositories.page_size.value();
6873            self.ecr_state.input_focus.handle_page_up(
6874                &mut self.ecr_state.repositories.selected,
6875                &mut self.ecr_state.repositories.scroll_offset,
6876                page_size,
6877            );
6878        } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
6879            let page_size = self.iam_state.policies.page_size.value();
6880            self.iam_state.policy_input_focus.handle_page_up(
6881                &mut self.iam_state.policies.selected,
6882                &mut self.iam_state.policies.scroll_offset,
6883                page_size,
6884            );
6885        } else if self.view_mode == ViewMode::PolicyView {
6886            self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
6887        } else if self.current_service == Service::CloudFormationStacks
6888            && self.cfn_state.current_stack.is_some()
6889            && self.cfn_state.detail_tab == CfnDetailTab::Template
6890        {
6891            self.cfn_state.template_scroll = self.cfn_state.template_scroll.saturating_sub(10);
6892        } else if self.current_service == Service::SqsQueues
6893            && self.sqs_state.current_queue.is_some()
6894        {
6895            if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
6896                self.sqs_state
6897                    .set_monitoring_scroll(self.sqs_state.monitoring_scroll().saturating_sub(1));
6898            } else {
6899                self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(10);
6900            }
6901        } else if self.current_service == Service::IamRoles
6902            && self.iam_state.current_role.is_some()
6903            && self.iam_state.role_tab == RoleTab::TrustRelationships
6904        {
6905            self.iam_state.trust_policy_scroll =
6906                self.iam_state.trust_policy_scroll.saturating_sub(10);
6907        } else if self.current_service == Service::IamRoles
6908            && self.iam_state.current_role.is_some()
6909            && self.iam_state.role_tab == RoleTab::RevokeSessions
6910        {
6911            self.iam_state.revoke_sessions_scroll =
6912                self.iam_state.revoke_sessions_scroll.saturating_sub(10);
6913        } else if self.mode == Mode::Normal {
6914            if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
6915            {
6916                self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
6917
6918                // Adjust scroll offset if selection goes above viewport
6919                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6920                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6921                }
6922            } else if self.current_service == Service::S3Buckets
6923                && self.s3_state.current_bucket.is_some()
6924            {
6925                self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
6926
6927                // Adjust scroll offset if selection goes above viewport
6928                if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
6929                    self.s3_state.object_scroll_offset = self.s3_state.selected_object;
6930                }
6931            } else if self.current_service == Service::CloudWatchLogGroups
6932                && self.view_mode == ViewMode::List
6933            {
6934                self.log_groups_state.log_groups.page_up();
6935            } else if self.current_service == Service::CloudWatchLogGroups
6936                && self.view_mode == ViewMode::Detail
6937            {
6938                self.log_groups_state.selected_stream =
6939                    self.log_groups_state.selected_stream.saturating_sub(10);
6940            } else if self.view_mode == ViewMode::Events {
6941                if self.log_groups_state.event_scroll_offset < 10
6942                    && self.log_groups_state.has_older_events
6943                {
6944                    self.log_groups_state.loading = true;
6945                }
6946                self.log_groups_state.event_scroll_offset =
6947                    self.log_groups_state.event_scroll_offset.saturating_sub(10);
6948            } else if self.view_mode == ViewMode::InsightsResults {
6949                self.insights_state.insights.results_selected = self
6950                    .insights_state
6951                    .insights
6952                    .results_selected
6953                    .saturating_sub(10);
6954            } else if self.current_service == Service::CloudWatchAlarms {
6955                self.alarms_state.table.page_up();
6956            } else if self.current_service == Service::CloudTrailEvents {
6957                self.cloudtrail_state.table.page_up();
6958            } else if self.current_service == Service::Ec2Instances {
6959                self.ec2_state.table.page_up();
6960            } else if self.current_service == Service::EcrRepositories {
6961                if self.ecr_state.current_repository.is_some() {
6962                    self.ecr_state.images.page_up();
6963                } else {
6964                    self.ecr_state.repositories.page_up();
6965                }
6966            } else if self.current_service == Service::SqsQueues {
6967                self.sqs_state.queues.page_up();
6968            } else if self.current_service == Service::LambdaFunctions {
6969                self.lambda_state.table.page_up();
6970            } else if self.current_service == Service::LambdaApplications {
6971                self.lambda_application_state.table.page_up();
6972            } else if self.current_service == Service::CloudFormationStacks {
6973                if self.cfn_state.current_stack.is_some()
6974                    && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6975                {
6976                    self.cfn_state.parameters.page_up();
6977                } else if self.cfn_state.current_stack.is_some()
6978                    && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6979                {
6980                    self.cfn_state.outputs.page_up();
6981                } else {
6982                    self.cfn_state.table.page_up();
6983                }
6984            } else if self.current_service == Service::IamUsers {
6985                self.iam_state.users.page_up();
6986            } else if self.current_service == Service::IamRoles {
6987                if self.iam_state.current_role.is_some() {
6988                    self.iam_state.policies.page_up();
6989                } else {
6990                    self.iam_state.roles.page_up();
6991                }
6992            }
6993        }
6994    }
6995
6996    fn next_pane(&mut self) {
6997        if self.current_service == Service::S3Buckets {
6998            if self.s3_state.current_bucket.is_some() {
6999                // In objects view - expand prefix and trigger preview load
7000                // Map visual index to actual object (including nested items)
7001                let mut visual_idx = 0;
7002                let mut found_obj: Option<S3Object> = None;
7003
7004                // Helper to recursively check nested items
7005                fn check_nested(
7006                    obj: &S3Object,
7007                    visual_idx: &mut usize,
7008                    target_idx: usize,
7009                    expanded_prefixes: &std::collections::HashSet<String>,
7010                    prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7011                    found_obj: &mut Option<S3Object>,
7012                ) {
7013                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7014                        if let Some(preview) = prefix_preview.get(&obj.key) {
7015                            for nested_obj in preview {
7016                                if *visual_idx == target_idx {
7017                                    *found_obj = Some(nested_obj.clone());
7018                                    return;
7019                                }
7020                                *visual_idx += 1;
7021
7022                                // Recursively check deeper levels
7023                                check_nested(
7024                                    nested_obj,
7025                                    visual_idx,
7026                                    target_idx,
7027                                    expanded_prefixes,
7028                                    prefix_preview,
7029                                    found_obj,
7030                                );
7031                                if found_obj.is_some() {
7032                                    return;
7033                                }
7034                            }
7035                        } else {
7036                            // Loading row
7037                            *visual_idx += 1;
7038                        }
7039                    }
7040                }
7041
7042                for obj in &self.s3_state.objects {
7043                    if visual_idx == self.s3_state.selected_object {
7044                        found_obj = Some(obj.clone());
7045                        break;
7046                    }
7047                    visual_idx += 1;
7048
7049                    // Check nested items recursively
7050                    check_nested(
7051                        obj,
7052                        &mut visual_idx,
7053                        self.s3_state.selected_object,
7054                        &self.s3_state.expanded_prefixes,
7055                        &self.s3_state.prefix_preview,
7056                        &mut found_obj,
7057                    );
7058                    if found_obj.is_some() {
7059                        break;
7060                    }
7061                }
7062
7063                if let Some(obj) = found_obj {
7064                    if obj.is_prefix {
7065                        if !self.s3_state.expanded_prefixes.contains(&obj.key) {
7066                            self.s3_state.expanded_prefixes.insert(obj.key.clone());
7067                            // Trigger preview load if not already cached
7068                            if !self.s3_state.prefix_preview.contains_key(&obj.key) {
7069                                self.s3_state.buckets.loading = true;
7070                            }
7071                        }
7072                        // Move to first child if expanded and has children
7073                        if self.s3_state.expanded_prefixes.contains(&obj.key) {
7074                            if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
7075                                if !preview.is_empty() {
7076                                    self.s3_state.selected_object += 1;
7077                                }
7078                            }
7079                        }
7080                    }
7081                }
7082            } else {
7083                // In bucket list - find which bucket/prefix the selected row corresponds to
7084                let mut row_idx = 0;
7085                let mut found = false;
7086                for bucket in &self.s3_state.buckets.items {
7087                    if row_idx == self.s3_state.selected_row {
7088                        // Selected row is a bucket - expand and move to first child
7089                        if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
7090                            self.s3_state.expanded_prefixes.insert(bucket.name.clone());
7091                            if !self.s3_state.bucket_preview.contains_key(&bucket.name)
7092                                && !self.s3_state.bucket_errors.contains_key(&bucket.name)
7093                            {
7094                                self.s3_state.buckets.loading = true;
7095                            }
7096                        }
7097                        // Move to first child if expanded and has children
7098                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7099                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7100                                if !preview.is_empty() {
7101                                    self.s3_state.selected_row = row_idx + 1;
7102                                }
7103                            }
7104                        }
7105                        break;
7106                    }
7107                    row_idx += 1;
7108
7109                    // Skip error rows - they're not selectable
7110                    if self.s3_state.bucket_errors.contains_key(&bucket.name)
7111                        && self.s3_state.expanded_prefixes.contains(&bucket.name)
7112                    {
7113                        continue;
7114                    }
7115
7116                    if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7117                        if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7118                            // Recursive function to check nested items at any depth
7119                            #[allow(clippy::too_many_arguments)]
7120                            fn check_nested_expansion(
7121                                objects: &[S3Object],
7122                                row_idx: &mut usize,
7123                                target_row: usize,
7124                                expanded_prefixes: &mut std::collections::HashSet<String>,
7125                                prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7126                                found: &mut bool,
7127                                loading: &mut bool,
7128                                selected_row: &mut usize,
7129                            ) {
7130                                for obj in objects {
7131                                    if *row_idx == target_row {
7132                                        // Selected this item - expand and move to first child
7133                                        if obj.is_prefix {
7134                                            if !expanded_prefixes.contains(&obj.key) {
7135                                                expanded_prefixes.insert(obj.key.clone());
7136                                                if !prefix_preview.contains_key(&obj.key) {
7137                                                    *loading = true;
7138                                                }
7139                                            }
7140                                            // Move to first child if expanded and has children
7141                                            if expanded_prefixes.contains(&obj.key) {
7142                                                if let Some(preview) = prefix_preview.get(&obj.key)
7143                                                {
7144                                                    if !preview.is_empty() {
7145                                                        *selected_row = *row_idx + 1;
7146                                                    }
7147                                                }
7148                                            }
7149                                        }
7150                                        *found = true;
7151                                        return;
7152                                    }
7153                                    *row_idx += 1;
7154
7155                                    // Recursively check nested items if expanded
7156                                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7157                                        if let Some(nested) = prefix_preview.get(&obj.key) {
7158                                            check_nested_expansion(
7159                                                nested,
7160                                                row_idx,
7161                                                target_row,
7162                                                expanded_prefixes,
7163                                                prefix_preview,
7164                                                found,
7165                                                loading,
7166                                                selected_row,
7167                                            );
7168                                            if *found {
7169                                                return;
7170                                            }
7171                                        } else {
7172                                            *row_idx += 1; // Loading row
7173                                        }
7174                                    }
7175                                }
7176                            }
7177
7178                            check_nested_expansion(
7179                                preview,
7180                                &mut row_idx,
7181                                self.s3_state.selected_row,
7182                                &mut self.s3_state.expanded_prefixes,
7183                                &self.s3_state.prefix_preview,
7184                                &mut found,
7185                                &mut self.s3_state.buckets.loading,
7186                                &mut self.s3_state.selected_row,
7187                            );
7188                            if found || row_idx > self.s3_state.selected_row {
7189                                break;
7190                            }
7191                        } else {
7192                            row_idx += 1;
7193                            if row_idx > self.s3_state.selected_row {
7194                                break;
7195                            }
7196                        }
7197                    }
7198                    if found {
7199                        break;
7200                    }
7201                }
7202            }
7203        } else if self.view_mode == ViewMode::InsightsResults {
7204            // Right arrow scrolls horizontally by 1 column
7205            let max_cols = self
7206                .insights_state
7207                .insights
7208                .query_results
7209                .first()
7210                .map(|r| r.len())
7211                .unwrap_or(0);
7212            if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
7213                self.insights_state.insights.results_horizontal_scroll += 1;
7214            }
7215        } else if self.current_service == Service::CloudWatchLogGroups
7216            && self.view_mode == ViewMode::List
7217        {
7218            // Expand selected log group
7219            if self.log_groups_state.log_groups.expanded_item
7220                != Some(self.log_groups_state.log_groups.selected)
7221            {
7222                self.log_groups_state.log_groups.expanded_item =
7223                    Some(self.log_groups_state.log_groups.selected);
7224            }
7225        } else if self.current_service == Service::CloudWatchLogGroups
7226            && self.view_mode == ViewMode::Detail
7227        {
7228            // Expand selected log stream
7229            if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
7230            {
7231                self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
7232            }
7233        } else if self.view_mode == ViewMode::Events {
7234            // Only scroll if there are hidden columns
7235            // Expand selected event
7236            if self.log_groups_state.expanded_event
7237                != Some(self.log_groups_state.event_scroll_offset)
7238            {
7239                self.log_groups_state.expanded_event =
7240                    Some(self.log_groups_state.event_scroll_offset);
7241            }
7242        } else if self.current_service == Service::CloudWatchAlarms {
7243            // Expand selected alarm
7244            if !self.alarms_state.table.is_expanded() {
7245                self.alarms_state.table.toggle_expand();
7246            }
7247        } else if self.current_service == Service::Ec2Instances {
7248            if self.ec2_state.current_instance.is_some()
7249                && self.ec2_state.detail_tab == Ec2DetailTab::Tags
7250            {
7251                self.ec2_state.tags.toggle_expand();
7252            } else if !self.ec2_state.table.is_expanded() {
7253                self.ec2_state.table.toggle_expand();
7254            }
7255        } else if self.current_service == Service::EcrRepositories {
7256            if self.ecr_state.current_repository.is_some() {
7257                // In images view - expand selected image
7258                self.ecr_state.images.toggle_expand();
7259            } else {
7260                // In repositories view - expand selected repository
7261                self.ecr_state.repositories.toggle_expand();
7262            }
7263        } else if self.current_service == Service::SqsQueues {
7264            if self.sqs_state.current_queue.is_some()
7265                && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
7266            {
7267                self.sqs_state.triggers.toggle_expand();
7268            } else if self.sqs_state.current_queue.is_some()
7269                && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
7270            {
7271                self.sqs_state.pipes.toggle_expand();
7272            } else if self.sqs_state.current_queue.is_some()
7273                && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
7274            {
7275                self.sqs_state.tags.toggle_expand();
7276            } else if self.sqs_state.current_queue.is_some()
7277                && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
7278            {
7279                self.sqs_state.subscriptions.toggle_expand();
7280            } else {
7281                self.sqs_state.queues.expand();
7282            }
7283        } else if self.current_service == Service::LambdaFunctions {
7284            if self.lambda_state.current_function.is_some()
7285                && self.lambda_state.detail_tab == LambdaDetailTab::Code
7286            {
7287                // Expand selected layer
7288                if self.lambda_state.layer_expanded != Some(self.lambda_state.layer_selected) {
7289                    self.lambda_state.layer_expanded = Some(self.lambda_state.layer_selected);
7290                } else {
7291                    self.lambda_state.layer_expanded = None;
7292                }
7293            } else if self.lambda_state.current_function.is_some()
7294                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
7295            {
7296                // Expand selected version
7297                self.lambda_state.version_table.toggle_expand();
7298            } else if self.lambda_state.current_function.is_some()
7299                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
7300                    || (self.lambda_state.current_version.is_some()
7301                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
7302            {
7303                // Expand selected alias
7304                self.lambda_state.alias_table.toggle_expand();
7305            } else if self.lambda_state.current_function.is_none() {
7306                // Expand selected function
7307                self.lambda_state.table.toggle_expand();
7308            }
7309        } else if self.current_service == Service::LambdaApplications {
7310            if self.lambda_application_state.current_application.is_some() {
7311                // In detail view - expand resource or deployment
7312                if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
7313                {
7314                    self.lambda_application_state.resources.toggle_expand();
7315                } else {
7316                    self.lambda_application_state.deployments.toggle_expand();
7317                }
7318            } else {
7319                // Expand selected application in list
7320                if self.lambda_application_state.table.expanded_item
7321                    != Some(self.lambda_application_state.table.selected)
7322                {
7323                    self.lambda_application_state.table.expanded_item =
7324                        Some(self.lambda_application_state.table.selected);
7325                }
7326            }
7327        } else if self.current_service == Service::CloudFormationStacks
7328            && self.cfn_state.current_stack.is_none()
7329        {
7330            self.cfn_state.table.toggle_expand();
7331        } else if self.current_service == Service::CloudFormationStacks
7332            && self.cfn_state.current_stack.is_some()
7333            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
7334        {
7335            self.cfn_state.parameters.toggle_expand();
7336        } else if self.current_service == Service::CloudFormationStacks
7337            && self.cfn_state.current_stack.is_some()
7338            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
7339        {
7340            self.cfn_state.outputs.toggle_expand();
7341        } else if self.current_service == Service::CloudFormationStacks
7342            && self.cfn_state.current_stack.is_some()
7343            && self.cfn_state.detail_tab == CfnDetailTab::Resources
7344        {
7345            self.cfn_state.resources.toggle_expand();
7346        } else if self.current_service == Service::IamUsers {
7347            if self.iam_state.current_user.is_some() {
7348                if self.iam_state.user_tab == UserTab::Tags {
7349                    if self.iam_state.user_tags.expanded_item
7350                        != Some(self.iam_state.user_tags.selected)
7351                    {
7352                        self.iam_state.user_tags.expanded_item =
7353                            Some(self.iam_state.user_tags.selected);
7354                    }
7355                } else if self.iam_state.policies.expanded_item
7356                    != Some(self.iam_state.policies.selected)
7357                {
7358                    self.iam_state.policies.toggle_expand();
7359                }
7360            } else if !self.iam_state.users.is_expanded() {
7361                self.iam_state.users.toggle_expand();
7362            }
7363        } else if self.current_service == Service::IamRoles {
7364            if self.iam_state.current_role.is_some() {
7365                // Handle expansion based on current tab
7366                if self.iam_state.role_tab == RoleTab::Tags {
7367                    if !self.iam_state.tags.is_expanded() {
7368                        self.iam_state.tags.expand();
7369                    }
7370                } else if self.iam_state.role_tab == RoleTab::LastAccessed {
7371                    if !self.iam_state.last_accessed_services.is_expanded() {
7372                        self.iam_state.last_accessed_services.expand();
7373                    }
7374                } else if !self.iam_state.policies.is_expanded() {
7375                    self.iam_state.policies.expand();
7376                }
7377            } else if !self.iam_state.roles.is_expanded() {
7378                self.iam_state.roles.expand();
7379            }
7380        } else if self.current_service == Service::IamUserGroups {
7381            if self.iam_state.current_group.is_some() {
7382                if self.iam_state.group_tab == GroupTab::Users {
7383                    if !self.iam_state.group_users.is_expanded() {
7384                        self.iam_state.group_users.expand();
7385                    }
7386                } else if self.iam_state.group_tab == GroupTab::Permissions {
7387                    if !self.iam_state.policies.is_expanded() {
7388                        self.iam_state.policies.expand();
7389                    }
7390                } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
7391                    && !self.iam_state.last_accessed_services.is_expanded()
7392                {
7393                    self.iam_state.last_accessed_services.expand();
7394                }
7395            } else if !self.iam_state.groups.is_expanded() {
7396                self.iam_state.groups.expand();
7397            }
7398        }
7399    }
7400
7401    fn go_to_page(&mut self, page: usize) {
7402        if page == 0 {
7403            return;
7404        }
7405
7406        match self.current_service {
7407            Service::CloudWatchAlarms => {
7408                let alarm_page_size = self.alarms_state.table.page_size.value();
7409                let target = (page - 1) * alarm_page_size;
7410                let filtered_count = match self.alarms_state.alarm_tab {
7411                    AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
7412                    AlarmTab::InAlarm => self
7413                        .alarms_state
7414                        .table
7415                        .items
7416                        .iter()
7417                        .filter(|a| a.state.to_uppercase() == "ALARM")
7418                        .count(),
7419                };
7420                let max_offset = filtered_count.saturating_sub(alarm_page_size);
7421                self.alarms_state.table.scroll_offset = target.min(max_offset);
7422                self.alarms_state.table.selected = self
7423                    .alarms_state
7424                    .table
7425                    .scroll_offset
7426                    .min(filtered_count.saturating_sub(1));
7427            }
7428            Service::CloudTrailEvents => {
7429                let page_size = self.cloudtrail_state.table.page_size.value();
7430                let filtered_count = self.cloudtrail_state.table.items.len();
7431                let max_page = (filtered_count / page_size) + 1; // Allow one page beyond loaded
7432
7433                // Only navigate if page is within valid range
7434                if page <= max_page {
7435                    let target = (page - 1) * page_size;
7436                    self.cloudtrail_state.table.scroll_offset = target;
7437                    self.cloudtrail_state.table.selected = target;
7438                    self.cloudtrail_state.table.expanded_item = None; // Reset expansion
7439                }
7440                // Otherwise do nothing (ignore invalid page numbers)
7441            }
7442            Service::CloudWatchLogGroups => match self.view_mode {
7443                ViewMode::Events => {
7444                    let page_size = 20;
7445                    let target = (page - 1) * page_size;
7446                    let max = self.log_groups_state.log_events.len().saturating_sub(1);
7447                    self.log_groups_state.event_scroll_offset = target.min(max);
7448                }
7449                ViewMode::Detail => {
7450                    let page_size = self.log_groups_state.stream_page_size;
7451                    self.log_groups_state.stream_current_page = (page - 1).min(
7452                        self.log_groups_state
7453                            .log_streams
7454                            .len()
7455                            .div_ceil(page_size)
7456                            .saturating_sub(1),
7457                    );
7458                    self.log_groups_state.selected_stream = 0;
7459                }
7460                ViewMode::List => {
7461                    let total = self.log_groups_state.log_groups.items.len();
7462                    self.log_groups_state.log_groups.goto_page(page, total);
7463                }
7464                _ => {}
7465            },
7466            Service::EcrRepositories => {
7467                if self.ecr_state.current_repository.is_some() {
7468                    let filtered_count = self
7469                        .ecr_state
7470                        .images
7471                        .filtered(|img| {
7472                            self.ecr_state.images.filter.is_empty()
7473                                || img
7474                                    .tag
7475                                    .to_lowercase()
7476                                    .contains(&self.ecr_state.images.filter.to_lowercase())
7477                                || img
7478                                    .digest
7479                                    .to_lowercase()
7480                                    .contains(&self.ecr_state.images.filter.to_lowercase())
7481                        })
7482                        .len();
7483                    self.ecr_state.images.goto_page(page, filtered_count);
7484                } else {
7485                    let filtered_count = self
7486                        .ecr_state
7487                        .repositories
7488                        .filtered(|r| {
7489                            self.ecr_state.repositories.filter.is_empty()
7490                                || r.name
7491                                    .to_lowercase()
7492                                    .contains(&self.ecr_state.repositories.filter.to_lowercase())
7493                        })
7494                        .len();
7495                    self.ecr_state.repositories.goto_page(page, filtered_count);
7496                }
7497            }
7498            Service::SqsQueues => {
7499                let filtered_count =
7500                    filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter)
7501                        .len();
7502                self.sqs_state.queues.goto_page(page, filtered_count);
7503            }
7504            Service::S3Buckets => {
7505                if self.s3_state.current_bucket.is_some() {
7506                    let page_size = 50; // S3 objects use fixed page size
7507                    let target = (page - 1) * page_size;
7508                    let total_rows = self.calculate_total_object_rows();
7509                    let max = total_rows.saturating_sub(1);
7510                    self.s3_state.selected_object = target.min(max);
7511                } else {
7512                    let page_size = self.s3_state.buckets.page_size.value();
7513                    let target = (page - 1) * page_size;
7514                    let total_rows = self.calculate_total_bucket_rows();
7515                    let max = total_rows.saturating_sub(1);
7516                    self.s3_state.selected_row = target.min(max);
7517                    // Adjust scroll offset to show the target page
7518                    self.s3_state.bucket_scroll_offset =
7519                        target.min(total_rows.saturating_sub(page_size));
7520                }
7521            }
7522            Service::LambdaFunctions => {
7523                if self.lambda_state.current_function.is_some()
7524                    && self.lambda_state.detail_tab == LambdaDetailTab::Versions
7525                {
7526                    let filtered_count = self
7527                        .lambda_state
7528                        .version_table
7529                        .filtered(|v| {
7530                            self.lambda_state.version_table.filter.is_empty()
7531                                || v.version.to_lowercase().contains(
7532                                    &self.lambda_state.version_table.filter.to_lowercase(),
7533                                )
7534                                || v.aliases.to_lowercase().contains(
7535                                    &self.lambda_state.version_table.filter.to_lowercase(),
7536                                )
7537                                || v.description.to_lowercase().contains(
7538                                    &self.lambda_state.version_table.filter.to_lowercase(),
7539                                )
7540                        })
7541                        .len();
7542                    self.lambda_state
7543                        .version_table
7544                        .goto_page(page, filtered_count);
7545                } else {
7546                    let filtered_count = filtered_lambda_functions(self).len();
7547                    self.lambda_state.table.goto_page(page, filtered_count);
7548                }
7549            }
7550            Service::LambdaApplications => {
7551                let filtered_count = filtered_lambda_applications(self).len();
7552                self.lambda_application_state
7553                    .table
7554                    .goto_page(page, filtered_count);
7555            }
7556            Service::CloudFormationStacks => {
7557                let filtered_count = filtered_cloudformation_stacks(self).len();
7558                self.cfn_state.table.goto_page(page, filtered_count);
7559            }
7560            Service::IamUsers => {
7561                let filtered_count = filtered_iam_users(self).len();
7562                self.iam_state.users.goto_page(page, filtered_count);
7563            }
7564            Service::IamRoles => {
7565                let filtered_count = filtered_iam_roles(self).len();
7566                self.iam_state.roles.goto_page(page, filtered_count);
7567            }
7568            _ => {}
7569        }
7570    }
7571
7572    fn prev_pane(&mut self) {
7573        if self.current_service == Service::S3Buckets {
7574            if self.s3_state.current_bucket.is_some() {
7575                // In objects view - collapse prefix or jump to parent
7576                // Map visual index to actual object (including nested items)
7577                let mut visual_idx = 0;
7578                let mut found_obj: Option<S3Object> = None;
7579                let mut parent_idx: Option<usize> = None;
7580
7581                // Helper to recursively find object and its parent
7582                #[allow(clippy::too_many_arguments)]
7583                fn find_with_parent(
7584                    objects: &[S3Object],
7585                    visual_idx: &mut usize,
7586                    target_idx: usize,
7587                    expanded_prefixes: &std::collections::HashSet<String>,
7588                    prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7589                    found_obj: &mut Option<S3Object>,
7590                    parent_idx: &mut Option<usize>,
7591                    current_parent: Option<usize>,
7592                ) {
7593                    for obj in objects {
7594                        if *visual_idx == target_idx {
7595                            *found_obj = Some(obj.clone());
7596                            *parent_idx = current_parent;
7597                            return;
7598                        }
7599                        let obj_idx = *visual_idx;
7600                        *visual_idx += 1;
7601
7602                        // Check nested items if expanded
7603                        if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7604                            if let Some(preview) = prefix_preview.get(&obj.key) {
7605                                find_with_parent(
7606                                    preview,
7607                                    visual_idx,
7608                                    target_idx,
7609                                    expanded_prefixes,
7610                                    prefix_preview,
7611                                    found_obj,
7612                                    parent_idx,
7613                                    Some(obj_idx),
7614                                );
7615                                if found_obj.is_some() {
7616                                    return;
7617                                }
7618                            }
7619                        }
7620                    }
7621                }
7622
7623                find_with_parent(
7624                    &self.s3_state.objects,
7625                    &mut visual_idx,
7626                    self.s3_state.selected_object,
7627                    &self.s3_state.expanded_prefixes,
7628                    &self.s3_state.prefix_preview,
7629                    &mut found_obj,
7630                    &mut parent_idx,
7631                    None,
7632                );
7633
7634                if let Some(obj) = found_obj {
7635                    if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
7636                        // Expanded: collapse it and jump to parent
7637                        self.s3_state.expanded_prefixes.remove(&obj.key);
7638                        if let Some(parent) = parent_idx {
7639                            self.s3_state.selected_object = parent;
7640                        }
7641                    } else if let Some(parent) = parent_idx {
7642                        // Already collapsed or not a prefix: jump to parent
7643                        self.s3_state.selected_object = parent;
7644                    }
7645                }
7646
7647                // Adjust scroll offset to keep selection visible
7648                let visible_rows = self.s3_state.object_visible_rows.get();
7649                if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
7650                    self.s3_state.object_scroll_offset = self.s3_state.selected_object;
7651                } else if self.s3_state.selected_object
7652                    >= self.s3_state.object_scroll_offset + visible_rows
7653                {
7654                    self.s3_state.object_scroll_offset = self
7655                        .s3_state
7656                        .selected_object
7657                        .saturating_sub(visible_rows - 1);
7658                }
7659            } else {
7660                // In bucket list - find which bucket/prefix the selected row corresponds to
7661                let mut row_idx = 0;
7662                for bucket in &self.s3_state.buckets.items {
7663                    if row_idx == self.s3_state.selected_row {
7664                        // Selected row is a bucket - collapse it
7665                        self.s3_state.expanded_prefixes.remove(&bucket.name);
7666                        break;
7667                    }
7668                    row_idx += 1;
7669                    if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7670                        if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7671                            // Recursive function to check nested items at any depth
7672                            #[allow(clippy::too_many_arguments)]
7673                            fn check_nested_collapse(
7674                                objects: &[S3Object],
7675                                row_idx: &mut usize,
7676                                target_row: usize,
7677                                expanded_prefixes: &mut std::collections::HashSet<String>,
7678                                prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7679                                found: &mut bool,
7680                                selected_row: &mut usize,
7681                                parent_row: usize,
7682                            ) {
7683                                for obj in objects {
7684                                    let current_row = *row_idx;
7685                                    if *row_idx == target_row {
7686                                        // Selected this item - collapse or jump to parent
7687                                        if obj.is_prefix {
7688                                            if expanded_prefixes.contains(&obj.key) {
7689                                                // Expanded: collapse it and move to parent
7690                                                expanded_prefixes.remove(&obj.key);
7691                                                *selected_row = parent_row;
7692                                            } else {
7693                                                // Already collapsed: jump to parent
7694                                                *selected_row = parent_row;
7695                                            }
7696                                        } else {
7697                                            // Not a prefix: jump to parent
7698                                            *selected_row = parent_row;
7699                                        }
7700                                        *found = true;
7701                                        return;
7702                                    }
7703                                    *row_idx += 1;
7704
7705                                    // Recursively check nested items if expanded
7706                                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7707                                        if let Some(nested) = prefix_preview.get(&obj.key) {
7708                                            check_nested_collapse(
7709                                                nested,
7710                                                row_idx,
7711                                                target_row,
7712                                                expanded_prefixes,
7713                                                prefix_preview,
7714                                                found,
7715                                                selected_row,
7716                                                current_row,
7717                                            );
7718                                            if *found {
7719                                                return;
7720                                            }
7721                                        } else {
7722                                            *row_idx += 1; // Loading row
7723                                        }
7724                                    }
7725                                }
7726                            }
7727
7728                            let mut found = false;
7729                            let parent_row = row_idx - 1; // Parent is the bucket
7730                            check_nested_collapse(
7731                                preview,
7732                                &mut row_idx,
7733                                self.s3_state.selected_row,
7734                                &mut self.s3_state.expanded_prefixes,
7735                                &self.s3_state.prefix_preview,
7736                                &mut found,
7737                                &mut self.s3_state.selected_row,
7738                                parent_row,
7739                            );
7740                            if found {
7741                                // Adjust scroll offset to keep selection visible
7742                                let visible_rows = self.s3_state.bucket_visible_rows.get();
7743                                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
7744                                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
7745                                } else if self.s3_state.selected_row
7746                                    >= self.s3_state.bucket_scroll_offset + visible_rows
7747                                {
7748                                    self.s3_state.bucket_scroll_offset =
7749                                        self.s3_state.selected_row.saturating_sub(visible_rows - 1);
7750                                }
7751                                return;
7752                            }
7753                        } else {
7754                            row_idx += 1;
7755                        }
7756                    }
7757                }
7758
7759                // Adjust scroll offset to keep selection visible after collapse
7760                let visible_rows = self.s3_state.bucket_visible_rows.get();
7761                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
7762                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
7763                } else if self.s3_state.selected_row
7764                    >= self.s3_state.bucket_scroll_offset + visible_rows
7765                {
7766                    self.s3_state.bucket_scroll_offset =
7767                        self.s3_state.selected_row.saturating_sub(visible_rows - 1);
7768                }
7769            }
7770        } else if self.view_mode == ViewMode::InsightsResults {
7771            // Left arrow scrolls horizontally by 1 column
7772            self.insights_state.insights.results_horizontal_scroll = self
7773                .insights_state
7774                .insights
7775                .results_horizontal_scroll
7776                .saturating_sub(1);
7777        } else if self.current_service == Service::CloudWatchLogGroups
7778            && self.view_mode == ViewMode::List
7779        {
7780            // Collapse expanded log group
7781            if self.log_groups_state.log_groups.has_expanded_item() {
7782                self.log_groups_state.log_groups.collapse();
7783            }
7784        } else if self.current_service == Service::CloudWatchLogGroups
7785            && self.view_mode == ViewMode::Detail
7786        {
7787            // Collapse expanded log stream
7788            if self.log_groups_state.expanded_stream.is_some() {
7789                self.log_groups_state.expanded_stream = None;
7790            }
7791        } else if self.view_mode == ViewMode::Events {
7792            // Collapse expanded event
7793            if self.log_groups_state.expanded_event.is_some() {
7794                self.log_groups_state.expanded_event = None;
7795            }
7796        } else if self.current_service == Service::CloudWatchAlarms {
7797            // Collapse expanded alarm
7798            self.alarms_state.table.collapse();
7799        } else if self.current_service == Service::Ec2Instances {
7800            self.ec2_state.table.collapse();
7801        } else if self.current_service == Service::ApiGatewayApis {
7802            self.apig_state.apis.collapse();
7803        } else if self.current_service == Service::EcrRepositories {
7804            if self.ecr_state.current_repository.is_some() {
7805                // In images view - collapse expanded image
7806                self.ecr_state.images.collapse();
7807            } else {
7808                // In repositories view - collapse expanded repository
7809                self.ecr_state.repositories.collapse();
7810            }
7811        } else if self.current_service == Service::SqsQueues {
7812            if self.sqs_state.current_queue.is_some()
7813                && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
7814            {
7815                self.sqs_state.triggers.collapse();
7816            } else if self.sqs_state.current_queue.is_some()
7817                && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
7818            {
7819                self.sqs_state.pipes.collapse();
7820            } else if self.sqs_state.current_queue.is_some()
7821                && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
7822            {
7823                self.sqs_state.tags.collapse();
7824            } else if self.sqs_state.current_queue.is_some()
7825                && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
7826            {
7827                self.sqs_state.subscriptions.collapse();
7828            } else {
7829                self.sqs_state.queues.collapse();
7830            }
7831        } else if self.current_service == Service::LambdaFunctions {
7832            if self.lambda_state.current_function.is_some()
7833                && self.lambda_state.detail_tab == LambdaDetailTab::Code
7834            {
7835                // Collapse selected layer
7836                self.lambda_state.layer_expanded = None;
7837            } else if self.lambda_state.current_function.is_some()
7838                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
7839            {
7840                // Collapse selected version
7841                self.lambda_state.version_table.collapse();
7842            } else if self.lambda_state.current_function.is_some()
7843                && (self.lambda_state.detail_tab == LambdaDetailTab::Aliases
7844                    || (self.lambda_state.current_version.is_some()
7845                        && self.lambda_state.detail_tab == LambdaDetailTab::Configuration))
7846            {
7847                // Collapse selected alias
7848                self.lambda_state.alias_table.collapse();
7849            } else if self.lambda_state.current_function.is_none() {
7850                // Collapse expanded function
7851                self.lambda_state.table.collapse();
7852            }
7853        } else if self.current_service == Service::LambdaApplications {
7854            if self.lambda_application_state.current_application.is_some() {
7855                // In detail view - collapse resource or deployment
7856                if self.lambda_application_state.detail_tab == LambdaApplicationDetailTab::Overview
7857                {
7858                    self.lambda_application_state.resources.collapse();
7859                } else {
7860                    self.lambda_application_state.deployments.collapse();
7861                }
7862            } else {
7863                // Collapse expanded application in list
7864                if self.lambda_application_state.table.has_expanded_item() {
7865                    self.lambda_application_state.table.collapse();
7866                }
7867            }
7868        } else if self.current_service == Service::CloudFormationStacks
7869            && self.cfn_state.current_stack.is_none()
7870        {
7871            self.cfn_state.table.collapse();
7872        } else if self.current_service == Service::CloudFormationStacks
7873            && self.cfn_state.current_stack.is_some()
7874            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
7875        {
7876            self.cfn_state.parameters.collapse();
7877        } else if self.current_service == Service::CloudFormationStacks
7878            && self.cfn_state.current_stack.is_some()
7879            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
7880        {
7881            self.cfn_state.outputs.collapse();
7882        } else if self.current_service == Service::CloudFormationStacks
7883            && self.cfn_state.current_stack.is_some()
7884            && self.cfn_state.detail_tab == CfnDetailTab::Resources
7885        {
7886            self.cfn_state.resources.collapse();
7887        } else if self.current_service == Service::IamUsers {
7888            if self.iam_state.users.has_expanded_item() {
7889                self.iam_state.users.collapse();
7890            }
7891        } else if self.current_service == Service::IamRoles {
7892            if self.view_mode == ViewMode::PolicyView {
7893                // Go back from policy view to role detail
7894                self.view_mode = ViewMode::Detail;
7895                self.iam_state.current_policy = None;
7896                self.iam_state.policy_document.clear();
7897                self.iam_state.policy_scroll = 0;
7898            } else if self.iam_state.current_role.is_some() {
7899                if self.iam_state.role_tab == RoleTab::Tags
7900                    && self.iam_state.tags.has_expanded_item()
7901                {
7902                    self.iam_state.tags.collapse();
7903                } else if self.iam_state.role_tab == RoleTab::LastAccessed
7904                    && self
7905                        .iam_state
7906                        .last_accessed_services
7907                        .expanded_item
7908                        .is_some()
7909                {
7910                    self.iam_state.last_accessed_services.collapse();
7911                } else if self.iam_state.policies.has_expanded_item() {
7912                    self.iam_state.policies.collapse();
7913                }
7914            } else if self.iam_state.roles.has_expanded_item() {
7915                self.iam_state.roles.collapse();
7916            }
7917        } else if self.current_service == Service::IamUserGroups {
7918            if self.iam_state.current_group.is_some() {
7919                if self.iam_state.group_tab == GroupTab::Users
7920                    && self.iam_state.group_users.has_expanded_item()
7921                {
7922                    self.iam_state.group_users.collapse();
7923                } else if self.iam_state.group_tab == GroupTab::Permissions
7924                    && self.iam_state.policies.has_expanded_item()
7925                {
7926                    self.iam_state.policies.collapse();
7927                } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
7928                    && self
7929                        .iam_state
7930                        .last_accessed_services
7931                        .expanded_item
7932                        .is_some()
7933                {
7934                    self.iam_state.last_accessed_services.collapse();
7935                }
7936            } else if self.iam_state.groups.has_expanded_item() {
7937                self.iam_state.groups.collapse();
7938            }
7939        }
7940    }
7941
7942    fn collapse_row(&mut self) {
7943        match self.current_service {
7944            Service::S3Buckets => {
7945                if self.s3_state.current_bucket.is_none() {
7946                    // Filter buckets first
7947                    let filtered_buckets: Vec<_> = self
7948                        .s3_state
7949                        .buckets
7950                        .items
7951                        .iter()
7952                        .filter(|b| {
7953                            if self.s3_state.buckets.filter.is_empty() {
7954                                true
7955                            } else {
7956                                b.name
7957                                    .to_lowercase()
7958                                    .contains(&self.s3_state.buckets.filter.to_lowercase())
7959                            }
7960                        })
7961                        .collect();
7962
7963                    // In bucket list - collapse bucket or nested prefix
7964                    let mut row_idx = 0;
7965
7966                    for bucket in filtered_buckets {
7967                        if row_idx == self.s3_state.selected_row {
7968                            // Selected row is a bucket - collapse it
7969                            self.s3_state.expanded_prefixes.remove(&bucket.name);
7970                            // Don't return - let scroll adjustment happen below
7971                            break;
7972                        }
7973                        row_idx += 1;
7974                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7975                            // Check if bucket has error - don't count error rows as they're not selectable
7976                            if self.s3_state.bucket_errors.contains_key(&bucket.name) {
7977                                // Bucket has error, no child rows to navigate
7978                                continue;
7979                            }
7980                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7981                                // Recursive function to check nested items at any depth
7982                                #[allow(clippy::too_many_arguments)]
7983                                fn check_nested_collapse(
7984                                    objects: &[S3Object],
7985                                    row_idx: &mut usize,
7986                                    target_row: usize,
7987                                    expanded_prefixes: &mut std::collections::HashSet<String>,
7988                                    prefix_preview: &std::collections::HashMap<
7989                                        String,
7990                                        Vec<S3Object>,
7991                                    >,
7992                                    found: &mut bool,
7993                                    selected_row: &mut usize,
7994                                    parent_row: usize,
7995                                ) {
7996                                    for obj in objects {
7997                                        let current_row = *row_idx;
7998                                        if *row_idx == target_row {
7999                                            // Selected this item - collapse or jump to parent
8000                                            if obj.is_prefix {
8001                                                if expanded_prefixes.contains(&obj.key) {
8002                                                    // Expanded: collapse it and move to parent
8003                                                    expanded_prefixes.remove(&obj.key);
8004                                                    *selected_row = parent_row;
8005                                                } else {
8006                                                    // Already collapsed: jump to parent
8007                                                    *selected_row = parent_row;
8008                                                }
8009                                            } else {
8010                                                // Not a prefix: jump to parent
8011                                                *selected_row = parent_row;
8012                                            }
8013                                            *found = true;
8014                                            return;
8015                                        }
8016                                        *row_idx += 1;
8017
8018                                        // Recursively check nested items if expanded
8019                                        if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
8020                                            if let Some(nested) = prefix_preview.get(&obj.key) {
8021                                                check_nested_collapse(
8022                                                    nested,
8023                                                    row_idx,
8024                                                    target_row,
8025                                                    expanded_prefixes,
8026                                                    prefix_preview,
8027                                                    found,
8028                                                    selected_row,
8029                                                    current_row,
8030                                                );
8031                                                if *found {
8032                                                    return;
8033                                                }
8034                                            } else {
8035                                                *row_idx += 1; // Loading row
8036                                            }
8037                                        }
8038                                    }
8039                                }
8040
8041                                let mut found = false;
8042                                let parent_row = row_idx - 1; // Parent is the bucket
8043                                check_nested_collapse(
8044                                    preview,
8045                                    &mut row_idx,
8046                                    self.s3_state.selected_row,
8047                                    &mut self.s3_state.expanded_prefixes,
8048                                    &self.s3_state.prefix_preview,
8049                                    &mut found,
8050                                    &mut self.s3_state.selected_row,
8051                                    parent_row,
8052                                );
8053                                if found {
8054                                    // Adjust scroll offset to keep selection visible
8055                                    let visible_rows = self.s3_state.bucket_visible_rows.get();
8056                                    if self.s3_state.selected_row
8057                                        < self.s3_state.bucket_scroll_offset
8058                                    {
8059                                        self.s3_state.bucket_scroll_offset =
8060                                            self.s3_state.selected_row;
8061                                    } else if self.s3_state.selected_row
8062                                        >= self.s3_state.bucket_scroll_offset + visible_rows
8063                                    {
8064                                        self.s3_state.bucket_scroll_offset = self
8065                                            .s3_state
8066                                            .selected_row
8067                                            .saturating_sub(visible_rows - 1);
8068                                    }
8069                                    return;
8070                                }
8071                            } else {
8072                                row_idx += 1;
8073                            }
8074                        }
8075                    }
8076
8077                    // Adjust scroll offset to keep selection visible after collapse
8078                    let visible_rows = self.s3_state.bucket_visible_rows.get();
8079                    if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
8080                        self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
8081                    } else if self.s3_state.selected_row
8082                        >= self.s3_state.bucket_scroll_offset + visible_rows
8083                    {
8084                        self.s3_state.bucket_scroll_offset =
8085                            self.s3_state.selected_row.saturating_sub(visible_rows - 1);
8086                    }
8087                }
8088            }
8089            Service::CloudWatchLogGroups => {
8090                if self.view_mode == ViewMode::Events {
8091                    if let Some(idx) = self.log_groups_state.expanded_event {
8092                        self.log_groups_state.expanded_event = None;
8093                        self.log_groups_state.selected_event = idx;
8094                    }
8095                } else if self.view_mode == ViewMode::Detail {
8096                    if let Some(idx) = self.log_groups_state.expanded_stream {
8097                        self.log_groups_state.expanded_stream = None;
8098                        self.log_groups_state.selected_stream = idx;
8099                    }
8100                } else {
8101                    self.log_groups_state.log_groups.collapse();
8102                }
8103            }
8104            Service::CloudWatchAlarms => self.alarms_state.table.collapse(),
8105            Service::Ec2Instances => {
8106                if self.ec2_state.current_instance.is_some()
8107                    && self.ec2_state.detail_tab == Ec2DetailTab::Tags
8108                {
8109                    self.ec2_state.tags.collapse();
8110                } else {
8111                    self.ec2_state.table.collapse();
8112                }
8113            }
8114            Service::EcrRepositories => {
8115                if self.ecr_state.current_repository.is_some() {
8116                    self.ecr_state.images.collapse();
8117                } else {
8118                    self.ecr_state.repositories.collapse();
8119                }
8120            }
8121            Service::LambdaFunctions => self.lambda_state.table.collapse(),
8122            Service::SqsQueues => self.sqs_state.queues.collapse(),
8123            Service::CloudFormationStacks => {
8124                if self.cfn_state.current_stack.is_some() {
8125                    match self.cfn_state.detail_tab {
8126                        crate::ui::cfn::DetailTab::Resources => {
8127                            self.cfn_state.resources.collapse();
8128                        }
8129                        crate::ui::cfn::DetailTab::Parameters => {
8130                            self.cfn_state.parameters.collapse();
8131                        }
8132                        crate::ui::cfn::DetailTab::Outputs => {
8133                            self.cfn_state.outputs.collapse();
8134                        }
8135                        _ => {}
8136                    }
8137                } else {
8138                    self.cfn_state.table.collapse();
8139                }
8140            }
8141            Service::IamUsers => {
8142                if self.iam_state.current_user.is_some() {
8143                    match self.iam_state.user_tab {
8144                        crate::ui::iam::UserTab::Permissions => {
8145                            self.iam_state.policies.collapse();
8146                        }
8147                        crate::ui::iam::UserTab::Groups => {
8148                            self.iam_state.user_group_memberships.collapse();
8149                        }
8150                        crate::ui::iam::UserTab::Tags => {
8151                            self.iam_state.user_tags.collapse();
8152                        }
8153                        _ => {}
8154                    }
8155                } else {
8156                    self.iam_state.users.collapse();
8157                }
8158            }
8159            Service::IamRoles => {
8160                if self.iam_state.current_role.is_some() {
8161                    match self.iam_state.role_tab {
8162                        crate::ui::iam::RoleTab::Permissions => {
8163                            self.iam_state.policies.collapse();
8164                        }
8165                        crate::ui::iam::RoleTab::Tags => {
8166                            self.iam_state.tags.collapse();
8167                        }
8168                        _ => {}
8169                    }
8170                } else {
8171                    self.iam_state.roles.collapse();
8172                }
8173            }
8174            Service::IamUserGroups => self.iam_state.groups.collapse(),
8175            Service::ApiGatewayApis => {
8176                if let Some(api) = &self.apig_state.current_api {
8177                    if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
8178                        let protocol = api.protocol_type.to_uppercase();
8179                        if protocol == "REST" {
8180                            // Handle resources tree - use filtered items
8181                            let (filtered_items, _filtered_children) =
8182                                crate::ui::apig::filter_tree_items(
8183                                    &self.apig_state.resources.items,
8184                                    &self.apig_state.resource_children,
8185                                    &self.apig_state.route_filter,
8186                                );
8187
8188                            let selected_row = self.apig_state.resources.selected;
8189                            let mut current_row = 0;
8190                            if let Some(resource_id) = self.find_resource_at_row(
8191                                &filtered_items,
8192                                selected_row,
8193                                &mut current_row,
8194                            ) {
8195                                if self.apig_state.expanded_resources.contains(&resource_id) {
8196                                    self.apig_state.expanded_resources.remove(&resource_id);
8197                                } else if let Some(parent_row) =
8198                                    self.find_resource_parent_row(&filtered_items, &resource_id)
8199                                {
8200                                    self.apig_state.resources.selected = parent_row;
8201                                }
8202                            }
8203                        } else {
8204                            // Handle routes tree - use filtered items
8205                            let (filtered_items, _filtered_children) =
8206                                crate::ui::apig::filter_tree_items(
8207                                    &self.apig_state.routes.items,
8208                                    &self.apig_state.route_children,
8209                                    &self.apig_state.route_filter,
8210                                );
8211
8212                            let selected_row = self.apig_state.routes.selected;
8213                            let mut current_row = 0;
8214                            if let Some(route_key) = self.find_route_at_row(
8215                                &filtered_items,
8216                                selected_row,
8217                                &mut current_row,
8218                            ) {
8219                                if self.apig_state.expanded_routes.contains(&route_key) {
8220                                    self.apig_state.expanded_routes.remove(&route_key);
8221                                } else if let Some(parent_row) =
8222                                    self.find_parent_row(&filtered_items, &route_key)
8223                                {
8224                                    self.apig_state.routes.selected = parent_row;
8225                                }
8226                            }
8227                        }
8228                    }
8229                } else {
8230                    self.apig_state.apis.collapse();
8231                }
8232            }
8233            Service::CloudTrailEvents => {
8234                if self.cloudtrail_state.current_event.is_some()
8235                    && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
8236                {
8237                    // Collapse resources table column
8238                    self.cloudtrail_state.resources_expanded_index = None;
8239                } else {
8240                    self.cloudtrail_state.table.collapse();
8241                }
8242            }
8243            _ => {}
8244        }
8245    }
8246
8247    fn expand_row(&mut self) {
8248        match self.current_service {
8249            Service::S3Buckets => {
8250                if self.s3_state.current_bucket.is_none() {
8251                    // Filter buckets first
8252                    let filtered_buckets: Vec<_> = self
8253                        .s3_state
8254                        .buckets
8255                        .items
8256                        .iter()
8257                        .filter(|b| {
8258                            if self.s3_state.buckets.filter.is_empty() {
8259                                true
8260                            } else {
8261                                b.name
8262                                    .to_lowercase()
8263                                    .contains(&self.s3_state.buckets.filter.to_lowercase())
8264                            }
8265                        })
8266                        .collect();
8267
8268                    // Recursive helper to check nested items
8269                    fn check_nested_expand(
8270                        objects: &[S3Object],
8271                        row_idx: &mut usize,
8272                        target_row: usize,
8273                        expanded_prefixes: &mut std::collections::HashSet<String>,
8274                        prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
8275                    ) -> Option<(bool, usize)> {
8276                        for obj in objects {
8277                            if *row_idx == target_row {
8278                                if obj.is_prefix {
8279                                    // Toggle expansion
8280                                    if expanded_prefixes.contains(&obj.key) {
8281                                        expanded_prefixes.remove(&obj.key);
8282                                        return Some((true, *row_idx));
8283                                    } else {
8284                                        expanded_prefixes.insert(obj.key.clone());
8285                                        return Some((true, *row_idx + 1)); // Move to first child
8286                                    }
8287                                }
8288                                return Some((false, *row_idx));
8289                            }
8290                            *row_idx += 1;
8291
8292                            // Check nested items if expanded
8293                            if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
8294                                if let Some(nested) = prefix_preview.get(&obj.key) {
8295                                    if let Some(result) = check_nested_expand(
8296                                        nested,
8297                                        row_idx,
8298                                        target_row,
8299                                        expanded_prefixes,
8300                                        prefix_preview,
8301                                    ) {
8302                                        return Some(result);
8303                                    }
8304                                }
8305                            }
8306                        }
8307                        None
8308                    }
8309
8310                    let mut row_idx = 0;
8311                    for bucket in filtered_buckets {
8312                        if row_idx == self.s3_state.selected_row {
8313                            // Toggle bucket expansion
8314                            if self.s3_state.expanded_prefixes.contains(&bucket.name) {
8315                                self.s3_state.expanded_prefixes.remove(&bucket.name);
8316                            } else {
8317                                self.s3_state.expanded_prefixes.insert(bucket.name.clone());
8318                                self.s3_state.selected_row = row_idx + 1; // Move to first child
8319                                self.s3_state.buckets.loading = true;
8320                            }
8321                            return;
8322                        }
8323                        row_idx += 1;
8324
8325                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
8326                            if self.s3_state.bucket_errors.contains_key(&bucket.name) {
8327                                continue;
8328                            }
8329                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
8330                                if let Some((loading, new_row)) = check_nested_expand(
8331                                    preview,
8332                                    &mut row_idx,
8333                                    self.s3_state.selected_row,
8334                                    &mut self.s3_state.expanded_prefixes,
8335                                    &self.s3_state.prefix_preview,
8336                                ) {
8337                                    self.s3_state.selected_row = new_row;
8338                                    if loading {
8339                                        self.s3_state.buckets.loading = true;
8340                                    }
8341                                    return;
8342                                }
8343                            }
8344                        }
8345                    }
8346                } else {
8347                    // Inside a bucket - expand objects
8348                    fn check_object_expand(
8349                        objects: &[S3Object],
8350                        row_idx: &mut usize,
8351                        target_row: usize,
8352                        expanded_prefixes: &mut std::collections::HashSet<String>,
8353                        prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
8354                    ) -> Option<(bool, usize)> {
8355                        for obj in objects {
8356                            if *row_idx == target_row {
8357                                if obj.is_prefix {
8358                                    if expanded_prefixes.contains(&obj.key) {
8359                                        expanded_prefixes.remove(&obj.key);
8360                                        return Some((true, *row_idx));
8361                                    } else {
8362                                        expanded_prefixes.insert(obj.key.clone());
8363                                        return Some((true, *row_idx + 1));
8364                                    }
8365                                }
8366                                return Some((false, *row_idx));
8367                            }
8368                            *row_idx += 1;
8369
8370                            if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
8371                                if let Some(nested) = prefix_preview.get(&obj.key) {
8372                                    if let Some(result) = check_object_expand(
8373                                        nested,
8374                                        row_idx,
8375                                        target_row,
8376                                        expanded_prefixes,
8377                                        prefix_preview,
8378                                    ) {
8379                                        return Some(result);
8380                                    }
8381                                }
8382                            }
8383                        }
8384                        None
8385                    }
8386
8387                    let mut row_idx = 0;
8388                    if let Some((loading, new_row)) = check_object_expand(
8389                        &self.s3_state.objects,
8390                        &mut row_idx,
8391                        self.s3_state.selected_object,
8392                        &mut self.s3_state.expanded_prefixes,
8393                        &self.s3_state.prefix_preview,
8394                    ) {
8395                        self.s3_state.selected_object = new_row;
8396                        if loading {
8397                            self.s3_state.buckets.loading = true;
8398                        }
8399                    }
8400                }
8401            }
8402            Service::ApiGatewayApis => {
8403                if let Some(api) = &self.apig_state.current_api {
8404                    if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
8405                        let protocol = api.protocol_type.to_uppercase();
8406                        if protocol == "REST" {
8407                            // Handle resources tree - use filtered items
8408                            let (filtered_items, filtered_children) =
8409                                crate::ui::apig::filter_tree_items(
8410                                    &self.apig_state.resources.items,
8411                                    &self.apig_state.resource_children,
8412                                    &self.apig_state.route_filter,
8413                                );
8414
8415                            let selected_row = self.apig_state.resources.selected;
8416                            let mut current_row = 0;
8417                            if let Some(resource_id) = self.find_resource_at_row(
8418                                &filtered_items,
8419                                selected_row,
8420                                &mut current_row,
8421                            ) {
8422                                if self.apig_state.expanded_resources.contains(&resource_id) {
8423                                    let total_rows =
8424                                        crate::ui::tree::TreeRenderer::count_visible_rows(
8425                                            &filtered_items,
8426                                            &self.apig_state.expanded_resources,
8427                                            &filtered_children,
8428                                        );
8429                                    if selected_row + 1 < total_rows {
8430                                        self.apig_state.resources.selected = selected_row + 1;
8431                                    }
8432                                } else {
8433                                    self.apig_state.expanded_resources.insert(resource_id);
8434                                }
8435                            }
8436                        } else {
8437                            // Handle routes tree - use filtered items
8438                            let (filtered_items, filtered_children) =
8439                                crate::ui::apig::filter_tree_items(
8440                                    &self.apig_state.routes.items,
8441                                    &self.apig_state.route_children,
8442                                    &self.apig_state.route_filter,
8443                                );
8444
8445                            let selected_row = self.apig_state.routes.selected;
8446                            let mut current_row = 0;
8447                            if let Some(route_key) = self.find_route_at_row(
8448                                &filtered_items,
8449                                selected_row,
8450                                &mut current_row,
8451                            ) {
8452                                if self.apig_state.expanded_routes.contains(&route_key) {
8453                                    let total_rows =
8454                                        crate::ui::tree::TreeRenderer::count_visible_rows(
8455                                            &filtered_items,
8456                                            &self.apig_state.expanded_routes,
8457                                            &filtered_children,
8458                                        );
8459                                    if selected_row + 1 < total_rows {
8460                                        self.apig_state.routes.selected = selected_row + 1;
8461                                    }
8462                                } else {
8463                                    self.apig_state.expanded_routes.insert(route_key);
8464                                }
8465                            }
8466                        }
8467                    }
8468                } else {
8469                    self.apig_state.apis.expand();
8470                }
8471            }
8472            Service::CloudTrailEvents => {
8473                if self.cloudtrail_state.current_event.is_some()
8474                    && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
8475                {
8476                    // Expand resources table column
8477                    self.cloudtrail_state.resources_expanded_index = Some(0);
8478                } else {
8479                    self.cloudtrail_state.table.expand();
8480                }
8481            }
8482            _ => {
8483                // For other services, Right arrow switches panes
8484                self.next_pane();
8485            }
8486        }
8487    }
8488
8489    fn find_route_at_row(
8490        &self,
8491        routes: &[Route],
8492        target_row: usize,
8493        current_row: &mut usize,
8494    ) -> Option<String> {
8495        for route in routes {
8496            if *current_row == target_row {
8497                return Some(route.route_key.clone());
8498            }
8499            *current_row += 1;
8500
8501            // Check children if expanded
8502            if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
8503                if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
8504                    if let Some(key) = self.find_route_at_row(children, target_row, current_row) {
8505                        return Some(key);
8506                    }
8507                }
8508            }
8509        }
8510        None
8511    }
8512
8513    fn find_route_id_at_row_with_children(
8514        &self,
8515        routes: &[Route],
8516        children_map: &HashMap<String, Vec<Route>>,
8517        target_row: usize,
8518        current_row: &mut usize,
8519    ) -> Option<String> {
8520        for route in routes {
8521            if *current_row == target_row {
8522                // Only return route_id if this is a leaf (has target/integration)
8523                if !route.target.is_empty() {
8524                    return Some(route.route_id.clone());
8525                } else {
8526                    return None;
8527                }
8528            }
8529            *current_row += 1;
8530
8531            // Check children if expanded
8532            if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
8533                if let Some(children) = children_map.get(&route.route_key) {
8534                    if let Some(id) = self.find_route_id_at_row_with_children(
8535                        children,
8536                        children_map,
8537                        target_row,
8538                        current_row,
8539                    ) {
8540                        return Some(id);
8541                    }
8542                }
8543            }
8544        }
8545        None
8546    }
8547
8548    fn find_parent_row(&self, routes: &[Route], child_key: &str) -> Option<usize> {
8549        let mut current_row = 0;
8550        self.find_parent_row_recursive(routes, child_key, &mut current_row)
8551    }
8552
8553    fn find_parent_row_recursive(
8554        &self,
8555        routes: &[Route],
8556        child_key: &str,
8557        current_row: &mut usize,
8558    ) -> Option<usize> {
8559        for route in routes {
8560            let parent_row = *current_row;
8561            *current_row += 1;
8562
8563            // Check if this route's children contain the target
8564            if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
8565                if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
8566                    // Check direct children
8567                    for child in children {
8568                        if child.route_key == child_key {
8569                            return Some(parent_row);
8570                        }
8571                    }
8572
8573                    // Check nested children
8574                    if let Some(row) =
8575                        self.find_parent_row_recursive(children, child_key, current_row)
8576                    {
8577                        return Some(row);
8578                    }
8579                }
8580            }
8581        }
8582        None
8583    }
8584
8585    fn find_resource_at_row(
8586        &self,
8587        resources: &[ApigResource],
8588        target_row: usize,
8589        current_row: &mut usize,
8590    ) -> Option<String> {
8591        for resource in resources {
8592            if *current_row == target_row {
8593                return Some(resource.id.clone());
8594            }
8595            *current_row += 1;
8596
8597            if self.apig_state.expanded_resources.contains(&resource.id) {
8598                if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
8599                    if let Some(id) = self.find_resource_at_row(children, target_row, current_row) {
8600                        return Some(id);
8601                    }
8602                }
8603            }
8604        }
8605        None
8606    }
8607
8608    fn find_resource_parent_row(
8609        &self,
8610        resources: &[ApigResource],
8611        child_id: &str,
8612    ) -> Option<usize> {
8613        let mut current_row = 0;
8614        self.find_resource_parent_row_recursive(resources, child_id, &mut current_row)
8615    }
8616
8617    fn find_resource_parent_row_recursive(
8618        &self,
8619        resources: &[ApigResource],
8620        child_id: &str,
8621        current_row: &mut usize,
8622    ) -> Option<usize> {
8623        for resource in resources {
8624            let parent_row = *current_row;
8625            *current_row += 1;
8626
8627            if self.apig_state.expanded_resources.contains(&resource.id) {
8628                if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
8629                    for child in children {
8630                        if child.id == child_id {
8631                            return Some(parent_row);
8632                        }
8633                    }
8634
8635                    if let Some(row) =
8636                        self.find_resource_parent_row_recursive(children, child_id, current_row)
8637                    {
8638                        return Some(row);
8639                    }
8640                }
8641            }
8642        }
8643        None
8644    }
8645
8646    fn select_item(&mut self) {
8647        if self.mode == Mode::RegionPicker {
8648            let filtered = self.get_filtered_regions();
8649            if let Some(region) = filtered.get(self.region_picker_selected) {
8650                // Save current session before changing region
8651                if !self.tabs.is_empty() {
8652                    let mut session = Session::new(
8653                        self.profile.clone(),
8654                        self.region.clone(),
8655                        self.config.account_id.clone(),
8656                        self.config.role_arn.clone(),
8657                    );
8658
8659                    for tab in &self.tabs {
8660                        session.tabs.push(SessionTab {
8661                            service: format!("{:?}", tab.service),
8662                            title: tab.title.clone(),
8663                            breadcrumb: tab.breadcrumb.clone(),
8664                            filter: None,
8665                            selected_item: None,
8666                        });
8667                    }
8668
8669                    let _ = session.save();
8670                }
8671
8672                self.region = region.code.to_string();
8673                self.config.region = region.code.to_string();
8674
8675                // Close all tabs - region change invalidates all data
8676                self.tabs.clear();
8677                self.current_tab = 0;
8678                self.service_selected = false;
8679
8680                self.mode = Mode::Normal;
8681            }
8682        } else if self.mode == Mode::ProfilePicker {
8683            let filtered = self.get_filtered_profiles();
8684            if let Some(profile) = filtered.get(self.profile_picker_selected) {
8685                let profile_name = profile.name.clone();
8686                let profile_region = profile.region.clone();
8687
8688                self.profile = profile_name.clone();
8689                std::env::set_var("AWS_PROFILE", &profile_name);
8690
8691                // Use profile's region if available
8692                if let Some(region) = profile_region {
8693                    self.region = region;
8694                }
8695
8696                self.mode = Mode::Normal;
8697                // Note: Changing profile requires reconnecting to AWS
8698            }
8699        } else if self.mode == Mode::ServicePicker {
8700            let filtered = self.filtered_services();
8701            if let Some(&service) = filtered.get(self.service_picker.selected) {
8702                let new_service = match service {
8703                    "API Gateway › APIs" => Service::ApiGatewayApis,
8704                    "CloudWatch › Log Groups" => Service::CloudWatchLogGroups,
8705                    "CloudWatch › Logs Insights" => Service::CloudWatchInsights,
8706                    "CloudWatch › Alarms" => Service::CloudWatchAlarms,
8707                    "CloudTrail › Event History" => Service::CloudTrailEvents,
8708                    "CloudFormation › Stacks" => Service::CloudFormationStacks,
8709                    "EC2 › Instances" => Service::Ec2Instances,
8710                    "ECR › Repositories" => Service::EcrRepositories,
8711                    "IAM › Users" => Service::IamUsers,
8712                    "IAM › Roles" => Service::IamRoles,
8713                    "IAM › User Groups" => Service::IamUserGroups,
8714                    "Lambda › Functions" => Service::LambdaFunctions,
8715                    "Lambda › Applications" => Service::LambdaApplications,
8716                    "S3 › Buckets" => Service::S3Buckets,
8717                    "SQS › Queues" => Service::SqsQueues,
8718                    _ => return,
8719                };
8720
8721                // Create new tab
8722                self.tabs.push(Tab {
8723                    service: new_service,
8724                    title: service.to_string(),
8725                    breadcrumb: service.to_string(),
8726                });
8727                self.current_tab = self.tabs.len() - 1;
8728                self.current_service = new_service;
8729                self.view_mode = ViewMode::List;
8730                self.service_selected = true;
8731                self.mode = Mode::Normal;
8732            }
8733        } else if self.mode == Mode::TabPicker {
8734            let filtered = self.get_filtered_tabs();
8735            if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
8736                self.current_tab = idx;
8737                self.current_service = self.tabs[idx].service;
8738                self.mode = Mode::Normal;
8739                self.tab_filter.clear();
8740            }
8741        } else if self.mode == Mode::SessionPicker {
8742            let filtered = self.get_filtered_sessions();
8743            if let Some(&session) = filtered.get(self.session_picker_selected) {
8744                let session = session.clone();
8745
8746                // Load the selected session
8747                self.current_session = Some(session.clone());
8748                self.profile = session.profile.clone();
8749                self.region = session.region.clone();
8750                self.config.region = session.region.clone();
8751                self.config.account_id = session.account_id.clone();
8752                self.config.role_arn = session.role_arn.clone();
8753
8754                // Restore tabs
8755                self.tabs = session
8756                    .tabs
8757                    .iter()
8758                    .map(|st| Tab {
8759                        service: match st.service.as_str() {
8760                            "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
8761                            "CloudWatchInsights" => Service::CloudWatchInsights,
8762                            "CloudWatchAlarms" => Service::CloudWatchAlarms,
8763                            "S3Buckets" => Service::S3Buckets,
8764                            "CloudTrailEvents" => Service::CloudTrailEvents,
8765                            "SqsQueues" => Service::SqsQueues,
8766                            _ => Service::CloudWatchLogGroups,
8767                        },
8768                        title: st.title.clone(),
8769                        breadcrumb: st.breadcrumb.clone(),
8770                    })
8771                    .collect();
8772
8773                if !self.tabs.is_empty() {
8774                    self.current_tab = 0;
8775                    self.current_service = self.tabs[0].service;
8776                    self.service_selected = true;
8777                }
8778
8779                self.mode = Mode::Normal;
8780            }
8781        } else if self.mode == Mode::InsightsInput {
8782            // In InsightsInput mode, behavior depends on focus
8783            use crate::app::InsightsFocus;
8784            match self.insights_state.insights.insights_focus {
8785                InsightsFocus::Query => {
8786                    // Add newline to query
8787                    self.insights_state.insights.query_text.push('\n');
8788                    self.insights_state.insights.query_cursor_line += 1;
8789                    self.insights_state.insights.query_cursor_col = 0;
8790                }
8791                InsightsFocus::LogGroupSearch => {
8792                    // Toggle dropdown
8793                    self.insights_state.insights.show_dropdown =
8794                        !self.insights_state.insights.show_dropdown;
8795                }
8796                _ => {}
8797            }
8798        } else if self.mode == Mode::Normal {
8799            // If no service selected, select from service picker
8800            if !self.service_selected {
8801                let filtered = self.filtered_services();
8802                if let Some(&service) = filtered.get(self.service_picker.selected) {
8803                    match service {
8804                        "CloudWatch › Log Groups" => {
8805                            self.current_service = Service::CloudWatchLogGroups;
8806                            self.view_mode = ViewMode::List;
8807                            self.service_selected = true;
8808                        }
8809                        "CloudWatch › Logs Insights" => {
8810                            self.current_service = Service::CloudWatchInsights;
8811                            self.view_mode = ViewMode::InsightsResults;
8812                            self.service_selected = true;
8813                        }
8814                        "CloudWatch › Alarms" => {
8815                            self.current_service = Service::CloudWatchAlarms;
8816                            self.view_mode = ViewMode::List;
8817                            self.service_selected = true;
8818                        }
8819                        "S3 › Buckets" => {
8820                            self.current_service = Service::S3Buckets;
8821                            self.view_mode = ViewMode::List;
8822                            self.service_selected = true;
8823                        }
8824                        "EC2 › Instances" => {
8825                            self.current_service = Service::Ec2Instances;
8826                            self.view_mode = ViewMode::List;
8827                            self.service_selected = true;
8828                        }
8829                        "ECR › Repositories" => {
8830                            self.current_service = Service::EcrRepositories;
8831                            self.view_mode = ViewMode::List;
8832                            self.service_selected = true;
8833                        }
8834                        "Lambda › Functions" => {
8835                            self.current_service = Service::LambdaFunctions;
8836                            self.view_mode = ViewMode::List;
8837                            self.service_selected = true;
8838                        }
8839                        "Lambda › Applications" => {
8840                            self.current_service = Service::LambdaApplications;
8841                            self.view_mode = ViewMode::List;
8842                            self.service_selected = true;
8843                        }
8844                        _ => {}
8845                    }
8846                }
8847                return;
8848            }
8849
8850            // Select in content area
8851            if self.view_mode == ViewMode::InsightsResults {
8852                // Toggle expand for selected result
8853                if self.insights_state.insights.expanded_result
8854                    == Some(self.insights_state.insights.results_selected)
8855                {
8856                    self.insights_state.insights.expanded_result = None;
8857                } else {
8858                    self.insights_state.insights.expanded_result =
8859                        Some(self.insights_state.insights.results_selected);
8860                }
8861            } else if self.current_service == Service::S3Buckets {
8862                if self.s3_state.current_bucket.is_none() {
8863                    // Filter buckets first
8864                    let filtered_buckets: Vec<_> = self
8865                        .s3_state
8866                        .buckets
8867                        .items
8868                        .iter()
8869                        .filter(|b| {
8870                            if self.s3_state.buckets.filter.is_empty() {
8871                                true
8872                            } else {
8873                                b.name
8874                                    .to_lowercase()
8875                                    .contains(&self.s3_state.buckets.filter.to_lowercase())
8876                            }
8877                        })
8878                        .collect();
8879
8880                    // Find which bucket/prefix the selected row corresponds to
8881                    let mut row_idx = 0;
8882                    for bucket in filtered_buckets {
8883                        if row_idx == self.s3_state.selected_row {
8884                            // Selected a bucket - drill into it
8885                            self.s3_state.current_bucket = Some(bucket.name.clone());
8886                            self.s3_state.prefix_stack.clear();
8887                            self.s3_state.buckets.loading = true;
8888                            return;
8889                        }
8890                        row_idx += 1;
8891
8892                        // Skip error rows - they're not selectable
8893                        if self.s3_state.bucket_errors.contains_key(&bucket.name)
8894                            && self.s3_state.expanded_prefixes.contains(&bucket.name)
8895                        {
8896                            continue;
8897                        }
8898
8899                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
8900                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
8901                                for obj in preview {
8902                                    if row_idx == self.s3_state.selected_row {
8903                                        // Selected a prefix - drill into bucket with this prefix
8904                                        if obj.is_prefix {
8905                                            self.s3_state.current_bucket =
8906                                                Some(bucket.name.clone());
8907                                            self.s3_state.prefix_stack = vec![obj.key.clone()];
8908                                            self.s3_state.buckets.loading = true;
8909                                        }
8910                                        return;
8911                                    }
8912                                    row_idx += 1;
8913
8914                                    // Check nested preview rows
8915                                    if obj.is_prefix
8916                                        && self.s3_state.expanded_prefixes.contains(&obj.key)
8917                                    {
8918                                        if let Some(nested) =
8919                                            self.s3_state.prefix_preview.get(&obj.key)
8920                                        {
8921                                            for nested_obj in nested {
8922                                                if row_idx == self.s3_state.selected_row {
8923                                                    // Selected a nested prefix - drill into bucket with this prefix
8924                                                    if nested_obj.is_prefix {
8925                                                        self.s3_state.current_bucket =
8926                                                            Some(bucket.name.clone());
8927                                                        // Build proper prefix stack: parent, then child
8928                                                        self.s3_state.prefix_stack = vec![
8929                                                            obj.key.clone(),
8930                                                            nested_obj.key.clone(),
8931                                                        ];
8932                                                        self.s3_state.buckets.loading = true;
8933                                                    }
8934                                                    return;
8935                                                }
8936                                                row_idx += 1;
8937                                            }
8938                                        } else {
8939                                            row_idx += 1;
8940                                        }
8941                                    }
8942                                }
8943                            } else {
8944                                row_idx += 1;
8945                            }
8946                        }
8947                    }
8948                } else {
8949                    // In objects view - map visual index to actual object (including nested items)
8950                    let mut visual_idx = 0;
8951                    let mut found_obj: Option<S3Object> = None;
8952
8953                    // Helper to recursively check nested items
8954                    fn check_nested_select(
8955                        obj: &S3Object,
8956                        visual_idx: &mut usize,
8957                        target_idx: usize,
8958                        expanded_prefixes: &std::collections::HashSet<String>,
8959                        prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
8960                        found_obj: &mut Option<S3Object>,
8961                    ) {
8962                        if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
8963                            if let Some(preview) = prefix_preview.get(&obj.key) {
8964                                for nested_obj in preview {
8965                                    if *visual_idx == target_idx {
8966                                        *found_obj = Some(nested_obj.clone());
8967                                        return;
8968                                    }
8969                                    *visual_idx += 1;
8970
8971                                    // Recursively check deeper levels
8972                                    check_nested_select(
8973                                        nested_obj,
8974                                        visual_idx,
8975                                        target_idx,
8976                                        expanded_prefixes,
8977                                        prefix_preview,
8978                                        found_obj,
8979                                    );
8980                                    if found_obj.is_some() {
8981                                        return;
8982                                    }
8983                                }
8984                            } else {
8985                                // Loading row
8986                                *visual_idx += 1;
8987                            }
8988                        }
8989                    }
8990
8991                    for obj in &self.s3_state.objects {
8992                        if visual_idx == self.s3_state.selected_object {
8993                            found_obj = Some(obj.clone());
8994                            break;
8995                        }
8996                        visual_idx += 1;
8997
8998                        // Check nested items recursively
8999                        check_nested_select(
9000                            obj,
9001                            &mut visual_idx,
9002                            self.s3_state.selected_object,
9003                            &self.s3_state.expanded_prefixes,
9004                            &self.s3_state.prefix_preview,
9005                            &mut found_obj,
9006                        );
9007                        if found_obj.is_some() {
9008                            break;
9009                        }
9010                    }
9011
9012                    if let Some(obj) = found_obj {
9013                        if obj.is_prefix {
9014                            // Drill into prefix
9015                            self.s3_state.prefix_stack.push(obj.key.clone());
9016                            self.s3_state.buckets.loading = true;
9017                        }
9018                    }
9019                }
9020            } else if self.current_service == Service::ApiGatewayApis {
9021                if self.apig_state.current_api.is_none() {
9022                    // In APIs view - drill into selected API
9023                    let filtered_apis = crate::ui::apig::filtered_apis(self);
9024                    if let Some(api) = self.apig_state.apis.get_selected(&filtered_apis) {
9025                        let protocol = api.protocol_type.to_uppercase();
9026                        self.apig_state.current_api = Some((*api).clone());
9027                        if protocol == "REST" {
9028                            self.apig_state.resources.loading = true;
9029                        } else {
9030                            self.apig_state.routes.loading = true;
9031                        }
9032                        self.update_current_tab_breadcrumb();
9033                    }
9034                }
9035            } else if self.current_service == Service::CloudFormationStacks {
9036                if self.cfn_state.current_stack.is_none() {
9037                    // Drill into stack detail view
9038                    let filtered_stacks = filtered_cloudformation_stacks(self);
9039                    if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
9040                        let stack_name = stack.name.clone();
9041                        let mut tags = stack.tags.clone();
9042                        tags.sort_by(|a, b| a.0.cmp(&b.0));
9043
9044                        self.cfn_state.current_stack = Some(stack_name);
9045                        self.cfn_state.tags.items = tags;
9046                        self.cfn_state.tags.reset();
9047                        self.cfn_state.table.loading = true;
9048                        self.update_current_tab_breadcrumb();
9049                    }
9050                }
9051            } else if self.current_service == Service::CloudTrailEvents {
9052                if self.cloudtrail_state.current_event.is_none() {
9053                    let filtered_events: Vec<_> =
9054                        self.cloudtrail_state.table.items.iter().collect();
9055                    if let Some(event) = self.cloudtrail_state.table.get_selected(&filtered_events)
9056                    {
9057                        self.cloudtrail_state.current_event = Some((*event).clone());
9058                        self.cloudtrail_state.event_json_scroll = 0;
9059                        self.update_current_tab_breadcrumb();
9060                    }
9061                }
9062            } else if self.current_service == Service::EcrRepositories {
9063                if self.ecr_state.current_repository.is_none() {
9064                    // In repositories view - drill into selected repository
9065                    let filtered_repos = filtered_ecr_repositories(self);
9066                    if let Some(repo) = self.ecr_state.repositories.get_selected(&filtered_repos) {
9067                        let repo_name = repo.name.clone();
9068                        let repo_uri = repo.uri.clone();
9069                        self.ecr_state.current_repository = Some(repo_name);
9070                        self.ecr_state.current_repository_uri = Some(repo_uri);
9071                        self.ecr_state.images.reset();
9072                        self.ecr_state.repositories.loading = true;
9073                    }
9074                }
9075            } else if self.current_service == Service::Ec2Instances {
9076                if self.ec2_state.current_instance.is_none() {
9077                    let filtered_instances = filtered_ec2_instances(self);
9078                    if let Some(instance) = self.ec2_state.table.get_selected(&filtered_instances) {
9079                        self.ec2_state.current_instance = Some(instance.instance_id.clone());
9080                        self.view_mode = ViewMode::Detail;
9081                        self.update_current_tab_breadcrumb();
9082                    }
9083                }
9084            } else if self.current_service == Service::SqsQueues {
9085                if self.sqs_state.current_queue.is_none() {
9086                    let filtered_queues = filtered_queues(
9087                        &self.sqs_state.queues.items,
9088                        &self.sqs_state.queues.filter,
9089                    );
9090                    if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
9091                        self.sqs_state.current_queue = Some(queue.url.clone());
9092
9093                        if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
9094                            self.sqs_state.metrics_loading = true;
9095                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
9096                            self.sqs_state.triggers.loading = true;
9097                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
9098                            self.sqs_state.pipes.loading = true;
9099                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
9100                            self.sqs_state.tags.loading = true;
9101                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
9102                            self.sqs_state.subscriptions.loading = true;
9103                        }
9104                    }
9105                }
9106            } else if self.current_service == Service::IamUsers {
9107                if self.iam_state.current_user.is_some() {
9108                    // Open policy view only when in Permissions tab
9109                    if self.iam_state.user_tab == UserTab::Permissions {
9110                        let filtered = filtered_iam_policies(self);
9111                        if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
9112                            self.iam_state.current_policy = Some(policy.policy_name.clone());
9113                            self.iam_state.policy_scroll = 0;
9114                            self.view_mode = ViewMode::PolicyView;
9115                            self.iam_state.policies.loading = true;
9116                            self.update_current_tab_breadcrumb();
9117                        }
9118                    }
9119                } else if self.iam_state.current_user.is_none() {
9120                    let filtered_users = filtered_iam_users(self);
9121                    if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
9122                        self.iam_state.current_user = Some(user.user_name.clone());
9123                        self.iam_state.user_tab = UserTab::Permissions;
9124                        self.iam_state.policies.reset();
9125                        self.update_current_tab_breadcrumb();
9126                    }
9127                }
9128            } else if self.current_service == Service::IamRoles {
9129                if self.iam_state.current_role.is_some() {
9130                    // Open policy view only when in Permissions tab
9131                    if self.iam_state.role_tab == RoleTab::Permissions {
9132                        let filtered = filtered_iam_policies(self);
9133                        if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
9134                            self.iam_state.current_policy = Some(policy.policy_name.clone());
9135                            self.iam_state.policy_scroll = 0;
9136                            self.view_mode = ViewMode::PolicyView;
9137                            self.iam_state.policies.loading = true;
9138                            self.update_current_tab_breadcrumb();
9139                        }
9140                    }
9141                } else if self.iam_state.current_role.is_none() {
9142                    let filtered_roles = filtered_iam_roles(self);
9143                    if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
9144                        self.iam_state.current_role = Some(role.role_name.clone());
9145                        self.iam_state.role_tab = RoleTab::Permissions;
9146                        self.iam_state.policies.reset();
9147                        self.update_current_tab_breadcrumb();
9148                    }
9149                }
9150            } else if self.current_service == Service::IamUserGroups {
9151                if self.iam_state.current_group.is_none() {
9152                    let filtered_groups: Vec<_> = self
9153                        .iam_state
9154                        .groups
9155                        .items
9156                        .iter()
9157                        .filter(|g| {
9158                            if self.iam_state.groups.filter.is_empty() {
9159                                true
9160                            } else {
9161                                g.group_name
9162                                    .to_lowercase()
9163                                    .contains(&self.iam_state.groups.filter.to_lowercase())
9164                            }
9165                        })
9166                        .collect();
9167                    if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
9168                        self.iam_state.current_group = Some(group.group_name.clone());
9169                        self.update_current_tab_breadcrumb();
9170                    }
9171                }
9172            } else if self.current_service == Service::LambdaFunctions {
9173                if self.lambda_state.current_function.is_some()
9174                    && self.lambda_state.detail_tab == LambdaDetailTab::Versions
9175                {
9176                    // In Normal mode, select version to open detail view
9177                    // In other modes (FilterInput), toggle expansion
9178                    if self.mode == Mode::Normal {
9179                        let page_size = self.lambda_state.version_table.page_size.value();
9180                        let filtered: Vec<_> = self
9181                            .lambda_state
9182                            .version_table
9183                            .items
9184                            .iter()
9185                            .filter(|v| {
9186                                self.lambda_state.version_table.filter.is_empty()
9187                                    || v.version.to_lowercase().contains(
9188                                        &self.lambda_state.version_table.filter.to_lowercase(),
9189                                    )
9190                                    || v.aliases.to_lowercase().contains(
9191                                        &self.lambda_state.version_table.filter.to_lowercase(),
9192                                    )
9193                            })
9194                            .collect();
9195                        let current_page = self.lambda_state.version_table.selected / page_size;
9196                        let start_idx = current_page * page_size;
9197                        let end_idx = (start_idx + page_size).min(filtered.len());
9198                        let paginated: Vec<_> = filtered[start_idx..end_idx].to_vec();
9199                        let page_index = self.lambda_state.version_table.selected % page_size;
9200                        if let Some(version) = paginated.get(page_index) {
9201                            self.lambda_state.current_version = Some(version.version.clone());
9202                            self.lambda_state.detail_tab = LambdaDetailTab::Code;
9203                        }
9204                    } else {
9205                        // Toggle expansion
9206                        if self.lambda_state.version_table.expanded_item
9207                            == Some(self.lambda_state.version_table.selected)
9208                        {
9209                            self.lambda_state.version_table.collapse();
9210                        } else {
9211                            self.lambda_state.version_table.expanded_item =
9212                                Some(self.lambda_state.version_table.selected);
9213                        }
9214                    }
9215                } else if self.lambda_state.current_function.is_some()
9216                    && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
9217                {
9218                    // Select alias to view detail (no tab change - alias view has no tabs)
9219                    let filtered: Vec<_> = self
9220                        .lambda_state
9221                        .alias_table
9222                        .items
9223                        .iter()
9224                        .filter(|a| {
9225                            self.lambda_state.alias_table.filter.is_empty()
9226                                || a.name
9227                                    .to_lowercase()
9228                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
9229                                || a.versions
9230                                    .to_lowercase()
9231                                    .contains(&self.lambda_state.alias_table.filter.to_lowercase())
9232                        })
9233                        .collect();
9234                    if let Some(alias) = self.lambda_state.alias_table.get_selected(&filtered) {
9235                        self.lambda_state.current_alias = Some(alias.name.clone());
9236                    }
9237                } else if self.lambda_state.current_function.is_none() {
9238                    let filtered_functions = filtered_lambda_functions(self);
9239                    if let Some(func) = self.lambda_state.table.get_selected(&filtered_functions) {
9240                        self.lambda_state.current_function = Some(func.name.clone());
9241                        self.lambda_state.detail_tab = LambdaDetailTab::Code;
9242                        self.update_current_tab_breadcrumb();
9243                    }
9244                }
9245            } else if self.current_service == Service::LambdaApplications {
9246                let filtered = filtered_lambda_applications(self);
9247                if let Some(app) = self.lambda_application_state.table.get_selected(&filtered) {
9248                    let app_name = app.name.clone();
9249                    self.lambda_application_state.current_application = Some(app_name.clone());
9250                    self.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Overview;
9251
9252                    // Load mock resources
9253                    use crate::lambda::Resource;
9254                    self.lambda_application_state.resources.items = vec![
9255                        Resource {
9256                            logical_id: "ApiGatewayRestApi".to_string(),
9257                            physical_id: "abc123xyz".to_string(),
9258                            resource_type: "AWS::ApiGateway::RestApi".to_string(),
9259                            last_modified: "2025-01-10 14:30:00 (UTC)".to_string(),
9260                        },
9261                        Resource {
9262                            logical_id: "LambdaFunction".to_string(),
9263                            physical_id: format!("{}-function", app_name),
9264                            resource_type: "AWS::Lambda::Function".to_string(),
9265                            last_modified: "2025-01-10 14:25:00 (UTC)".to_string(),
9266                        },
9267                        Resource {
9268                            logical_id: "DynamoDBTable".to_string(),
9269                            physical_id: format!("{}-table", app_name),
9270                            resource_type: "AWS::DynamoDB::Table".to_string(),
9271                            last_modified: "2025-01-09 10:15:00 (UTC)".to_string(),
9272                        },
9273                    ];
9274
9275                    // Load mock deployments
9276                    use crate::lambda::Deployment;
9277                    self.lambda_application_state.deployments.items = vec![
9278                        Deployment {
9279                            deployment_id: "d-ABC123XYZ".to_string(),
9280                            resource_type: "AWS::Serverless::Application".to_string(),
9281                            last_updated: "2025-01-10 14:30:00 (UTC)".to_string(),
9282                            status: "Succeeded".to_string(),
9283                        },
9284                        Deployment {
9285                            deployment_id: "d-DEF456UVW".to_string(),
9286                            resource_type: "AWS::Serverless::Application".to_string(),
9287                            last_updated: "2025-01-09 10:15:00 (UTC)".to_string(),
9288                            status: "Succeeded".to_string(),
9289                        },
9290                    ];
9291
9292                    self.update_current_tab_breadcrumb();
9293                }
9294            } else if self.current_service == Service::CloudWatchLogGroups {
9295                if self.view_mode == ViewMode::List {
9296                    // Map filtered selection to actual group index
9297                    let filtered_groups = filtered_log_groups(self);
9298                    if let Some(selected_group) =
9299                        filtered_groups.get(self.log_groups_state.log_groups.selected)
9300                    {
9301                        if let Some(actual_idx) = self
9302                            .log_groups_state
9303                            .log_groups
9304                            .items
9305                            .iter()
9306                            .position(|g| g.name == selected_group.name)
9307                        {
9308                            self.log_groups_state.log_groups.selected = actual_idx;
9309                        }
9310                    }
9311                    self.view_mode = ViewMode::Detail;
9312                    self.log_groups_state.log_streams.clear();
9313                    self.log_groups_state.tags.items.clear();
9314                    self.log_groups_state.tags.reset();
9315                    self.log_groups_state.selected_stream = 0;
9316                    self.log_groups_state.loading = true;
9317                    self.column_selector_index = 0;
9318                    self.update_current_tab_breadcrumb();
9319                } else if self.view_mode == ViewMode::Detail {
9320                    // Map filtered stream selection to actual stream index
9321                    let filtered_streams = filtered_log_streams(self);
9322                    if let Some(selected_stream) =
9323                        filtered_streams.get(self.log_groups_state.selected_stream)
9324                    {
9325                        if let Some(actual_idx) = self
9326                            .log_groups_state
9327                            .log_streams
9328                            .iter()
9329                            .position(|s| s.name == selected_stream.name)
9330                        {
9331                            self.log_groups_state.selected_stream = actual_idx;
9332                        }
9333                    }
9334                    self.view_mode = ViewMode::Events;
9335                    self.update_current_tab_breadcrumb();
9336                    self.log_groups_state.log_events.clear();
9337                    self.log_groups_state.event_scroll_offset = 0;
9338                    self.log_groups_state.next_backward_token = None;
9339                    self.log_groups_state.loading = true;
9340                } else if self.view_mode == ViewMode::Events {
9341                    // Toggle expand for selected event
9342                    if self.log_groups_state.expanded_event
9343                        == Some(self.log_groups_state.event_scroll_offset)
9344                    {
9345                        self.log_groups_state.expanded_event = None;
9346                    } else {
9347                        self.log_groups_state.expanded_event =
9348                            Some(self.log_groups_state.event_scroll_offset);
9349                    }
9350                }
9351            } else if self.current_service == Service::CloudWatchAlarms {
9352                // Toggle expand for selected alarm
9353                self.alarms_state.table.toggle_expand();
9354            } else if self.current_service == Service::CloudWatchInsights {
9355                // In Normal mode, Enter always executes query
9356                if !self.insights_state.insights.selected_log_groups.is_empty() {
9357                    self.log_groups_state.loading = true;
9358                    self.insights_state.insights.query_completed = true;
9359                }
9360            }
9361        }
9362    }
9363
9364    pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
9365        self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
9366        Ok(())
9367    }
9368
9369    pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
9370        let alarms = self.alarms_client.list_alarms().await?;
9371        self.alarms_state.table.items = alarms
9372            .into_iter()
9373            .map(
9374                |(
9375                    name,
9376                    state,
9377                    state_updated,
9378                    description,
9379                    metric_name,
9380                    namespace,
9381                    statistic,
9382                    period,
9383                    comparison,
9384                    threshold,
9385                    actions_enabled,
9386                    state_reason,
9387                    resource,
9388                    dimensions,
9389                    expression,
9390                    alarm_type,
9391                    cross_account,
9392                )| Alarm {
9393                    name,
9394                    state,
9395                    state_updated_timestamp: state_updated,
9396                    description,
9397                    metric_name,
9398                    namespace,
9399                    statistic,
9400                    period,
9401                    comparison_operator: comparison,
9402                    threshold,
9403                    actions_enabled,
9404                    state_reason,
9405                    resource,
9406                    dimensions,
9407                    expression,
9408                    alarm_type,
9409                    cross_account,
9410                },
9411            )
9412            .collect();
9413        Ok(())
9414    }
9415
9416    pub async fn load_cloudtrail_events(&mut self) -> anyhow::Result<()> {
9417        let (events, next_token) = self.cloudtrail_client.lookup_events(None, None).await?;
9418        self.cloudtrail_state.table.items = events
9419            .into_iter()
9420            .map(
9421                |(
9422                    event_name,
9423                    event_time,
9424                    username,
9425                    event_source,
9426                    resource_type,
9427                    resource_name,
9428                    read_only,
9429                    aws_region,
9430                    event_id,
9431                    access_key_id,
9432                    source_ip_address,
9433                    error_code,
9434                    request_id,
9435                    event_type,
9436                    cloud_trail_event_json,
9437                )| CloudTrailEvent {
9438                    event_name,
9439                    event_time,
9440                    username,
9441                    event_source,
9442                    resource_type,
9443                    resource_name,
9444                    read_only,
9445                    aws_region,
9446                    event_id,
9447                    access_key_id,
9448                    source_ip_address,
9449                    error_code,
9450                    request_id,
9451                    event_type,
9452                    cloud_trail_event_json,
9453                },
9454            )
9455            .collect();
9456        self.cloudtrail_state.table.next_token = next_token;
9457        Ok(())
9458    }
9459
9460    pub async fn load_more_cloudtrail_events(&mut self) -> anyhow::Result<()> {
9461        if let Some(token) = self.cloudtrail_state.table.next_token.clone() {
9462            // Just load the next batch of events
9463            let (events, next_token) = self
9464                .cloudtrail_client
9465                .lookup_events(None, Some(token))
9466                .await?;
9467            self.cloudtrail_state
9468                .table
9469                .items
9470                .extend(events.into_iter().map(
9471                    |(
9472                        event_name,
9473                        event_time,
9474                        username,
9475                        event_source,
9476                        resource_type,
9477                        resource_name,
9478                        read_only,
9479                        aws_region,
9480                        event_id,
9481                        access_key_id,
9482                        source_ip_address,
9483                        error_code,
9484                        request_id,
9485                        event_type,
9486                        cloud_trail_event_json,
9487                    )| CloudTrailEvent {
9488                        event_name,
9489                        event_time,
9490                        username,
9491                        event_source,
9492                        resource_type,
9493                        resource_name,
9494                        read_only,
9495                        aws_region,
9496                        event_id,
9497                        access_key_id,
9498                        source_ip_address,
9499                        error_code,
9500                        request_id,
9501                        event_type,
9502                        cloud_trail_event_json,
9503                    },
9504                ));
9505            self.cloudtrail_state.table.next_token = next_token;
9506        }
9507        Ok(())
9508    }
9509
9510    pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
9511        if let Some(bucket_name) = &self.s3_state.current_bucket {
9512            // Get or fetch bucket region
9513            let bucket_region = if let Some(bucket) = self
9514                .s3_state
9515                .buckets
9516                .items
9517                .iter_mut()
9518                .find(|b| &b.name == bucket_name)
9519            {
9520                if bucket.region.is_empty() {
9521                    // Fetch the actual region
9522                    let region = self.s3_client.get_bucket_location(bucket_name).await?;
9523                    bucket.region = region.clone();
9524                    region
9525                } else {
9526                    bucket.region.clone()
9527                }
9528            } else {
9529                self.config.region.clone()
9530            };
9531
9532            let prefix = self
9533                .s3_state
9534                .prefix_stack
9535                .last()
9536                .cloned()
9537                .unwrap_or_default();
9538            let objects = self
9539                .s3_client
9540                .list_objects(bucket_name, &bucket_region, &prefix)
9541                .await?;
9542            self.s3_state.objects = objects
9543                .into_iter()
9544                .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
9545                    key,
9546                    size,
9547                    last_modified: modified,
9548                    is_prefix,
9549                    storage_class,
9550                })
9551                .collect();
9552            self.s3_state.selected_object = 0;
9553        }
9554        Ok(())
9555    }
9556
9557    pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
9558        let bucket_region = self
9559            .s3_state
9560            .buckets
9561            .items
9562            .iter()
9563            .find(|b| b.name == bucket_name)
9564            .and_then(|b| {
9565                if b.region.is_empty() {
9566                    None
9567                } else {
9568                    Some(b.region.as_str())
9569                }
9570            })
9571            .unwrap_or(self.config.region.as_str());
9572        let objects = self
9573            .s3_client
9574            .list_objects(&bucket_name, bucket_region, "")
9575            .await?;
9576        let preview: Vec<S3Object> = objects
9577            .into_iter()
9578            .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
9579                key,
9580                size,
9581                last_modified: modified,
9582                is_prefix,
9583                storage_class,
9584            })
9585            .collect();
9586        self.s3_state.bucket_preview.insert(bucket_name, preview);
9587        Ok(())
9588    }
9589
9590    pub async fn load_prefix_preview(
9591        &mut self,
9592        bucket_name: String,
9593        prefix: String,
9594    ) -> anyhow::Result<()> {
9595        let bucket_region = self
9596            .s3_state
9597            .buckets
9598            .items
9599            .iter()
9600            .find(|b| b.name == bucket_name)
9601            .and_then(|b| {
9602                if b.region.is_empty() {
9603                    None
9604                } else {
9605                    Some(b.region.as_str())
9606                }
9607            })
9608            .unwrap_or(self.config.region.as_str());
9609        let objects = self
9610            .s3_client
9611            .list_objects(&bucket_name, bucket_region, &prefix)
9612            .await?;
9613        let preview: Vec<S3Object> = objects
9614            .into_iter()
9615            .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
9616                key,
9617                size,
9618                last_modified: modified,
9619                is_prefix,
9620                storage_class,
9621            })
9622            .collect();
9623        self.s3_state.prefix_preview.insert(prefix, preview);
9624        Ok(())
9625    }
9626
9627    pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
9628        let repos = match self.ecr_state.tab {
9629            EcrTab::Private => self.ecr_client.list_private_repositories().await?,
9630            EcrTab::Public => self.ecr_client.list_public_repositories().await?,
9631        };
9632
9633        self.ecr_state.repositories.items = repos
9634            .into_iter()
9635            .map(|r| EcrRepository {
9636                name: r.name,
9637                uri: r.uri,
9638                created_at: r.created_at,
9639                tag_immutability: r.tag_immutability,
9640                encryption_type: r.encryption_type,
9641            })
9642            .collect();
9643
9644        self.ecr_state
9645            .repositories
9646            .items
9647            .sort_by(|a, b| a.name.cmp(&b.name));
9648        Ok(())
9649    }
9650
9651    pub async fn load_apis(&mut self) -> anyhow::Result<()> {
9652        let apis = self.apig_client.list_rest_apis().await?;
9653
9654        self.apig_state.apis.items = apis
9655            .into_iter()
9656            .map(|a| crate::apig::api::RestApi {
9657                id: a.id,
9658                name: a.name,
9659                description: a.description,
9660                created_date: a.created_date,
9661                api_key_source: a.api_key_source,
9662                endpoint_configuration: a.endpoint_configuration,
9663                protocol_type: a.protocol_type,
9664                disable_execute_api_endpoint: a.disable_execute_api_endpoint,
9665                status: a.status,
9666            })
9667            .collect();
9668
9669        self.apig_state
9670            .apis
9671            .items
9672            .sort_by(|a, b| a.name.cmp(&b.name));
9673        Ok(())
9674    }
9675
9676    pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
9677        let instances = self.ec2_client.list_instances().await?;
9678
9679        self.ec2_state.table.items = instances
9680            .into_iter()
9681            .map(|i| Ec2Instance {
9682                instance_id: i.instance_id,
9683                name: i.name,
9684                state: i.state,
9685                instance_type: i.instance_type,
9686                availability_zone: i.availability_zone,
9687                public_ipv4_dns: i.public_ipv4_dns,
9688                public_ipv4_address: i.public_ipv4_address,
9689                elastic_ip: i.elastic_ip,
9690                ipv6_ips: i.ipv6_ips,
9691                monitoring: i.monitoring,
9692                security_groups: i.security_groups,
9693                key_name: i.key_name,
9694                launch_time: i.launch_time,
9695                platform_details: i.platform_details,
9696                status_checks: i.status_checks,
9697                alarm_status: i.alarm_status,
9698                private_dns_name: String::new(),
9699                private_ip_address: String::new(),
9700                security_group_ids: String::new(),
9701                owner_id: String::new(),
9702                volume_id: String::new(),
9703                root_device_name: String::new(),
9704                root_device_type: String::new(),
9705                ebs_optimized: String::new(),
9706                image_id: String::new(),
9707                kernel_id: String::new(),
9708                ramdisk_id: String::new(),
9709                ami_launch_index: String::new(),
9710                reservation_id: String::new(),
9711                vpc_id: String::new(),
9712                subnet_ids: String::new(),
9713                instance_lifecycle: String::new(),
9714                architecture: String::new(),
9715                virtualization_type: String::new(),
9716                platform: String::new(),
9717                iam_instance_profile_arn: String::new(),
9718                tenancy: String::new(),
9719                affinity: String::new(),
9720                host_id: String::new(),
9721                placement_group: String::new(),
9722                partition_number: String::new(),
9723                capacity_reservation_id: String::new(),
9724                state_transition_reason_code: String::new(),
9725                state_transition_reason_message: String::new(),
9726                stop_hibernation_behavior: String::new(),
9727                outpost_arn: String::new(),
9728                product_codes: String::new(),
9729                availability_zone_id: String::new(),
9730                imdsv2: String::new(),
9731                usage_operation: String::new(),
9732                managed: String::new(),
9733                operator: String::new(),
9734            })
9735            .collect();
9736
9737        // Sort by launch time descending by default
9738        self.ec2_state
9739            .table
9740            .items
9741            .sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
9742        Ok(())
9743    }
9744
9745    pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
9746        if let Some(repo_name) = &self.ecr_state.current_repository {
9747            if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
9748                let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
9749
9750                self.ecr_state.images.items = images
9751                    .into_iter()
9752                    .map(|i| EcrImage {
9753                        tag: i.tag,
9754                        artifact_type: i.artifact_type,
9755                        pushed_at: i.pushed_at,
9756                        size_bytes: i.size_bytes,
9757                        uri: i.uri,
9758                        digest: i.digest,
9759                        last_pull_time: i.last_pull_time,
9760                    })
9761                    .collect();
9762
9763                self.ecr_state
9764                    .images
9765                    .items
9766                    .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
9767            }
9768        }
9769        Ok(())
9770    }
9771
9772    pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
9773        let stacks = self
9774            .cloudformation_client
9775            .list_stacks(self.cfn_state.view_nested)
9776            .await?;
9777
9778        let mut stacks: Vec<CfnStack> = stacks
9779            .into_iter()
9780            .map(|s| CfnStack {
9781                name: s.name,
9782                stack_id: s.stack_id,
9783                status: s.status,
9784                created_time: s.created_time,
9785                updated_time: s.updated_time,
9786                deleted_time: s.deleted_time,
9787                drift_status: s.drift_status,
9788                last_drift_check_time: s.last_drift_check_time,
9789                status_reason: s.status_reason,
9790                description: s.description,
9791                detailed_status: String::new(),
9792                root_stack: String::new(),
9793                parent_stack: String::new(),
9794                termination_protection: false,
9795                iam_role: String::new(),
9796                tags: Vec::new(),
9797                stack_policy: String::new(),
9798                rollback_monitoring_time: String::new(),
9799                rollback_alarms: Vec::new(),
9800                notification_arns: Vec::new(),
9801            })
9802            .collect();
9803
9804        // Sort by created_time DESC
9805        stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
9806
9807        self.cfn_state.table.items = stacks;
9808
9809        Ok(())
9810    }
9811
9812    pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
9813        let template = self.cloudformation_client.get_template(stack_name).await?;
9814        self.cfn_state.template_body = template;
9815        self.cfn_state.template_scroll = 0;
9816        Ok(())
9817    }
9818
9819    pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
9820        let mut parameters = self
9821            .cloudformation_client
9822            .get_stack_parameters(stack_name)
9823            .await?;
9824        parameters.sort_by(|a, b| a.key.cmp(&b.key));
9825        self.cfn_state.parameters.items = parameters;
9826        self.cfn_state.parameters.reset();
9827        Ok(())
9828    }
9829
9830    pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
9831        let outputs = self
9832            .cloudformation_client
9833            .get_stack_outputs(stack_name)
9834            .await?;
9835        self.cfn_state.outputs.items = outputs;
9836        self.cfn_state.outputs.reset();
9837        Ok(())
9838    }
9839
9840    pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
9841        let resources = self
9842            .cloudformation_client
9843            .get_stack_resources(stack_name)
9844            .await?;
9845        self.cfn_state.resources.items = resources;
9846        self.cfn_state.resources.reset();
9847        Ok(())
9848    }
9849
9850    pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
9851        // Load attached (managed) policies
9852        let attached_policies = self
9853            .iam_client
9854            .list_attached_role_policies(role_name)
9855            .await
9856            .map_err(|e| anyhow::anyhow!(e))?;
9857
9858        let mut policies: Vec<IamPolicy> = attached_policies
9859            .into_iter()
9860            .map(|p| IamPolicy {
9861                policy_name: p.policy_name().unwrap_or("").to_string(),
9862                policy_type: "Managed".to_string(),
9863                attached_via: "Direct".to_string(),
9864                attached_entities: "-".to_string(),
9865                description: "-".to_string(),
9866                creation_time: "-".to_string(),
9867                edited_time: "-".to_string(),
9868                policy_arn: p.policy_arn().map(|s| s.to_string()),
9869            })
9870            .collect();
9871
9872        // Load inline policies
9873        let inline_policy_names = self
9874            .iam_client
9875            .list_role_policies(role_name)
9876            .await
9877            .map_err(|e| anyhow::anyhow!(e))?;
9878
9879        for policy_name in inline_policy_names {
9880            policies.push(IamPolicy {
9881                policy_name,
9882                policy_type: "Inline".to_string(),
9883                attached_via: "Direct".to_string(),
9884                attached_entities: "-".to_string(),
9885                description: "-".to_string(),
9886                creation_time: "-".to_string(),
9887                edited_time: "-".to_string(),
9888                policy_arn: None,
9889            });
9890        }
9891
9892        self.iam_state.policies.items = policies;
9893
9894        Ok(())
9895    }
9896
9897    pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
9898        let attached_policies = self
9899            .iam_client
9900            .list_attached_group_policies(group_name)
9901            .await
9902            .map_err(|e| anyhow::anyhow!(e))?;
9903
9904        let mut policies: Vec<IamPolicy> = attached_policies
9905            .into_iter()
9906            .map(|p| IamPolicy {
9907                policy_name: p.policy_name().unwrap_or("").to_string(),
9908                policy_type: "AWS managed".to_string(),
9909                attached_via: "Direct".to_string(),
9910                attached_entities: "-".to_string(),
9911                description: "-".to_string(),
9912                creation_time: "-".to_string(),
9913                edited_time: "-".to_string(),
9914                policy_arn: p.policy_arn().map(|s| s.to_string()),
9915            })
9916            .collect();
9917
9918        let inline_policy_names = self
9919            .iam_client
9920            .list_group_policies(group_name)
9921            .await
9922            .map_err(|e| anyhow::anyhow!(e))?;
9923
9924        for policy_name in inline_policy_names {
9925            policies.push(IamPolicy {
9926                policy_name,
9927                policy_type: "Inline".to_string(),
9928                attached_via: "Direct".to_string(),
9929                attached_entities: "-".to_string(),
9930                description: "-".to_string(),
9931                creation_time: "-".to_string(),
9932                edited_time: "-".to_string(),
9933                policy_arn: None,
9934            });
9935        }
9936
9937        self.iam_state.policies.items = policies;
9938
9939        Ok(())
9940    }
9941
9942    pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
9943        let users = self
9944            .iam_client
9945            .get_group_users(group_name)
9946            .await
9947            .map_err(|e| anyhow::anyhow!(e))?;
9948
9949        let group_users: Vec<IamGroupUser> = users
9950            .into_iter()
9951            .map(|u| {
9952                let creation_time = {
9953                    let dt = u.create_date();
9954                    let timestamp = dt.secs();
9955                    let datetime =
9956                        chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
9957                    datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
9958                };
9959
9960                IamGroupUser {
9961                    user_name: u.user_name().to_string(),
9962                    groups: String::new(),
9963                    last_activity: String::new(),
9964                    creation_time,
9965                }
9966            })
9967            .collect();
9968
9969        self.iam_state.group_users.items = group_users;
9970
9971        Ok(())
9972    }
9973
9974    pub async fn load_policy_document(
9975        &mut self,
9976        role_name: &str,
9977        policy_name: &str,
9978    ) -> anyhow::Result<()> {
9979        // Find the policy to get its ARN and type
9980        let policy = self
9981            .iam_state
9982            .policies
9983            .items
9984            .iter()
9985            .find(|p| p.policy_name == policy_name)
9986            .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
9987
9988        let document = if let Some(policy_arn) = &policy.policy_arn {
9989            // Managed policy - use get_policy_version
9990            self.iam_client
9991                .get_policy_version(policy_arn)
9992                .await
9993                .map_err(|e| anyhow::anyhow!(e))?
9994        } else {
9995            // Inline policy - use get_role_policy
9996            self.iam_client
9997                .get_role_policy(role_name, policy_name)
9998                .await
9999                .map_err(|e| anyhow::anyhow!(e))?
10000        };
10001
10002        self.iam_state.policy_document = document;
10003
10004        Ok(())
10005    }
10006
10007    pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
10008        let document = self
10009            .iam_client
10010            .get_role(role_name)
10011            .await
10012            .map_err(|e| anyhow::anyhow!(e))?;
10013
10014        self.iam_state.trust_policy_document = document;
10015
10016        Ok(())
10017    }
10018
10019    pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
10020        // TODO: Implement real AWS API call to get service last accessed details
10021        self.iam_state.last_accessed_services.items = vec![];
10022        self.iam_state.last_accessed_services.selected = 0;
10023
10024        Ok(())
10025    }
10026
10027    pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
10028        let tags = self
10029            .iam_client
10030            .list_role_tags(role_name)
10031            .await
10032            .map_err(|e| anyhow::anyhow!(e))?;
10033        self.iam_state.tags.items = tags
10034            .into_iter()
10035            .map(|(k, v)| IamRoleTag { key: k, value: v })
10036            .collect();
10037        self.iam_state.tags.reset();
10038        Ok(())
10039    }
10040
10041    pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
10042        let tags = self
10043            .iam_client
10044            .list_user_tags(user_name)
10045            .await
10046            .map_err(|e| anyhow::anyhow!(e))?;
10047        self.iam_state.user_tags.items = tags
10048            .into_iter()
10049            .map(|(k, v)| IamUserTag { key: k, value: v })
10050            .collect();
10051        self.iam_state.user_tags.reset();
10052        Ok(())
10053    }
10054
10055    pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
10056        if let Some(group) = self
10057            .log_groups_state
10058            .log_groups
10059            .items
10060            .get(self.log_groups_state.log_groups.selected)
10061        {
10062            self.log_groups_state.log_streams =
10063                self.cloudwatch_client.list_log_streams(&group.name).await?;
10064            self.log_groups_state.selected_stream = 0;
10065        }
10066        Ok(())
10067    }
10068
10069    pub async fn load_log_group_tags(&mut self) -> anyhow::Result<()> {
10070        if let Some(group) = self
10071            .log_groups_state
10072            .log_groups
10073            .items
10074            .get(self.log_groups_state.log_groups.selected)
10075        {
10076            // Use log_group_arn if available, otherwise construct from name
10077            let arn = if let Some(arn) = &group.log_group_arn {
10078                arn.clone()
10079            } else if let Some(arn) = &group.arn {
10080                arn.clone()
10081            } else {
10082                // Construct ARN from log group name
10083                let account_id = if self.config.account_id.is_empty() {
10084                    "*"
10085                } else {
10086                    &self.config.account_id
10087                };
10088                format!(
10089                    "arn:aws:logs:{}:{}:log-group:{}",
10090                    self.config.region, account_id, group.name
10091                )
10092            };
10093
10094            let tags = self.cloudwatch_client.list_tags_for_log_group(&arn).await?;
10095            self.log_groups_state.tags.items = tags;
10096            self.log_groups_state.tags.selected = 0;
10097            self.log_groups_state.tags.scroll_offset = 0;
10098        }
10099        Ok(())
10100    }
10101
10102    pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
10103        if let Some(group) = self
10104            .log_groups_state
10105            .log_groups
10106            .items
10107            .get(self.log_groups_state.log_groups.selected)
10108        {
10109            if let Some(stream) = self
10110                .log_groups_state
10111                .log_streams
10112                .get(self.log_groups_state.selected_stream)
10113            {
10114                // Calculate time range from relative date picker
10115                let (start_time, end_time) =
10116                    if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
10117                        let now = chrono::Utc::now().timestamp_millis();
10118                        let duration_ms = match self.log_groups_state.relative_unit {
10119                            TimeUnit::Minutes => amount * 60 * 1000,
10120                            TimeUnit::Hours => amount * 60 * 60 * 1000,
10121                            TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
10122                            TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
10123                        };
10124                        (Some(now - duration_ms), Some(now))
10125                    } else {
10126                        (None, None)
10127                    };
10128
10129                let (mut events, has_more, token) = self
10130                    .cloudwatch_client
10131                    .get_log_events(
10132                        &group.name,
10133                        &stream.name,
10134                        self.log_groups_state.next_backward_token.clone(),
10135                        start_time,
10136                        end_time,
10137                    )
10138                    .await?;
10139
10140                if self.log_groups_state.next_backward_token.is_some() {
10141                    // Prepend older events - keep selection at top
10142                    events.append(&mut self.log_groups_state.log_events);
10143                    self.log_groups_state.event_scroll_offset = 0;
10144                } else {
10145                    // Initial load - start at first event
10146                    self.log_groups_state.event_scroll_offset = 0;
10147                }
10148
10149                self.log_groups_state.log_events = events;
10150                self.log_groups_state.has_older_events =
10151                    has_more && self.log_groups_state.log_events.len() >= 25;
10152                self.log_groups_state.next_backward_token = token;
10153                self.log_groups_state.selected_event = 0;
10154            }
10155        }
10156        Ok(())
10157    }
10158
10159    pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
10160        if self.insights_state.insights.selected_log_groups.is_empty() {
10161            return Err(anyhow::anyhow!(
10162                "No log groups selected. Please select at least one log group."
10163            ));
10164        }
10165
10166        let now = chrono::Utc::now().timestamp_millis();
10167        let amount = self
10168            .insights_state
10169            .insights
10170            .insights_relative_amount
10171            .parse::<i64>()
10172            .unwrap_or(1);
10173        let duration_ms = match self.insights_state.insights.insights_relative_unit {
10174            TimeUnit::Minutes => amount * 60 * 1000,
10175            TimeUnit::Hours => amount * 60 * 60 * 1000,
10176            TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
10177            TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
10178        };
10179        let start_time = now - duration_ms;
10180
10181        let query_id = self
10182            .cloudwatch_client
10183            .start_query(
10184                self.insights_state.insights.selected_log_groups.clone(),
10185                self.insights_state.insights.query_text.trim().to_string(),
10186                start_time,
10187                now,
10188            )
10189            .await?;
10190
10191        // Poll for results
10192        for _ in 0..60 {
10193            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
10194            let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
10195
10196            if status == "Complete" {
10197                self.insights_state.insights.query_results = results;
10198                self.insights_state.insights.query_completed = true;
10199                self.insights_state.insights.results_selected = 0;
10200                self.insights_state.insights.expanded_result = None;
10201                self.view_mode = ViewMode::InsightsResults;
10202                return Ok(());
10203            } else if status == "Failed" || status == "Cancelled" {
10204                return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
10205            }
10206        }
10207
10208        Err(anyhow::anyhow!("Query timeout"))
10209    }
10210}
10211
10212impl CloudWatchInsightsState {
10213    fn new() -> Self {
10214        Self {
10215            insights: InsightsState::default(),
10216            loading: false,
10217        }
10218    }
10219}
10220
10221impl CloudWatchAlarmsState {
10222    fn new() -> Self {
10223        Self {
10224            table: TableState::new(),
10225            alarm_tab: AlarmTab::AllAlarms,
10226            view_as: AlarmViewMode::Table,
10227            wrap_lines: false,
10228            sort_column: "Last state update".to_string(),
10229            sort_direction: SortDirection::Asc,
10230            input_focus: InputFocus::Filter,
10231        }
10232    }
10233}
10234
10235impl ServicePickerState {
10236    fn new() -> Self {
10237        Self {
10238            filter: String::new(),
10239            filter_active: false,
10240            selected: 0,
10241            services: vec![
10242                "API Gateway › APIs",
10243                "CloudWatch › Log Groups",
10244                "CloudWatch › Logs Insights",
10245                "CloudWatch › Alarms",
10246                "CloudTrail › Event History",
10247                "CloudFormation › Stacks",
10248                "EC2 › Instances",
10249                "ECR › Repositories",
10250                "IAM › Users",
10251                "IAM › Roles",
10252                "IAM › User Groups",
10253                "Lambda › Functions",
10254                "Lambda › Applications",
10255                "S3 › Buckets",
10256                "SQS › Queues",
10257            ],
10258        }
10259    }
10260}
10261
10262#[cfg(test)]
10263mod test_helpers {
10264    use super::*;
10265
10266    // Test helper functions to reduce boilerplate
10267    pub fn test_app() -> App {
10268        App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
10269    }
10270
10271    pub fn test_app_no_region() -> App {
10272        App::new_without_client("test".to_string(), None)
10273    }
10274}
10275
10276#[cfg(test)]
10277mod tests {
10278    use super::*;
10279    use crate::keymap::Action;
10280    use test_helpers::*;
10281
10282    #[test]
10283    fn test_next_tab_cycles_forward() {
10284        let mut app = test_app();
10285        app.tabs = vec![
10286            Tab {
10287                service: Service::CloudWatchLogGroups,
10288                title: "CloudWatch › Log Groups".to_string(),
10289                breadcrumb: "CloudWatch › Log Groups".to_string(),
10290            },
10291            Tab {
10292                service: Service::CloudWatchInsights,
10293                title: "CloudWatch › Logs Insights".to_string(),
10294                breadcrumb: "CloudWatch › Logs Insights".to_string(),
10295            },
10296            Tab {
10297                service: Service::CloudWatchAlarms,
10298                title: "CloudWatch › Alarms".to_string(),
10299                breadcrumb: "CloudWatch › Alarms".to_string(),
10300            },
10301        ];
10302        app.current_tab = 0;
10303
10304        app.handle_action(Action::NextTab);
10305        assert_eq!(app.current_tab, 1);
10306        assert_eq!(app.current_service, Service::CloudWatchInsights);
10307
10308        app.handle_action(Action::NextTab);
10309        assert_eq!(app.current_tab, 2);
10310        assert_eq!(app.current_service, Service::CloudWatchAlarms);
10311
10312        // Should wrap around
10313        app.handle_action(Action::NextTab);
10314        assert_eq!(app.current_tab, 0);
10315        assert_eq!(app.current_service, Service::CloudWatchLogGroups);
10316    }
10317
10318    #[test]
10319    fn test_prev_tab_cycles_backward() {
10320        let mut app = test_app();
10321        app.tabs = vec![
10322            Tab {
10323                service: Service::CloudWatchLogGroups,
10324                title: "CloudWatch › Log Groups".to_string(),
10325                breadcrumb: "CloudWatch › Log Groups".to_string(),
10326            },
10327            Tab {
10328                service: Service::CloudWatchInsights,
10329                title: "CloudWatch › Logs Insights".to_string(),
10330                breadcrumb: "CloudWatch › Logs Insights".to_string(),
10331            },
10332            Tab {
10333                service: Service::CloudWatchAlarms,
10334                title: "CloudWatch › Alarms".to_string(),
10335                breadcrumb: "CloudWatch › Alarms".to_string(),
10336            },
10337        ];
10338        app.current_tab = 2;
10339
10340        app.handle_action(Action::PrevTab);
10341        assert_eq!(app.current_tab, 1);
10342        assert_eq!(app.current_service, Service::CloudWatchInsights);
10343
10344        app.handle_action(Action::PrevTab);
10345        assert_eq!(app.current_tab, 0);
10346        assert_eq!(app.current_service, Service::CloudWatchLogGroups);
10347
10348        // Should wrap around
10349        app.handle_action(Action::PrevTab);
10350        assert_eq!(app.current_tab, 2);
10351        assert_eq!(app.current_service, Service::CloudWatchAlarms);
10352    }
10353
10354    #[test]
10355    fn test_close_tab_removes_current() {
10356        let mut app = test_app();
10357        app.tabs = vec![
10358            Tab {
10359                service: Service::CloudWatchLogGroups,
10360                title: "CloudWatch › Log Groups".to_string(),
10361                breadcrumb: "CloudWatch › Log Groups".to_string(),
10362            },
10363            Tab {
10364                service: Service::CloudWatchInsights,
10365                title: "CloudWatch › Logs Insights".to_string(),
10366                breadcrumb: "CloudWatch › Logs Insights".to_string(),
10367            },
10368            Tab {
10369                service: Service::CloudWatchAlarms,
10370                title: "CloudWatch › Alarms".to_string(),
10371                breadcrumb: "CloudWatch › Alarms".to_string(),
10372            },
10373        ];
10374        app.current_tab = 1;
10375        app.service_selected = true;
10376
10377        app.handle_action(Action::CloseTab);
10378        assert_eq!(app.tabs.len(), 2);
10379        assert_eq!(app.current_tab, 1);
10380        assert_eq!(app.current_service, Service::CloudWatchAlarms);
10381    }
10382
10383    #[test]
10384    fn test_close_last_tab_exits_service() {
10385        let mut app = test_app();
10386        app.tabs = vec![Tab {
10387            service: Service::CloudWatchLogGroups,
10388            title: "CloudWatch › Log Groups".to_string(),
10389            breadcrumb: "CloudWatch › Log Groups".to_string(),
10390        }];
10391        app.current_tab = 0;
10392        app.service_selected = true;
10393
10394        app.handle_action(Action::CloseTab);
10395        assert_eq!(app.tabs.len(), 0);
10396        assert!(!app.service_selected);
10397        assert_eq!(app.current_tab, 0);
10398    }
10399
10400    #[test]
10401    fn test_close_service_removes_current_tab() {
10402        let mut app = test_app();
10403        app.tabs = vec![
10404            Tab {
10405                service: Service::CloudWatchLogGroups,
10406                title: "CloudWatch › Log Groups".to_string(),
10407                breadcrumb: "CloudWatch › Log Groups".to_string(),
10408            },
10409            Tab {
10410                service: Service::CloudWatchInsights,
10411                title: "CloudWatch › Logs Insights".to_string(),
10412                breadcrumb: "CloudWatch › Logs Insights".to_string(),
10413            },
10414            Tab {
10415                service: Service::CloudWatchAlarms,
10416                title: "CloudWatch › Alarms".to_string(),
10417                breadcrumb: "CloudWatch › Alarms".to_string(),
10418            },
10419        ];
10420        app.current_tab = 1;
10421        app.service_selected = true;
10422
10423        app.handle_action(Action::CloseService);
10424
10425        // Tab should be removed
10426        assert_eq!(app.tabs.len(), 2);
10427        // Should switch to next tab (Alarms at index 1)
10428        assert_eq!(app.current_tab, 1);
10429        assert_eq!(app.current_service, Service::CloudWatchAlarms);
10430        // Should stay in service mode, NOT show service picker
10431        assert!(app.service_selected);
10432        assert_eq!(app.mode, Mode::Normal);
10433    }
10434
10435    #[test]
10436    fn test_close_service_last_tab_shows_picker() {
10437        let mut app = test_app();
10438        app.tabs = vec![Tab {
10439            service: Service::CloudWatchLogGroups,
10440            title: "CloudWatch › Log Groups".to_string(),
10441            breadcrumb: "CloudWatch › Log Groups".to_string(),
10442        }];
10443        app.current_tab = 0;
10444        app.service_selected = true;
10445
10446        app.handle_action(Action::CloseService);
10447
10448        // Tab should be removed
10449        assert_eq!(app.tabs.len(), 0);
10450        // Should show service picker
10451        assert!(!app.service_selected);
10452        assert_eq!(app.mode, Mode::ServicePicker);
10453    }
10454
10455    #[test]
10456    fn test_open_tab_picker_with_tabs() {
10457        let mut app = test_app();
10458        app.tabs = vec![
10459            Tab {
10460                service: Service::CloudWatchLogGroups,
10461                title: "CloudWatch › Log Groups".to_string(),
10462                breadcrumb: "CloudWatch › Log Groups".to_string(),
10463            },
10464            Tab {
10465                service: Service::CloudWatchInsights,
10466                title: "CloudWatch › Logs Insights".to_string(),
10467                breadcrumb: "CloudWatch › Logs Insights".to_string(),
10468            },
10469        ];
10470        app.current_tab = 1;
10471
10472        app.handle_action(Action::OpenTabPicker);
10473        assert_eq!(app.mode, Mode::TabPicker);
10474        assert_eq!(app.tab_picker_selected, 1);
10475    }
10476
10477    #[test]
10478    fn test_open_tab_picker_without_tabs() {
10479        let mut app = test_app();
10480        app.tabs = vec![];
10481
10482        app.handle_action(Action::OpenTabPicker);
10483        assert_eq!(app.mode, Mode::Normal);
10484    }
10485
10486    #[test]
10487    fn test_pending_key_state() {
10488        let mut app = test_app();
10489        assert_eq!(app.pending_key, None);
10490
10491        app.pending_key = Some('g');
10492        assert_eq!(app.pending_key, Some('g'));
10493    }
10494
10495    #[test]
10496    fn test_tab_breadcrumb_updates() {
10497        let mut app = test_app();
10498        app.tabs = vec![Tab {
10499            service: Service::CloudWatchLogGroups,
10500            title: "CloudWatch › Log Groups".to_string(),
10501            breadcrumb: "CloudWatch > Log groups".to_string(),
10502        }];
10503        app.current_tab = 0;
10504        app.service_selected = true;
10505        app.current_service = Service::CloudWatchLogGroups;
10506
10507        // Initial breadcrumb
10508        assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
10509
10510        // Add a log group and update breadcrumb
10511        app.log_groups_state
10512            .log_groups
10513            .items
10514            .push(rusticity_core::LogGroup {
10515                name: "/aws/lambda/test".to_string(),
10516                creation_time: None,
10517                stored_bytes: Some(1024),
10518                retention_days: None,
10519                log_class: None,
10520                arn: None,
10521                log_group_arn: None,
10522                deletion_protection_enabled: None,
10523            });
10524        app.log_groups_state.log_groups.reset();
10525        app.view_mode = ViewMode::Detail;
10526        app.update_current_tab_breadcrumb();
10527
10528        // Breadcrumb should now include log group
10529        assert_eq!(
10530            app.tabs[0].breadcrumb,
10531            "CloudWatch > Log groups > /aws/lambda/test"
10532        );
10533    }
10534
10535    #[test]
10536    fn test_s3_bucket_column_selector_navigation() {
10537        let mut app = test_app();
10538        app.current_service = Service::S3Buckets;
10539        app.mode = Mode::ColumnSelector;
10540        app.column_selector_index = 0;
10541
10542        // Should navigate through 3 S3 bucket columns (0, 1, 2)
10543        app.handle_action(Action::NextItem);
10544        assert_eq!(app.column_selector_index, 1);
10545
10546        app.handle_action(Action::NextItem);
10547        assert_eq!(app.column_selector_index, 2);
10548
10549        app.handle_action(Action::NextItem);
10550        assert_eq!(app.column_selector_index, 3);
10551
10552        // Should not go beyond max (now includes page size options: 3 columns + 6 = 9)
10553        for _ in 0..10 {
10554            app.handle_action(Action::NextItem);
10555        }
10556        assert_eq!(app.column_selector_index, 9);
10557
10558        // Navigate back
10559        app.handle_action(Action::PrevItem);
10560        assert_eq!(app.column_selector_index, 8);
10561
10562        app.handle_action(Action::PrevItem);
10563        assert_eq!(app.column_selector_index, 7);
10564
10565        // Should not go below 0
10566        for _ in 0..10 {
10567            app.handle_action(Action::PrevItem);
10568        }
10569        assert_eq!(app.column_selector_index, 0);
10570    }
10571
10572    #[test]
10573    fn test_cloudwatch_alarms_state_initialized() {
10574        let app = test_app();
10575
10576        // Alarms state should be initialized
10577        assert_eq!(app.alarms_state.table.items.len(), 0);
10578        assert_eq!(app.alarms_state.table.selected, 0);
10579        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
10580        assert!(!app.alarms_state.table.loading);
10581        assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
10582        assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
10583    }
10584
10585    #[test]
10586    fn test_cloudwatch_alarms_service_selection() {
10587        let mut app = test_app();
10588
10589        // Switch to alarms service
10590        app.current_service = Service::CloudWatchAlarms;
10591        app.service_selected = true;
10592
10593        assert_eq!(app.current_service, Service::CloudWatchAlarms);
10594        assert!(app.service_selected);
10595    }
10596
10597    #[test]
10598    fn test_cloudwatch_alarms_column_preferences() {
10599        let app = test_app();
10600
10601        // Should have alarm columns defined
10602        assert!(!app.cw_alarm_column_ids.is_empty());
10603        assert!(!app.cw_alarm_visible_column_ids.is_empty());
10604
10605        // Default visible columns
10606        assert!(app
10607            .cw_alarm_visible_column_ids
10608            .contains(&AlarmColumn::Name.id()));
10609        assert!(app
10610            .cw_alarm_visible_column_ids
10611            .contains(&AlarmColumn::State.id()));
10612    }
10613
10614    #[test]
10615    fn test_s3_bucket_navigation_without_expansion() {
10616        let mut app = test_app();
10617        app.current_service = Service::S3Buckets;
10618        app.service_selected = true;
10619        app.mode = Mode::Normal;
10620
10621        // Add 3 buckets
10622        app.s3_state.buckets.items = vec![
10623            S3Bucket {
10624                name: "bucket1".to_string(),
10625                region: "us-east-1".to_string(),
10626                creation_date: "2024-01-01T00:00:00Z".to_string(),
10627            },
10628            S3Bucket {
10629                name: "bucket2".to_string(),
10630                region: "us-east-1".to_string(),
10631                creation_date: "2024-01-02T00:00:00Z".to_string(),
10632            },
10633            S3Bucket {
10634                name: "bucket3".to_string(),
10635                region: "us-east-1".to_string(),
10636                creation_date: "2024-01-03T00:00:00Z".to_string(),
10637            },
10638        ];
10639        app.s3_state.selected_row = 0;
10640
10641        // Navigate down
10642        app.handle_action(Action::NextItem);
10643        assert_eq!(app.s3_state.selected_row, 1);
10644
10645        app.handle_action(Action::NextItem);
10646        assert_eq!(app.s3_state.selected_row, 2);
10647
10648        // Should not go beyond last bucket
10649        app.handle_action(Action::NextItem);
10650        assert_eq!(app.s3_state.selected_row, 2);
10651
10652        // Navigate up
10653        app.handle_action(Action::PrevItem);
10654        assert_eq!(app.s3_state.selected_row, 1);
10655
10656        app.handle_action(Action::PrevItem);
10657        assert_eq!(app.s3_state.selected_row, 0);
10658
10659        // Should not go below 0
10660        app.handle_action(Action::PrevItem);
10661        assert_eq!(app.s3_state.selected_row, 0);
10662    }
10663
10664    #[test]
10665    fn test_s3_bucket_navigation_with_expansion() {
10666        let mut app = test_app();
10667        app.current_service = Service::S3Buckets;
10668        app.service_selected = true;
10669        app.mode = Mode::Normal;
10670
10671        // Add 2 buckets
10672        app.s3_state.buckets.items = vec![
10673            S3Bucket {
10674                name: "bucket1".to_string(),
10675                region: "us-east-1".to_string(),
10676                creation_date: "2024-01-01T00:00:00Z".to_string(),
10677            },
10678            S3Bucket {
10679                name: "bucket2".to_string(),
10680                region: "us-east-1".to_string(),
10681                creation_date: "2024-01-02T00:00:00Z".to_string(),
10682            },
10683        ];
10684
10685        // Expand bucket1 with 2 objects
10686        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
10687        app.s3_state.bucket_preview.insert(
10688            "bucket1".to_string(),
10689            vec![
10690                S3Object {
10691                    key: "file1.txt".to_string(),
10692                    size: 100,
10693                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10694                    is_prefix: false,
10695                    storage_class: "STANDARD".to_string(),
10696                },
10697                S3Object {
10698                    key: "folder/".to_string(),
10699                    size: 0,
10700                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10701                    is_prefix: true,
10702                    storage_class: String::new(),
10703                },
10704            ],
10705        );
10706
10707        app.s3_state.selected_row = 0;
10708
10709        // Total rows: bucket1 (row 0) + file1.txt (row 1) + folder/ (row 2) + bucket2 (row 3) = 4 rows
10710        // Navigate through all rows
10711        app.handle_action(Action::NextItem);
10712        assert_eq!(app.s3_state.selected_row, 1); // file1.txt
10713
10714        app.handle_action(Action::NextItem);
10715        assert_eq!(app.s3_state.selected_row, 2); // folder/
10716
10717        app.handle_action(Action::NextItem);
10718        assert_eq!(app.s3_state.selected_row, 3); // bucket2
10719
10720        // Should not go beyond last row
10721        app.handle_action(Action::NextItem);
10722        assert_eq!(app.s3_state.selected_row, 3);
10723    }
10724
10725    #[test]
10726    fn test_s3_bucket_navigation_with_nested_expansion() {
10727        let mut app = test_app();
10728        app.current_service = Service::S3Buckets;
10729        app.service_selected = true;
10730        app.mode = Mode::Normal;
10731
10732        // Add 1 bucket
10733        app.s3_state.buckets.items = vec![S3Bucket {
10734            name: "bucket1".to_string(),
10735            region: "us-east-1".to_string(),
10736            creation_date: "2024-01-01T00:00:00Z".to_string(),
10737        }];
10738
10739        // Expand bucket1 with a folder
10740        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
10741        app.s3_state.bucket_preview.insert(
10742            "bucket1".to_string(),
10743            vec![S3Object {
10744                key: "folder/".to_string(),
10745                size: 0,
10746                last_modified: "2024-01-01T00:00:00Z".to_string(),
10747                is_prefix: true,
10748                storage_class: String::new(),
10749            }],
10750        );
10751
10752        // Expand the folder with 2 nested objects
10753        app.s3_state.expanded_prefixes.insert("folder/".to_string());
10754        app.s3_state.prefix_preview.insert(
10755            "folder/".to_string(),
10756            vec![
10757                S3Object {
10758                    key: "folder/file1.txt".to_string(),
10759                    size: 100,
10760                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10761                    is_prefix: false,
10762                    storage_class: "STANDARD".to_string(),
10763                },
10764                S3Object {
10765                    key: "folder/file2.txt".to_string(),
10766                    size: 200,
10767                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10768                    is_prefix: false,
10769                    storage_class: "STANDARD".to_string(),
10770                },
10771            ],
10772        );
10773
10774        app.s3_state.selected_row = 0;
10775
10776        // Total rows: bucket1 (0) + folder/ (1) + file1.txt (2) + file2.txt (3) = 4 rows
10777        app.handle_action(Action::NextItem);
10778        assert_eq!(app.s3_state.selected_row, 1); // folder/
10779
10780        app.handle_action(Action::NextItem);
10781        assert_eq!(app.s3_state.selected_row, 2); // folder/file1.txt
10782
10783        app.handle_action(Action::NextItem);
10784        assert_eq!(app.s3_state.selected_row, 3); // folder/file2.txt
10785
10786        // Should not go beyond last row
10787        app.handle_action(Action::NextItem);
10788        assert_eq!(app.s3_state.selected_row, 3);
10789    }
10790
10791    #[test]
10792    fn test_calculate_total_bucket_rows() {
10793        let mut app = test_app();
10794
10795        // No buckets
10796        assert_eq!(app.calculate_total_bucket_rows(), 0);
10797
10798        // 2 buckets, no expansion
10799        app.s3_state.buckets.items = vec![
10800            S3Bucket {
10801                name: "bucket1".to_string(),
10802                region: "us-east-1".to_string(),
10803                creation_date: "2024-01-01T00:00:00Z".to_string(),
10804            },
10805            S3Bucket {
10806                name: "bucket2".to_string(),
10807                region: "us-east-1".to_string(),
10808                creation_date: "2024-01-02T00:00:00Z".to_string(),
10809            },
10810        ];
10811        assert_eq!(app.calculate_total_bucket_rows(), 2);
10812
10813        // Expand bucket1 with 3 objects
10814        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
10815        app.s3_state.bucket_preview.insert(
10816            "bucket1".to_string(),
10817            vec![
10818                S3Object {
10819                    key: "file1.txt".to_string(),
10820                    size: 100,
10821                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10822                    is_prefix: false,
10823                    storage_class: "STANDARD".to_string(),
10824                },
10825                S3Object {
10826                    key: "file2.txt".to_string(),
10827                    size: 200,
10828                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10829                    is_prefix: false,
10830                    storage_class: "STANDARD".to_string(),
10831                },
10832                S3Object {
10833                    key: "folder/".to_string(),
10834                    size: 0,
10835                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10836                    is_prefix: true,
10837                    storage_class: String::new(),
10838                },
10839            ],
10840        );
10841        assert_eq!(app.calculate_total_bucket_rows(), 5); // 2 buckets + 3 objects
10842
10843        // Expand folder/ with 2 nested objects
10844        app.s3_state.expanded_prefixes.insert("folder/".to_string());
10845        app.s3_state.prefix_preview.insert(
10846            "folder/".to_string(),
10847            vec![
10848                S3Object {
10849                    key: "folder/nested1.txt".to_string(),
10850                    size: 50,
10851                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10852                    is_prefix: false,
10853                    storage_class: "STANDARD".to_string(),
10854                },
10855                S3Object {
10856                    key: "folder/nested2.txt".to_string(),
10857                    size: 75,
10858                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10859                    is_prefix: false,
10860                    storage_class: "STANDARD".to_string(),
10861                },
10862            ],
10863        );
10864        assert_eq!(app.calculate_total_bucket_rows(), 7); // 2 buckets + 3 objects + 2 nested
10865    }
10866
10867    #[test]
10868    fn test_calculate_total_object_rows() {
10869        let mut app = test_app();
10870        app.s3_state.current_bucket = Some("test-bucket".to_string());
10871
10872        // No objects
10873        assert_eq!(app.calculate_total_object_rows(), 0);
10874
10875        // 2 objects, no expansion
10876        app.s3_state.objects = vec![
10877            S3Object {
10878                key: "file1.txt".to_string(),
10879                size: 100,
10880                last_modified: "2024-01-01T00:00:00Z".to_string(),
10881                is_prefix: false,
10882                storage_class: "STANDARD".to_string(),
10883            },
10884            S3Object {
10885                key: "folder/".to_string(),
10886                size: 0,
10887                last_modified: "2024-01-01T00:00:00Z".to_string(),
10888                is_prefix: true,
10889                storage_class: String::new(),
10890            },
10891        ];
10892        assert_eq!(app.calculate_total_object_rows(), 2);
10893
10894        // Expand folder/ with 2 items
10895        app.s3_state.expanded_prefixes.insert("folder/".to_string());
10896        app.s3_state.prefix_preview.insert(
10897            "folder/".to_string(),
10898            vec![
10899                S3Object {
10900                    key: "folder/file2.txt".to_string(),
10901                    size: 200,
10902                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10903                    is_prefix: false,
10904                    storage_class: "STANDARD".to_string(),
10905                },
10906                S3Object {
10907                    key: "folder/subfolder/".to_string(),
10908                    size: 0,
10909                    last_modified: "2024-01-01T00:00:00Z".to_string(),
10910                    is_prefix: true,
10911                    storage_class: String::new(),
10912                },
10913            ],
10914        );
10915        assert_eq!(app.calculate_total_object_rows(), 4); // 2 + 2 nested
10916
10917        // Expand subfolder/ with 1 item (3rd level)
10918        app.s3_state
10919            .expanded_prefixes
10920            .insert("folder/subfolder/".to_string());
10921        app.s3_state.prefix_preview.insert(
10922            "folder/subfolder/".to_string(),
10923            vec![S3Object {
10924                key: "folder/subfolder/deep.txt".to_string(),
10925                size: 50,
10926                last_modified: "2024-01-01T00:00:00Z".to_string(),
10927                is_prefix: false,
10928                storage_class: "STANDARD".to_string(),
10929            }],
10930        );
10931        assert_eq!(app.calculate_total_object_rows(), 5); // 2 + 2 nested + 1 deep
10932    }
10933
10934    #[test]
10935    fn test_s3_object_navigation_with_deep_nesting() {
10936        let mut app = test_app();
10937        app.current_service = Service::S3Buckets;
10938        app.service_selected = true;
10939        app.mode = Mode::Normal;
10940        app.s3_state.current_bucket = Some("test-bucket".to_string());
10941
10942        // Add folder structure: folder1/ -> folder2/ -> file.txt
10943        app.s3_state.objects = vec![S3Object {
10944            key: "folder1/".to_string(),
10945            size: 0,
10946            last_modified: "2024-01-01T00:00:00Z".to_string(),
10947            is_prefix: true,
10948            storage_class: String::new(),
10949        }];
10950
10951        // Expand folder1/
10952        app.s3_state
10953            .expanded_prefixes
10954            .insert("folder1/".to_string());
10955        app.s3_state.prefix_preview.insert(
10956            "folder1/".to_string(),
10957            vec![S3Object {
10958                key: "folder1/folder2/".to_string(),
10959                size: 0,
10960                last_modified: "2024-01-01T00:00:00Z".to_string(),
10961                is_prefix: true,
10962                storage_class: String::new(),
10963            }],
10964        );
10965
10966        // Expand folder2/
10967        app.s3_state
10968            .expanded_prefixes
10969            .insert("folder1/folder2/".to_string());
10970        app.s3_state.prefix_preview.insert(
10971            "folder1/folder2/".to_string(),
10972            vec![S3Object {
10973                key: "folder1/folder2/file.txt".to_string(),
10974                size: 100,
10975                last_modified: "2024-01-01T00:00:00Z".to_string(),
10976                is_prefix: false,
10977                storage_class: "STANDARD".to_string(),
10978            }],
10979        );
10980
10981        app.s3_state.selected_object = 0;
10982
10983        // Total: folder1/ (0) + folder2/ (1) + file.txt (2) = 3 rows
10984        app.handle_action(Action::NextItem);
10985        assert_eq!(app.s3_state.selected_object, 1); // folder2/
10986
10987        app.handle_action(Action::NextItem);
10988        assert_eq!(app.s3_state.selected_object, 2); // file.txt
10989
10990        // Should not go beyond
10991        app.handle_action(Action::NextItem);
10992        assert_eq!(app.s3_state.selected_object, 2);
10993    }
10994
10995    #[test]
10996    fn test_s3_expand_nested_folder_in_objects_view() {
10997        let mut app = test_app();
10998        app.current_service = Service::S3Buckets;
10999        app.service_selected = true;
11000        app.mode = Mode::Normal;
11001        app.s3_state.current_bucket = Some("test-bucket".to_string());
11002
11003        // Add parent folder
11004        app.s3_state.objects = vec![S3Object {
11005            key: "parent/".to_string(),
11006            size: 0,
11007            last_modified: "2024-01-01T00:00:00Z".to_string(),
11008            is_prefix: true,
11009            storage_class: String::new(),
11010        }];
11011
11012        // Expand parent
11013        app.s3_state.expanded_prefixes.insert("parent/".to_string());
11014        app.s3_state.prefix_preview.insert(
11015            "parent/".to_string(),
11016            vec![S3Object {
11017                key: "parent/child/".to_string(),
11018                size: 0,
11019                last_modified: "2024-01-01T00:00:00Z".to_string(),
11020                is_prefix: true,
11021                storage_class: String::new(),
11022            }],
11023        );
11024
11025        // Select the nested folder (index 1)
11026        app.s3_state.selected_object = 1;
11027
11028        // Expand it (simulate pressing Enter/Right)
11029        app.handle_action(Action::NextPane);
11030
11031        // Should be expanded now
11032        assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
11033        assert!(app.s3_state.buckets.loading); // Should trigger load
11034    }
11035
11036    #[test]
11037    fn test_s3_drill_into_nested_folder() {
11038        let mut app = test_app();
11039        app.current_service = Service::S3Buckets;
11040        app.service_selected = true;
11041        app.mode = Mode::Normal;
11042        app.s3_state.current_bucket = Some("test-bucket".to_string());
11043
11044        // Add parent folder
11045        app.s3_state.objects = vec![S3Object {
11046            key: "parent/".to_string(),
11047            size: 0,
11048            last_modified: "2024-01-01T00:00:00Z".to_string(),
11049            is_prefix: true,
11050            storage_class: String::new(),
11051        }];
11052
11053        // Expand parent
11054        app.s3_state.expanded_prefixes.insert("parent/".to_string());
11055        app.s3_state.prefix_preview.insert(
11056            "parent/".to_string(),
11057            vec![S3Object {
11058                key: "parent/child/".to_string(),
11059                size: 0,
11060                last_modified: "2024-01-01T00:00:00Z".to_string(),
11061                is_prefix: true,
11062                storage_class: String::new(),
11063            }],
11064        );
11065
11066        // Select the nested folder (index 1)
11067        app.s3_state.selected_object = 1;
11068
11069        // Drill into it (simulate pressing Enter)
11070        app.handle_action(Action::Select);
11071
11072        // Should navigate into the folder
11073        assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
11074        assert!(app.s3_state.buckets.loading); // Should trigger load
11075    }
11076
11077    #[test]
11078    fn test_s3_esc_pops_navigation_stack() {
11079        let mut app = test_app();
11080        app.current_service = Service::S3Buckets;
11081        app.s3_state.current_bucket = Some("test-bucket".to_string());
11082        app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
11083
11084        // Press Esc - should pop from stack
11085        app.handle_action(Action::GoBack);
11086        assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
11087        assert!(app.s3_state.buckets.loading);
11088
11089        // Press Esc again - should pop to bucket root
11090        app.s3_state.buckets.loading = false;
11091        app.handle_action(Action::GoBack);
11092        assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
11093        assert!(app.s3_state.buckets.loading);
11094
11095        // Press Esc again - should exit bucket
11096        app.s3_state.buckets.loading = false;
11097        app.handle_action(Action::GoBack);
11098        assert_eq!(app.s3_state.current_bucket, None);
11099    }
11100
11101    #[test]
11102    fn test_s3_esc_from_bucket_root_exits() {
11103        let mut app = test_app();
11104        app.current_service = Service::S3Buckets;
11105        app.s3_state.current_bucket = Some("test-bucket".to_string());
11106        app.s3_state.prefix_stack = vec![];
11107
11108        // Press Esc from bucket root - should exit bucket
11109        app.handle_action(Action::GoBack);
11110        assert_eq!(app.s3_state.current_bucket, None);
11111        assert_eq!(app.s3_state.objects.len(), 0);
11112    }
11113
11114    #[test]
11115    fn test_s3_drill_into_nested_prefix_from_bucket_list() {
11116        let mut app = test_app();
11117        app.current_service = Service::S3Buckets;
11118        app.service_selected = true;
11119        app.mode = Mode::Normal;
11120
11121        // Setup bucket with nested preview
11122        app.s3_state.buckets.items = vec![S3Bucket {
11123            name: "test-bucket".to_string(),
11124            region: "us-east-1".to_string(),
11125            creation_date: "2024-01-01".to_string(),
11126        }];
11127
11128        // Expand bucket to show first-level prefix
11129        app.s3_state
11130            .expanded_prefixes
11131            .insert("test-bucket".to_string());
11132        app.s3_state.bucket_preview.insert(
11133            "test-bucket".to_string(),
11134            vec![S3Object {
11135                key: "parent/".to_string(),
11136                size: 0,
11137                last_modified: "2024-01-01".to_string(),
11138                is_prefix: true,
11139                storage_class: String::new(),
11140            }],
11141        );
11142
11143        // Expand parent to show nested prefix
11144        app.s3_state.expanded_prefixes.insert("parent/".to_string());
11145        app.s3_state.prefix_preview.insert(
11146            "parent/".to_string(),
11147            vec![S3Object {
11148                key: "parent/child/".to_string(),
11149                size: 0,
11150                last_modified: "2024-01-01".to_string(),
11151                is_prefix: true,
11152                storage_class: String::new(),
11153            }],
11154        );
11155
11156        // Select nested prefix (row 2: bucket, parent, nested)
11157        app.s3_state.selected_row = 2;
11158
11159        // Drill into nested prefix
11160        app.handle_action(Action::Select);
11161
11162        // Should have both parent and child in stack
11163        assert_eq!(
11164            app.s3_state.prefix_stack,
11165            vec!["parent/".to_string(), "parent/child/".to_string()]
11166        );
11167        assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
11168        assert!(app.s3_state.buckets.loading);
11169
11170        // Now press Esc - should go back to parent
11171        app.s3_state.buckets.loading = false;
11172        app.handle_action(Action::GoBack);
11173        assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
11174        assert!(app.s3_state.buckets.loading);
11175
11176        // Press Esc again - should go to bucket root
11177        app.s3_state.buckets.loading = false;
11178        app.handle_action(Action::GoBack);
11179        assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
11180        assert!(app.s3_state.buckets.loading);
11181
11182        // Press Esc again - should exit bucket
11183        app.s3_state.buckets.loading = false;
11184        app.handle_action(Action::GoBack);
11185        assert_eq!(app.s3_state.current_bucket, None);
11186    }
11187
11188    #[test]
11189    fn test_region_picker_fuzzy_filter() {
11190        let mut app = test_app();
11191        app.region_latencies.insert("us-east-1".to_string(), 10);
11192        app.region_filter = "vir".to_string();
11193        let filtered = app.get_filtered_regions();
11194        assert!(filtered.iter().any(|r| r.code == "us-east-1"));
11195    }
11196
11197    #[test]
11198    fn test_profile_picker_loads_profiles() {
11199        let profiles = App::load_aws_profiles();
11200        // Should at least have default profile or be empty if no config
11201        assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
11202    }
11203
11204    #[test]
11205    fn test_profile_with_region_uses_it() {
11206        let mut app = test_app_no_region();
11207        app.available_profiles = vec![AwsProfile {
11208            name: "test-profile".to_string(),
11209            region: Some("eu-west-1".to_string()),
11210            account: Some("123456789".to_string()),
11211            role_arn: None,
11212            source_profile: None,
11213        }];
11214        app.profile_picker_selected = 0;
11215        app.mode = Mode::ProfilePicker;
11216
11217        // Simulate selecting the profile
11218        let filtered = app.get_filtered_profiles();
11219        if let Some(profile) = filtered.first() {
11220            let profile_name = profile.name.clone();
11221            let profile_region = profile.region.clone();
11222
11223            app.profile = profile_name;
11224            if let Some(region) = profile_region {
11225                app.region = region;
11226            }
11227        }
11228
11229        assert_eq!(app.profile, "test-profile");
11230        assert_eq!(app.region, "eu-west-1");
11231    }
11232
11233    #[test]
11234    fn test_profile_without_region_keeps_unknown() {
11235        let mut app = test_app_no_region();
11236        let initial_region = app.region.clone();
11237
11238        app.available_profiles = vec![AwsProfile {
11239            name: "test-profile".to_string(),
11240            region: None,
11241            account: None,
11242            role_arn: None,
11243            source_profile: None,
11244        }];
11245        app.profile_picker_selected = 0;
11246        app.mode = Mode::ProfilePicker;
11247
11248        let filtered = app.get_filtered_profiles();
11249        if let Some(profile) = filtered.first() {
11250            let profile_name = profile.name.clone();
11251            let profile_region = profile.region.clone();
11252
11253            app.profile = profile_name;
11254            if let Some(region) = profile_region {
11255                app.region = region;
11256            }
11257        }
11258
11259        assert_eq!(app.profile, "test-profile");
11260        assert_eq!(app.region, initial_region); // Should keep initial region
11261    }
11262
11263    #[test]
11264    fn test_region_selection_closes_all_tabs() {
11265        let mut app = test_app();
11266
11267        // Add some tabs
11268        app.tabs.push(Tab {
11269            service: Service::CloudWatchLogGroups,
11270            title: "CloudWatch".to_string(),
11271            breadcrumb: "CloudWatch".to_string(),
11272        });
11273        app.tabs.push(Tab {
11274            service: Service::S3Buckets,
11275            title: "S3".to_string(),
11276            breadcrumb: "S3".to_string(),
11277        });
11278        app.service_selected = true;
11279        app.current_tab = 1;
11280
11281        // Add latency for region
11282        app.region_latencies.insert("eu-west-1".to_string(), 50);
11283
11284        // Simulate selecting a different region
11285        app.mode = Mode::RegionPicker;
11286        app.region_picker_selected = 0;
11287
11288        let filtered = app.get_filtered_regions();
11289        if let Some(region) = filtered.first() {
11290            app.region = region.code.to_string();
11291            app.tabs.clear();
11292            app.current_tab = 0;
11293            app.service_selected = false;
11294            app.mode = Mode::Normal;
11295        }
11296
11297        assert_eq!(app.tabs.len(), 0);
11298        assert_eq!(app.current_tab, 0);
11299        assert!(!app.service_selected);
11300        assert_eq!(app.region, "eu-west-1");
11301    }
11302
11303    #[test]
11304    fn test_region_picker_can_be_closed_without_selection() {
11305        let mut app = test_app();
11306        let initial_region = app.region.clone();
11307
11308        app.mode = Mode::RegionPicker;
11309
11310        // Close without selecting (Esc)
11311        app.mode = Mode::Normal;
11312
11313        // Region should not change
11314        assert_eq!(app.region, initial_region);
11315    }
11316
11317    #[test]
11318    fn test_session_filter_works() {
11319        let mut app = test_app();
11320
11321        app.sessions = vec![
11322            Session {
11323                id: "1".to_string(),
11324                timestamp: "2024-01-01".to_string(),
11325                profile: "prod-profile".to_string(),
11326                region: "us-east-1".to_string(),
11327                account_id: "123456789".to_string(),
11328                role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
11329                tabs: vec![],
11330            },
11331            Session {
11332                id: "2".to_string(),
11333                timestamp: "2024-01-02".to_string(),
11334                profile: "dev-profile".to_string(),
11335                region: "eu-west-1".to_string(),
11336                account_id: "987654321".to_string(),
11337                role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
11338                tabs: vec![],
11339            },
11340        ];
11341
11342        // Filter by profile
11343        app.session_filter = "prod".to_string();
11344        let filtered = app.get_filtered_sessions();
11345        assert_eq!(filtered.len(), 1);
11346        assert_eq!(filtered[0].profile, "prod-profile");
11347
11348        // Filter by region
11349        app.session_filter = "eu".to_string();
11350        let filtered = app.get_filtered_sessions();
11351        assert_eq!(filtered.len(), 1);
11352        assert_eq!(filtered[0].region, "eu-west-1");
11353
11354        // No filter
11355        app.session_filter.clear();
11356        let filtered = app.get_filtered_sessions();
11357        assert_eq!(filtered.len(), 2);
11358    }
11359
11360    #[test]
11361    fn test_profile_picker_shows_account() {
11362        let mut app = test_app_no_region();
11363        app.available_profiles = vec![AwsProfile {
11364            name: "test-profile".to_string(),
11365            region: Some("us-east-1".to_string()),
11366            account: Some("123456789".to_string()),
11367            role_arn: None,
11368            source_profile: None,
11369        }];
11370
11371        let filtered = app.get_filtered_profiles();
11372        assert_eq!(filtered.len(), 1);
11373        assert_eq!(filtered[0].account, Some("123456789".to_string()));
11374    }
11375
11376    #[test]
11377    fn test_profile_without_account() {
11378        let mut app = test_app_no_region();
11379        app.available_profiles = vec![AwsProfile {
11380            name: "test-profile".to_string(),
11381            region: Some("us-east-1".to_string()),
11382            account: None,
11383            role_arn: None,
11384            source_profile: None,
11385        }];
11386
11387        let filtered = app.get_filtered_profiles();
11388        assert_eq!(filtered.len(), 1);
11389        assert_eq!(filtered[0].account, None);
11390    }
11391
11392    #[test]
11393    fn test_profile_with_all_fields() {
11394        let mut app = test_app_no_region();
11395        app.available_profiles = vec![AwsProfile {
11396            name: "prod-profile".to_string(),
11397            region: Some("us-west-2".to_string()),
11398            account: Some("123456789".to_string()),
11399            role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
11400            source_profile: Some("base-profile".to_string()),
11401        }];
11402
11403        let filtered = app.get_filtered_profiles();
11404        assert_eq!(filtered.len(), 1);
11405        assert_eq!(filtered[0].name, "prod-profile");
11406        assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
11407        assert_eq!(filtered[0].account, Some("123456789".to_string()));
11408        assert_eq!(
11409            filtered[0].role_arn,
11410            Some("arn:aws:iam::123456789:role/AdminRole".to_string())
11411        );
11412        assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
11413    }
11414
11415    #[test]
11416    fn test_profile_filter_by_source_profile() {
11417        let mut app = test_app_no_region();
11418        app.available_profiles = vec![
11419            AwsProfile {
11420                name: "profile1".to_string(),
11421                region: None,
11422                account: None,
11423                role_arn: None,
11424                source_profile: Some("base".to_string()),
11425            },
11426            AwsProfile {
11427                name: "profile2".to_string(),
11428                region: None,
11429                account: None,
11430                role_arn: None,
11431                source_profile: Some("other".to_string()),
11432            },
11433        ];
11434
11435        app.profile_filter = "base".to_string();
11436        let filtered = app.get_filtered_profiles();
11437        assert_eq!(filtered.len(), 1);
11438        assert_eq!(filtered[0].name, "profile1");
11439    }
11440
11441    #[test]
11442    fn test_profile_filter_by_role() {
11443        let mut app = test_app_no_region();
11444        app.available_profiles = vec![
11445            AwsProfile {
11446                name: "admin-profile".to_string(),
11447                region: None,
11448                account: None,
11449                role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
11450                source_profile: None,
11451            },
11452            AwsProfile {
11453                name: "dev-profile".to_string(),
11454                region: None,
11455                account: None,
11456                role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
11457                source_profile: None,
11458            },
11459        ];
11460
11461        app.profile_filter = "Admin".to_string();
11462        let filtered = app.get_filtered_profiles();
11463        assert_eq!(filtered.len(), 1);
11464        assert_eq!(filtered[0].name, "admin-profile");
11465    }
11466
11467    #[test]
11468    fn test_profiles_sorted_by_name() {
11469        let mut app = test_app_no_region();
11470        app.available_profiles = vec![
11471            AwsProfile {
11472                name: "zebra-profile".to_string(),
11473                region: None,
11474                account: None,
11475                role_arn: None,
11476                source_profile: None,
11477            },
11478            AwsProfile {
11479                name: "alpha-profile".to_string(),
11480                region: None,
11481                account: None,
11482                role_arn: None,
11483                source_profile: None,
11484            },
11485            AwsProfile {
11486                name: "beta-profile".to_string(),
11487                region: None,
11488                account: None,
11489                role_arn: None,
11490                source_profile: None,
11491            },
11492        ];
11493
11494        let filtered = app.get_filtered_profiles();
11495        assert_eq!(filtered.len(), 3);
11496        assert_eq!(filtered[0].name, "alpha-profile");
11497        assert_eq!(filtered[1].name, "beta-profile");
11498        assert_eq!(filtered[2].name, "zebra-profile");
11499    }
11500
11501    #[test]
11502    fn test_profile_with_role_arn() {
11503        let mut app = test_app_no_region();
11504        app.available_profiles = vec![AwsProfile {
11505            name: "role-profile".to_string(),
11506            region: Some("us-east-1".to_string()),
11507            account: Some("123456789".to_string()),
11508            role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
11509            source_profile: None,
11510        }];
11511
11512        let filtered = app.get_filtered_profiles();
11513        assert_eq!(filtered.len(), 1);
11514        assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
11515    }
11516
11517    #[test]
11518    fn test_profile_with_user_arn() {
11519        let mut app = test_app_no_region();
11520        app.available_profiles = vec![AwsProfile {
11521            name: "user-profile".to_string(),
11522            region: Some("us-east-1".to_string()),
11523            account: Some("123456789".to_string()),
11524            role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
11525            source_profile: None,
11526        }];
11527
11528        let filtered = app.get_filtered_profiles();
11529        assert_eq!(filtered.len(), 1);
11530        assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
11531    }
11532
11533    #[test]
11534    fn test_filtered_profiles_also_sorted() {
11535        let mut app = test_app_no_region();
11536        app.available_profiles = vec![
11537            AwsProfile {
11538                name: "prod-zebra".to_string(),
11539                region: Some("us-east-1".to_string()),
11540                account: None,
11541                role_arn: None,
11542                source_profile: None,
11543            },
11544            AwsProfile {
11545                name: "prod-alpha".to_string(),
11546                region: Some("us-east-1".to_string()),
11547                account: None,
11548                role_arn: None,
11549                source_profile: None,
11550            },
11551            AwsProfile {
11552                name: "dev-profile".to_string(),
11553                region: Some("us-west-2".to_string()),
11554                account: None,
11555                role_arn: None,
11556                source_profile: None,
11557            },
11558        ];
11559
11560        app.profile_filter = "prod".to_string();
11561        let filtered = app.get_filtered_profiles();
11562        assert_eq!(filtered.len(), 2);
11563        assert_eq!(filtered[0].name, "prod-alpha");
11564        assert_eq!(filtered[1].name, "prod-zebra");
11565    }
11566
11567    #[test]
11568    fn test_profile_picker_has_all_columns() {
11569        let mut app = test_app_no_region();
11570        app.available_profiles = vec![AwsProfile {
11571            name: "test".to_string(),
11572            region: Some("us-east-1".to_string()),
11573            account: Some("123456789".to_string()),
11574            role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
11575            source_profile: Some("base".to_string()),
11576        }];
11577
11578        let filtered = app.get_filtered_profiles();
11579        assert_eq!(filtered.len(), 1);
11580        assert!(filtered[0].name == "test");
11581        assert!(filtered[0].region.is_some());
11582        assert!(filtered[0].account.is_some());
11583        assert!(filtered[0].role_arn.is_some());
11584        assert!(filtered[0].source_profile.is_some());
11585    }
11586
11587    #[test]
11588    fn test_session_picker_shows_tab_count() {
11589        let mut app = test_app_no_region();
11590        app.sessions = vec![Session {
11591            id: "1".to_string(),
11592            timestamp: "2024-01-01".to_string(),
11593            profile: "test".to_string(),
11594            region: "us-east-1".to_string(),
11595            account_id: "123".to_string(),
11596            role_arn: String::new(),
11597            tabs: vec![
11598                SessionTab {
11599                    service: "CloudWatch".to_string(),
11600                    title: "Logs".to_string(),
11601                    breadcrumb: String::new(),
11602                    filter: None,
11603                    selected_item: None,
11604                },
11605                SessionTab {
11606                    service: "S3".to_string(),
11607                    title: "Buckets".to_string(),
11608                    breadcrumb: String::new(),
11609                    filter: None,
11610                    selected_item: None,
11611                },
11612            ],
11613        }];
11614
11615        let filtered = app.get_filtered_sessions();
11616        assert_eq!(filtered.len(), 1);
11617        assert_eq!(filtered[0].tabs.len(), 2);
11618    }
11619
11620    #[test]
11621    fn test_start_background_data_fetch_loads_profiles() {
11622        let mut app = test_app_no_region();
11623        assert!(app.available_profiles.is_empty());
11624
11625        // Load profiles synchronously
11626        app.available_profiles = App::load_aws_profiles();
11627
11628        // Profiles should be loaded
11629        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
11630    }
11631
11632    #[test]
11633    fn test_refresh_in_profile_picker() {
11634        let mut app = test_app_no_region();
11635        app.mode = Mode::ProfilePicker;
11636        app.available_profiles = vec![AwsProfile {
11637            name: "test".to_string(),
11638            region: None,
11639            account: None,
11640            role_arn: None,
11641            source_profile: None,
11642        }];
11643
11644        app.handle_action(Action::Refresh);
11645
11646        // Should set loading state
11647        assert!(app.log_groups_state.loading);
11648        assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
11649    }
11650
11651    #[test]
11652    fn test_refresh_sets_loading_for_profile_picker() {
11653        let mut app = test_app_no_region();
11654        app.mode = Mode::ProfilePicker;
11655
11656        assert!(!app.log_groups_state.loading);
11657
11658        app.handle_action(Action::Refresh);
11659
11660        assert!(app.log_groups_state.loading);
11661    }
11662
11663    #[test]
11664    fn test_profiles_loaded_on_demand() {
11665        let mut app = test_app_no_region();
11666
11667        // Profiles not loaded by default
11668        assert!(app.available_profiles.is_empty());
11669
11670        // Load on demand
11671        app.available_profiles = App::load_aws_profiles();
11672
11673        // Now loaded
11674        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
11675    }
11676
11677    #[test]
11678    fn test_profile_accounts_not_fetched_automatically() {
11679        let mut app = test_app_no_region();
11680        app.available_profiles = App::load_aws_profiles();
11681
11682        // Accounts should not be populated automatically
11683        for profile in &app.available_profiles {
11684            // Account may or may not be set depending on what's in config
11685            // But we're not fetching them automatically
11686            assert!(profile.account.is_none() || profile.account.is_some());
11687        }
11688    }
11689
11690    #[test]
11691    fn test_ctrl_r_triggers_account_fetch() {
11692        let mut app = test_app_no_region();
11693        app.mode = Mode::ProfilePicker;
11694        app.available_profiles = vec![AwsProfile {
11695            name: "test".to_string(),
11696            region: Some("us-east-1".to_string()),
11697            account: None,
11698            role_arn: None,
11699            source_profile: None,
11700        }];
11701
11702        // Before refresh, account is None
11703        assert!(app.available_profiles[0].account.is_none());
11704
11705        // Trigger refresh
11706        app.handle_action(Action::Refresh);
11707
11708        // Should set loading state (actual fetch happens in main.rs event loop)
11709        assert!(app.log_groups_state.loading);
11710    }
11711
11712    #[test]
11713    fn test_refresh_in_region_picker() {
11714        let mut app = test_app_no_region();
11715        app.mode = Mode::RegionPicker;
11716
11717        let initial_latencies = app.region_latencies.len();
11718        app.handle_action(Action::Refresh);
11719
11720        // Latencies should be cleared and remeasured
11721        assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
11722    }
11723
11724    #[test]
11725    fn test_refresh_in_session_picker() {
11726        let mut app = test_app_no_region();
11727        app.mode = Mode::SessionPicker;
11728        app.sessions = vec![];
11729
11730        app.handle_action(Action::Refresh);
11731
11732        // Sessions should be reloaded (may be empty if no saved sessions)
11733        assert!(app.sessions.is_empty() || !app.sessions.is_empty());
11734    }
11735
11736    #[test]
11737    fn test_session_picker_selection() {
11738        let mut app = test_app();
11739
11740        app.sessions = vec![Session {
11741            id: "1".to_string(),
11742            timestamp: "2024-01-01".to_string(),
11743            profile: "prod-profile".to_string(),
11744            region: "us-west-2".to_string(),
11745            account_id: "123456789".to_string(),
11746            role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
11747            tabs: vec![SessionTab {
11748                service: "CloudWatchLogGroups".to_string(),
11749                title: "Log Groups".to_string(),
11750                breadcrumb: "CloudWatch › Log Groups".to_string(),
11751                filter: Some("test".to_string()),
11752                selected_item: None,
11753            }],
11754        }];
11755
11756        app.mode = Mode::SessionPicker;
11757        app.session_picker_selected = 0;
11758
11759        // Simulate selecting the session
11760        app.handle_action(Action::Select);
11761
11762        assert_eq!(app.mode, Mode::Normal);
11763        assert_eq!(app.profile, "prod-profile");
11764        assert_eq!(app.region, "us-west-2");
11765        assert_eq!(app.config.account_id, "123456789");
11766        assert_eq!(app.tabs.len(), 1);
11767        assert_eq!(app.tabs[0].title, "Log Groups");
11768    }
11769
11770    #[test]
11771    fn test_save_session_creates_session() {
11772        let mut app =
11773            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
11774        app.config.account_id = "123456789".to_string();
11775        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
11776
11777        app.tabs.push(Tab {
11778            service: Service::CloudWatchLogGroups,
11779            title: "Log Groups".to_string(),
11780            breadcrumb: "CloudWatch › Log Groups".to_string(),
11781        });
11782
11783        app.save_current_session();
11784
11785        assert!(app.current_session.is_some());
11786        let session = app.current_session.clone().unwrap();
11787        assert_eq!(session.profile, "test-profile");
11788        assert_eq!(session.region, "us-east-1");
11789        assert_eq!(session.account_id, "123456789");
11790        assert_eq!(session.tabs.len(), 1);
11791
11792        // Cleanup
11793        let _ = session.delete();
11794    }
11795
11796    #[test]
11797    fn test_save_session_updates_existing() {
11798        let mut app =
11799            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
11800        app.config.account_id = "123456789".to_string();
11801        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
11802
11803        app.current_session = Some(Session {
11804            id: "existing".to_string(),
11805            timestamp: "2024-01-01".to_string(),
11806            profile: "test-profile".to_string(),
11807            region: "us-east-1".to_string(),
11808            account_id: "123456789".to_string(),
11809            role_arn: "arn:aws:iam::123456789:role/test".to_string(),
11810            tabs: vec![],
11811        });
11812
11813        app.tabs.push(Tab {
11814            service: Service::CloudWatchLogGroups,
11815            title: "Log Groups".to_string(),
11816            breadcrumb: "CloudWatch › Log Groups".to_string(),
11817        });
11818
11819        app.save_current_session();
11820
11821        let session = app.current_session.clone().unwrap();
11822        assert_eq!(session.id, "existing");
11823        assert_eq!(session.tabs.len(), 1);
11824
11825        // Cleanup
11826        let _ = session.delete();
11827    }
11828
11829    #[test]
11830    fn test_save_session_skips_empty_tabs() {
11831        let mut app =
11832            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
11833        app.config.account_id = "123456789".to_string();
11834
11835        app.save_current_session();
11836
11837        assert!(app.current_session.is_none());
11838    }
11839
11840    #[test]
11841    fn test_save_session_deletes_when_tabs_closed() {
11842        let mut app =
11843            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
11844        app.config.account_id = "123456789".to_string();
11845        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
11846
11847        // Create a session with tabs
11848        app.current_session = Some(Session {
11849            id: "test_delete".to_string(),
11850            timestamp: "2024-01-01 10:00:00 UTC".to_string(),
11851            profile: "test-profile".to_string(),
11852            region: "us-east-1".to_string(),
11853            account_id: "123456789".to_string(),
11854            role_arn: "arn:aws:iam::123456789:role/test".to_string(),
11855            tabs: vec![],
11856        });
11857
11858        // Save with no tabs should delete session
11859        app.save_current_session();
11860
11861        assert!(app.current_session.is_none());
11862    }
11863
11864    #[test]
11865    fn test_closing_all_tabs_deletes_session() {
11866        let mut app =
11867            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
11868        app.config.account_id = "123456789".to_string();
11869        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
11870
11871        // Add a tab
11872        app.tabs.push(Tab {
11873            service: Service::CloudWatchLogGroups,
11874            title: "Log Groups".to_string(),
11875            breadcrumb: "CloudWatch › Log Groups".to_string(),
11876        });
11877
11878        // Create session
11879        app.save_current_session();
11880        assert!(app.current_session.is_some());
11881        let session_id = app.current_session.as_ref().unwrap().id.clone();
11882
11883        // Close all tabs
11884        app.tabs.clear();
11885
11886        // Save should delete session
11887        app.save_current_session();
11888        assert!(app.current_session.is_none());
11889
11890        // Cleanup - ensure session file is deleted
11891        let _ = Session::load(&session_id).map(|s| s.delete());
11892    }
11893
11894    #[test]
11895    fn test_credential_error_opens_profile_picker() {
11896        // Simulate what main.rs does on credential error
11897        let mut app = App::new_without_client("default".to_string(), None);
11898        let error_str = "Unable to load credentials from any source";
11899
11900        if error_str.contains("credentials") {
11901            app.available_profiles = App::load_aws_profiles();
11902            app.mode = Mode::ProfilePicker;
11903        }
11904
11905        assert_eq!(app.mode, Mode::ProfilePicker);
11906        // Should have loaded profiles
11907        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
11908    }
11909
11910    #[test]
11911    fn test_non_credential_error_shows_error_modal() {
11912        let mut app = App::new_without_client("default".to_string(), None);
11913        let error_str = "Network timeout";
11914
11915        if !error_str.contains("credentials") {
11916            app.error_message = Some(error_str.to_string());
11917            app.mode = Mode::ErrorModal;
11918        }
11919
11920        assert_eq!(app.mode, Mode::ErrorModal);
11921        assert!(app.error_message.is_some());
11922    }
11923
11924    #[tokio::test]
11925    async fn test_profile_selection_loads_credentials() {
11926        // Set a valid AWS profile if available
11927        std::env::set_var("AWS_PROFILE", "default");
11928
11929        // Try to create app with profile
11930        let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
11931
11932        if let Ok(app) = result {
11933            // If credentials are available, verify they're loaded
11934            assert!(!app.config.account_id.is_empty());
11935            assert!(!app.config.role_arn.is_empty());
11936            assert_eq!(app.profile, "default");
11937            assert_eq!(app.config.region, "us-east-1");
11938        }
11939        // If no credentials, test passes (can't test without real AWS creds)
11940    }
11941
11942    #[test]
11943    fn test_new_app_shows_service_picker_with_no_tabs() {
11944        let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
11945
11946        // Should start with no service selected
11947        assert!(!app.service_selected);
11948        // Should be in ServicePicker mode (service picker)
11949        assert_eq!(app.mode, Mode::ServicePicker);
11950        // Should have no tabs
11951        assert!(app.tabs.is_empty());
11952    }
11953
11954    #[tokio::test]
11955    async fn test_aws_profile_env_var_read_before_config_load() {
11956        // This test verifies the bug: AWS_PROFILE should be read and used
11957        std::env::set_var("AWS_PROFILE", "test-profile");
11958
11959        // Simulate what happens in App::new
11960        let profile_name = None
11961            .or_else(|| std::env::var("AWS_PROFILE").ok())
11962            .unwrap_or_else(|| "default".to_string());
11963
11964        // Should have read test-profile from env
11965        assert_eq!(profile_name, "test-profile");
11966
11967        // Now set it (redundant but that's what the code does)
11968        std::env::set_var("AWS_PROFILE", &profile_name);
11969
11970        // Verify it's still set
11971        assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
11972
11973        std::env::remove_var("AWS_PROFILE");
11974    }
11975
11976    #[test]
11977    fn test_next_preferences_cloudformation() {
11978        let mut app = test_app();
11979        app.current_service = Service::CloudFormationStacks;
11980        app.mode = Mode::ColumnSelector;
11981        app.column_selector_index = 0;
11982
11983        // Should jump to PageSize section
11984        let page_size_idx = app.cfn_column_ids.len() + 2;
11985        app.handle_action(Action::NextPreferences);
11986        assert_eq!(app.column_selector_index, page_size_idx);
11987
11988        // Should wrap back to Columns
11989        app.handle_action(Action::NextPreferences);
11990        assert_eq!(app.column_selector_index, 0);
11991    }
11992
11993    #[test]
11994    fn test_s3_preferences_tab_cycling() {
11995        let mut app = test_app();
11996        app.current_service = Service::S3Buckets;
11997        app.mode = Mode::ColumnSelector;
11998        app.column_selector_index = 0;
11999
12000        let page_size_idx = app.s3_bucket_column_ids.len() + 2;
12001
12002        // Tab should jump to PageSize
12003        app.handle_action(Action::NextPreferences);
12004        assert_eq!(app.column_selector_index, page_size_idx);
12005
12006        // Tab should wrap back to Columns
12007        app.handle_action(Action::NextPreferences);
12008        assert_eq!(app.column_selector_index, 0);
12009
12010        // Shift+Tab should jump to PageSize
12011        app.handle_action(Action::PrevPreferences);
12012        assert_eq!(app.column_selector_index, page_size_idx);
12013
12014        // Shift+Tab should wrap back to Columns
12015        app.handle_action(Action::PrevPreferences);
12016        assert_eq!(app.column_selector_index, 0);
12017    }
12018
12019    #[test]
12020    fn test_s3_filter_resets_selection() {
12021        let mut app = test_app();
12022        app.current_service = Service::S3Buckets;
12023        app.service_selected = true;
12024
12025        // Add some buckets
12026        app.s3_state.buckets.items = vec![
12027            S3Bucket {
12028                name: "bucket-1".to_string(),
12029                region: "us-east-1".to_string(),
12030                creation_date: "2023-01-01".to_string(),
12031            },
12032            S3Bucket {
12033                name: "bucket-2".to_string(),
12034                region: "us-east-1".to_string(),
12035                creation_date: "2023-01-02".to_string(),
12036            },
12037            S3Bucket {
12038                name: "other-bucket".to_string(),
12039                region: "us-east-1".to_string(),
12040                creation_date: "2023-01-03".to_string(),
12041            },
12042        ];
12043
12044        // Navigate to second bucket
12045        app.s3_state.selected_row = 1;
12046        app.s3_state.bucket_scroll_offset = 1;
12047
12048        // Apply filter
12049        app.mode = Mode::FilterInput;
12050        app.apply_filter_operation(|f| f.push_str("bucket-"));
12051
12052        // Selection should be reset
12053        assert_eq!(app.s3_state.selected_row, 0);
12054        assert_eq!(app.s3_state.bucket_scroll_offset, 0);
12055        assert_eq!(app.s3_state.buckets.filter, "bucket-");
12056    }
12057
12058    #[test]
12059    fn test_s3_navigation_respects_filter() {
12060        let mut app = test_app();
12061        app.current_service = Service::S3Buckets;
12062        app.service_selected = true;
12063        app.mode = Mode::Normal;
12064
12065        // Add buckets
12066        app.s3_state.buckets.items = vec![
12067            S3Bucket {
12068                name: "prod-bucket".to_string(),
12069                region: "us-east-1".to_string(),
12070                creation_date: "2023-01-01".to_string(),
12071            },
12072            S3Bucket {
12073                name: "dev-bucket".to_string(),
12074                region: "us-east-1".to_string(),
12075                creation_date: "2023-01-02".to_string(),
12076            },
12077            S3Bucket {
12078                name: "prod-logs".to_string(),
12079                region: "us-east-1".to_string(),
12080                creation_date: "2023-01-03".to_string(),
12081            },
12082        ];
12083
12084        // Filter to only "prod" buckets (2 results)
12085        app.s3_state.buckets.filter = "prod".to_string();
12086
12087        // Should start at 0
12088        assert_eq!(app.s3_state.selected_row, 0);
12089
12090        // Navigate down - should go to row 1 (prod-logs)
12091        app.handle_action(Action::NextItem);
12092        assert_eq!(app.s3_state.selected_row, 1);
12093
12094        // Navigate down again - should stay at 1 (max for 2 filtered results)
12095        app.handle_action(Action::NextItem);
12096        assert_eq!(app.s3_state.selected_row, 1);
12097
12098        // Navigate up - should go back to 0
12099        app.handle_action(Action::PrevItem);
12100        assert_eq!(app.s3_state.selected_row, 0);
12101    }
12102
12103    #[test]
12104    fn test_next_preferences_lambda_functions() {
12105        let mut app = test_app();
12106        app.current_service = Service::LambdaFunctions;
12107        app.mode = Mode::ColumnSelector;
12108        app.column_selector_index = 0;
12109
12110        let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
12111        app.handle_action(Action::NextPreferences);
12112        assert_eq!(app.column_selector_index, page_size_idx);
12113
12114        app.handle_action(Action::NextPreferences);
12115        assert_eq!(app.column_selector_index, 0);
12116    }
12117
12118    #[test]
12119    fn test_next_preferences_lambda_applications() {
12120        let mut app = test_app();
12121        app.current_service = Service::LambdaApplications;
12122        app.mode = Mode::ColumnSelector;
12123        app.column_selector_index = 0;
12124
12125        let page_size_idx = app.lambda_application_column_ids.len() + 2;
12126        app.handle_action(Action::NextPreferences);
12127        assert_eq!(app.column_selector_index, page_size_idx);
12128
12129        app.handle_action(Action::NextPreferences);
12130        assert_eq!(app.column_selector_index, 0);
12131    }
12132
12133    #[test]
12134    fn test_next_preferences_ecr_images() {
12135        let mut app = test_app();
12136        app.current_service = Service::EcrRepositories;
12137        app.ecr_state.current_repository = Some("test-repo".to_string());
12138        app.mode = Mode::ColumnSelector;
12139        app.column_selector_index = 0;
12140
12141        let page_size_idx = app.ecr_image_column_ids.len() + 2;
12142        app.handle_action(Action::NextPreferences);
12143        assert_eq!(app.column_selector_index, page_size_idx);
12144
12145        app.handle_action(Action::NextPreferences);
12146        assert_eq!(app.column_selector_index, 0);
12147    }
12148
12149    #[test]
12150    fn test_next_preferences_cloudwatch_log_groups() {
12151        let mut app = test_app();
12152        app.current_service = Service::CloudWatchLogGroups;
12153        app.view_mode = ViewMode::List;
12154        app.mode = Mode::ColumnSelector;
12155        app.column_selector_index = 0;
12156
12157        // Tab from Columns to PageSize
12158        let page_size_idx = app.cw_log_group_column_ids.len() + 2;
12159        app.handle_action(Action::NextPreferences);
12160        assert_eq!(app.column_selector_index, page_size_idx);
12161
12162        // Tab from PageSize back to Columns
12163        app.handle_action(Action::NextPreferences);
12164        assert_eq!(app.column_selector_index, 0);
12165
12166        // Shift+Tab from Columns to PageSize
12167        app.handle_action(Action::PrevPreferences);
12168        assert_eq!(app.column_selector_index, page_size_idx);
12169
12170        // Shift+Tab from PageSize back to Columns
12171        app.handle_action(Action::PrevPreferences);
12172        assert_eq!(app.column_selector_index, 0);
12173    }
12174
12175    #[test]
12176    fn test_next_preferences_cloudwatch_log_streams() {
12177        let mut app = test_app();
12178        app.current_service = Service::CloudWatchLogGroups;
12179        app.view_mode = ViewMode::Detail;
12180        app.mode = Mode::ColumnSelector;
12181        app.column_selector_index = 0;
12182
12183        // Tab from Columns to PageSize
12184        let page_size_idx = app.cw_log_stream_column_ids.len() + 2;
12185        app.handle_action(Action::NextPreferences);
12186        assert_eq!(app.column_selector_index, page_size_idx);
12187
12188        // Tab from PageSize back to Columns
12189        app.handle_action(Action::NextPreferences);
12190        assert_eq!(app.column_selector_index, 0);
12191    }
12192
12193    #[test]
12194    fn test_cloudformation_next_item() {
12195        let mut app = test_app();
12196        app.current_service = Service::CloudFormationStacks;
12197        app.service_selected = true;
12198        app.mode = Mode::Normal;
12199        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12200        app.cfn_state.table.items = vec![
12201            CfnStack {
12202                name: "stack1".to_string(),
12203                stack_id: "id1".to_string(),
12204                status: "CREATE_COMPLETE".to_string(),
12205                created_time: "2024-01-01".to_string(),
12206                updated_time: String::new(),
12207                deleted_time: String::new(),
12208                drift_status: String::new(),
12209                last_drift_check_time: String::new(),
12210                status_reason: String::new(),
12211                description: String::new(),
12212                detailed_status: String::new(),
12213                root_stack: String::new(),
12214                parent_stack: String::new(),
12215                termination_protection: false,
12216                iam_role: String::new(),
12217                tags: Vec::new(),
12218                stack_policy: String::new(),
12219                rollback_monitoring_time: String::new(),
12220                rollback_alarms: Vec::new(),
12221                notification_arns: Vec::new(),
12222            },
12223            CfnStack {
12224                name: "stack2".to_string(),
12225                stack_id: "id2".to_string(),
12226                status: "UPDATE_COMPLETE".to_string(),
12227                created_time: "2024-01-02".to_string(),
12228                updated_time: String::new(),
12229                deleted_time: String::new(),
12230                drift_status: String::new(),
12231                last_drift_check_time: String::new(),
12232                status_reason: String::new(),
12233                description: String::new(),
12234                detailed_status: String::new(),
12235                root_stack: String::new(),
12236                parent_stack: String::new(),
12237                termination_protection: false,
12238                iam_role: String::new(),
12239                tags: Vec::new(),
12240                stack_policy: String::new(),
12241                rollback_monitoring_time: String::new(),
12242                rollback_alarms: Vec::new(),
12243                notification_arns: Vec::new(),
12244            },
12245        ];
12246        app.cfn_state.table.reset();
12247
12248        app.handle_action(Action::NextItem);
12249        assert_eq!(app.cfn_state.table.selected, 1);
12250
12251        app.handle_action(Action::NextItem);
12252        assert_eq!(app.cfn_state.table.selected, 1); // At max
12253    }
12254
12255    #[test]
12256    fn test_cloudformation_prev_item() {
12257        let mut app = test_app();
12258        app.current_service = Service::CloudFormationStacks;
12259        app.service_selected = true;
12260        app.mode = Mode::Normal;
12261        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12262        app.cfn_state.table.items = vec![
12263            CfnStack {
12264                name: "stack1".to_string(),
12265                stack_id: "id1".to_string(),
12266                status: "CREATE_COMPLETE".to_string(),
12267                created_time: "2024-01-01".to_string(),
12268                updated_time: String::new(),
12269                deleted_time: String::new(),
12270                drift_status: String::new(),
12271                last_drift_check_time: String::new(),
12272                status_reason: String::new(),
12273                description: String::new(),
12274                detailed_status: String::new(),
12275                root_stack: String::new(),
12276                parent_stack: String::new(),
12277                termination_protection: false,
12278                iam_role: String::new(),
12279                tags: Vec::new(),
12280                stack_policy: String::new(),
12281                rollback_monitoring_time: String::new(),
12282                rollback_alarms: Vec::new(),
12283                notification_arns: Vec::new(),
12284            },
12285            CfnStack {
12286                name: "stack2".to_string(),
12287                stack_id: "id2".to_string(),
12288                status: "UPDATE_COMPLETE".to_string(),
12289                created_time: "2024-01-02".to_string(),
12290                updated_time: String::new(),
12291                deleted_time: String::new(),
12292                drift_status: String::new(),
12293                last_drift_check_time: String::new(),
12294                status_reason: String::new(),
12295                description: String::new(),
12296                detailed_status: String::new(),
12297                root_stack: String::new(),
12298                parent_stack: String::new(),
12299                termination_protection: false,
12300                iam_role: String::new(),
12301                tags: Vec::new(),
12302                stack_policy: String::new(),
12303                rollback_monitoring_time: String::new(),
12304                rollback_alarms: Vec::new(),
12305                notification_arns: Vec::new(),
12306            },
12307        ];
12308        app.cfn_state.table.selected = 1;
12309
12310        app.handle_action(Action::PrevItem);
12311        assert_eq!(app.cfn_state.table.selected, 0);
12312
12313        app.handle_action(Action::PrevItem);
12314        assert_eq!(app.cfn_state.table.selected, 0); // At min
12315    }
12316
12317    #[test]
12318    fn test_cloudformation_page_down() {
12319        let mut app = test_app();
12320        app.current_service = Service::CloudFormationStacks;
12321        app.service_selected = true;
12322        app.mode = Mode::Normal;
12323        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12324
12325        // Create 20 stacks
12326        for i in 0..20 {
12327            app.cfn_state.table.items.push(CfnStack {
12328                name: format!("stack{}", i),
12329                stack_id: format!("id{}", i),
12330                status: "CREATE_COMPLETE".to_string(),
12331                created_time: format!("2024-01-{:02}", i + 1),
12332                updated_time: String::new(),
12333                deleted_time: String::new(),
12334                drift_status: String::new(),
12335                last_drift_check_time: String::new(),
12336                status_reason: String::new(),
12337                description: String::new(),
12338                detailed_status: String::new(),
12339                root_stack: String::new(),
12340                parent_stack: String::new(),
12341                termination_protection: false,
12342                iam_role: String::new(),
12343                tags: Vec::new(),
12344                stack_policy: String::new(),
12345                rollback_monitoring_time: String::new(),
12346                rollback_alarms: Vec::new(),
12347                notification_arns: Vec::new(),
12348            });
12349        }
12350        app.cfn_state.table.reset();
12351
12352        app.handle_action(Action::PageDown);
12353        assert_eq!(app.cfn_state.table.selected, 10);
12354
12355        app.handle_action(Action::PageDown);
12356        assert_eq!(app.cfn_state.table.selected, 19); // Clamped to max
12357    }
12358
12359    #[test]
12360    fn test_cloudformation_page_up() {
12361        let mut app = test_app();
12362        app.current_service = Service::CloudFormationStacks;
12363        app.service_selected = true;
12364        app.mode = Mode::Normal;
12365        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12366
12367        // Create 20 stacks
12368        for i in 0..20 {
12369            app.cfn_state.table.items.push(CfnStack {
12370                name: format!("stack{}", i),
12371                stack_id: format!("id{}", i),
12372                status: "CREATE_COMPLETE".to_string(),
12373                created_time: format!("2024-01-{:02}", i + 1),
12374                updated_time: String::new(),
12375                deleted_time: String::new(),
12376                drift_status: String::new(),
12377                last_drift_check_time: String::new(),
12378                status_reason: String::new(),
12379                description: String::new(),
12380                detailed_status: String::new(),
12381                root_stack: String::new(),
12382                parent_stack: String::new(),
12383                termination_protection: false,
12384                iam_role: String::new(),
12385                tags: Vec::new(),
12386                stack_policy: String::new(),
12387                rollback_monitoring_time: String::new(),
12388                rollback_alarms: Vec::new(),
12389                notification_arns: Vec::new(),
12390            });
12391        }
12392        app.cfn_state.table.selected = 15;
12393
12394        app.handle_action(Action::PageUp);
12395        assert_eq!(app.cfn_state.table.selected, 5);
12396
12397        app.handle_action(Action::PageUp);
12398        assert_eq!(app.cfn_state.table.selected, 0); // Clamped to min
12399    }
12400
12401    #[test]
12402    fn test_cloudformation_filter_input() {
12403        let mut app = test_app();
12404        app.current_service = Service::CloudFormationStacks;
12405        app.service_selected = true;
12406        app.mode = Mode::Normal;
12407
12408        app.handle_action(Action::StartFilter);
12409        assert_eq!(app.mode, Mode::FilterInput);
12410
12411        // Directly set filter (character input is handled in event loop, not actions)
12412        app.cfn_state.table.filter = "test".to_string();
12413        assert_eq!(app.cfn_state.table.filter, "test");
12414    }
12415
12416    #[test]
12417    fn test_cloudformation_filter_applies() {
12418        let mut app = test_app();
12419        app.current_service = Service::CloudFormationStacks;
12420        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12421        app.cfn_state.table.items = vec![
12422            CfnStack {
12423                name: "prod-stack".to_string(),
12424                stack_id: "id1".to_string(),
12425                status: "CREATE_COMPLETE".to_string(),
12426                created_time: "2024-01-01".to_string(),
12427                updated_time: String::new(),
12428                deleted_time: String::new(),
12429                drift_status: String::new(),
12430                last_drift_check_time: String::new(),
12431                status_reason: String::new(),
12432                description: "Production stack".to_string(),
12433                detailed_status: String::new(),
12434                root_stack: String::new(),
12435                parent_stack: String::new(),
12436                termination_protection: false,
12437                iam_role: String::new(),
12438                tags: Vec::new(),
12439                stack_policy: String::new(),
12440                rollback_monitoring_time: String::new(),
12441                rollback_alarms: Vec::new(),
12442                notification_arns: Vec::new(),
12443            },
12444            CfnStack {
12445                name: "dev-stack".to_string(),
12446                stack_id: "id2".to_string(),
12447                status: "UPDATE_COMPLETE".to_string(),
12448                created_time: "2024-01-02".to_string(),
12449                updated_time: String::new(),
12450                deleted_time: String::new(),
12451                drift_status: String::new(),
12452                last_drift_check_time: String::new(),
12453                status_reason: String::new(),
12454                description: "Development stack".to_string(),
12455                detailed_status: String::new(),
12456                root_stack: String::new(),
12457                parent_stack: String::new(),
12458                termination_protection: false,
12459                iam_role: String::new(),
12460                tags: Vec::new(),
12461                stack_policy: String::new(),
12462                rollback_monitoring_time: String::new(),
12463                rollback_alarms: Vec::new(),
12464                notification_arns: Vec::new(),
12465            },
12466        ];
12467        app.cfn_state.table.filter = "prod".to_string();
12468
12469        let filtered = filtered_cloudformation_stacks(&app);
12470        assert_eq!(filtered.len(), 1);
12471        assert_eq!(filtered[0].name, "prod-stack");
12472    }
12473
12474    #[test]
12475    fn test_cloudformation_right_arrow_expands() {
12476        let mut app = test_app();
12477        app.current_service = Service::CloudFormationStacks;
12478        app.service_selected = true;
12479        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12480        app.cfn_state.table.items = vec![CfnStack {
12481            name: "test-stack".to_string(),
12482            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12483                .to_string(),
12484            status: "CREATE_COMPLETE".to_string(),
12485            created_time: "2024-01-01".to_string(),
12486            updated_time: String::new(),
12487            deleted_time: String::new(),
12488            drift_status: String::new(),
12489            last_drift_check_time: String::new(),
12490            status_reason: String::new(),
12491            description: "Test stack".to_string(),
12492            detailed_status: String::new(),
12493            root_stack: String::new(),
12494            parent_stack: String::new(),
12495            termination_protection: false,
12496            iam_role: String::new(),
12497            tags: Vec::new(),
12498            stack_policy: String::new(),
12499            rollback_monitoring_time: String::new(),
12500            rollback_alarms: Vec::new(),
12501            notification_arns: Vec::new(),
12502        }];
12503        app.cfn_state.table.reset();
12504
12505        assert_eq!(app.cfn_state.table.expanded_item, None);
12506
12507        app.handle_action(Action::NextPane);
12508        assert_eq!(app.cfn_state.table.expanded_item, Some(0));
12509    }
12510
12511    #[test]
12512    fn test_cloudformation_left_arrow_collapses() {
12513        let mut app = test_app();
12514        app.current_service = Service::CloudFormationStacks;
12515        app.service_selected = true;
12516        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12517        app.cfn_state.table.items = vec![CfnStack {
12518            name: "test-stack".to_string(),
12519            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12520                .to_string(),
12521            status: "CREATE_COMPLETE".to_string(),
12522            created_time: "2024-01-01".to_string(),
12523            updated_time: String::new(),
12524            deleted_time: String::new(),
12525            drift_status: String::new(),
12526            last_drift_check_time: String::new(),
12527            status_reason: String::new(),
12528            description: "Test stack".to_string(),
12529            detailed_status: String::new(),
12530            root_stack: String::new(),
12531            parent_stack: String::new(),
12532            termination_protection: false,
12533            iam_role: String::new(),
12534            tags: Vec::new(),
12535            stack_policy: String::new(),
12536            rollback_monitoring_time: String::new(),
12537            rollback_alarms: Vec::new(),
12538            notification_arns: Vec::new(),
12539        }];
12540        app.cfn_state.table.reset();
12541        app.cfn_state.table.expanded_item = Some(0);
12542
12543        app.handle_action(Action::PrevPane);
12544        assert_eq!(app.cfn_state.table.expanded_item, None);
12545    }
12546
12547    #[test]
12548    fn test_cloudformation_enter_drills_into_stack() {
12549        let mut app = test_app();
12550        app.current_service = Service::CloudFormationStacks;
12551        app.service_selected = true;
12552        app.mode = Mode::Normal;
12553        app.tabs = vec![Tab {
12554            service: Service::CloudFormationStacks,
12555            title: "CloudFormation › Stacks".to_string(),
12556            breadcrumb: "CloudFormation › Stacks".to_string(),
12557        }];
12558        app.current_tab = 0;
12559        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12560        app.cfn_state.table.items = vec![CfnStack {
12561            name: "test-stack".to_string(),
12562            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12563                .to_string(),
12564            status: "CREATE_COMPLETE".to_string(),
12565            created_time: "2024-01-01".to_string(),
12566            updated_time: String::new(),
12567            deleted_time: String::new(),
12568            drift_status: String::new(),
12569            last_drift_check_time: String::new(),
12570            status_reason: String::new(),
12571            description: "Test stack".to_string(),
12572            detailed_status: String::new(),
12573            root_stack: String::new(),
12574            parent_stack: String::new(),
12575            termination_protection: false,
12576            iam_role: String::new(),
12577            tags: Vec::new(),
12578            stack_policy: String::new(),
12579            rollback_monitoring_time: String::new(),
12580            rollback_alarms: Vec::new(),
12581            notification_arns: Vec::new(),
12582        }];
12583        app.cfn_state.table.reset();
12584
12585        // Verify filtering works
12586        let filtered = filtered_cloudformation_stacks(&app);
12587        assert_eq!(filtered.len(), 1);
12588        assert_eq!(filtered[0].name, "test-stack");
12589
12590        assert_eq!(app.cfn_state.current_stack, None);
12591
12592        // Enter drills into stack detail view
12593        app.handle_action(Action::Select);
12594        assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
12595    }
12596
12597    #[test]
12598    fn test_cloudformation_copy_to_clipboard() {
12599        let mut app = test_app();
12600        app.current_service = Service::CloudFormationStacks;
12601        app.service_selected = true;
12602        app.mode = Mode::Normal;
12603        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12604        app.cfn_state.table.items = vec![
12605            CfnStack {
12606                name: "stack1".to_string(),
12607                stack_id: "id1".to_string(),
12608                status: "CREATE_COMPLETE".to_string(),
12609                created_time: "2024-01-01".to_string(),
12610                updated_time: String::new(),
12611                deleted_time: String::new(),
12612                drift_status: String::new(),
12613                last_drift_check_time: String::new(),
12614                status_reason: String::new(),
12615                description: String::new(),
12616                detailed_status: String::new(),
12617                root_stack: String::new(),
12618                parent_stack: String::new(),
12619                termination_protection: false,
12620                iam_role: String::new(),
12621                tags: Vec::new(),
12622                stack_policy: String::new(),
12623                rollback_monitoring_time: String::new(),
12624                rollback_alarms: Vec::new(),
12625                notification_arns: Vec::new(),
12626            },
12627            CfnStack {
12628                name: "stack2".to_string(),
12629                stack_id: "id2".to_string(),
12630                status: "UPDATE_COMPLETE".to_string(),
12631                created_time: "2024-01-02".to_string(),
12632                updated_time: String::new(),
12633                deleted_time: String::new(),
12634                drift_status: String::new(),
12635                last_drift_check_time: String::new(),
12636                status_reason: String::new(),
12637                description: String::new(),
12638                detailed_status: String::new(),
12639                root_stack: String::new(),
12640                parent_stack: String::new(),
12641                termination_protection: false,
12642                iam_role: String::new(),
12643                tags: Vec::new(),
12644                stack_policy: String::new(),
12645                rollback_monitoring_time: String::new(),
12646                rollback_alarms: Vec::new(),
12647                notification_arns: Vec::new(),
12648            },
12649        ];
12650
12651        assert!(!app.snapshot_requested);
12652        app.handle_action(Action::CopyToClipboard);
12653
12654        // Should set snapshot_requested flag
12655        assert!(app.snapshot_requested);
12656    }
12657
12658    #[test]
12659    fn test_cloudformation_expansion_shows_all_visible_columns() {
12660        let mut app = test_app();
12661        app.current_service = Service::CloudFormationStacks;
12662        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12663        app.cfn_state.table.items = vec![CfnStack {
12664            name: "test-stack".to_string(),
12665            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12666                .to_string(),
12667            status: "CREATE_COMPLETE".to_string(),
12668            created_time: "2024-01-01".to_string(),
12669            updated_time: "2024-01-02".to_string(),
12670            deleted_time: String::new(),
12671            drift_status: "IN_SYNC".to_string(),
12672            last_drift_check_time: "2024-01-03".to_string(),
12673            status_reason: String::new(),
12674            description: "Test description".to_string(),
12675            detailed_status: String::new(),
12676            root_stack: String::new(),
12677            parent_stack: String::new(),
12678            termination_protection: false,
12679            iam_role: String::new(),
12680            tags: Vec::new(),
12681            stack_policy: String::new(),
12682            rollback_monitoring_time: String::new(),
12683            rollback_alarms: Vec::new(),
12684            notification_arns: Vec::new(),
12685        }];
12686
12687        // Set visible columns
12688        app.cfn_visible_column_ids = [
12689            CfnColumn::Name,
12690            CfnColumn::Status,
12691            CfnColumn::CreatedTime,
12692            CfnColumn::Description,
12693        ]
12694        .iter()
12695        .map(|c| c.id())
12696        .collect();
12697
12698        app.cfn_state.table.expanded_item = Some(0);
12699
12700        // Verify all visible columns would be shown in expansion
12701        // (This is a structural test - actual rendering is in UI layer)
12702        assert_eq!(app.cfn_visible_column_ids.len(), 4);
12703        assert!(app.cfn_state.table.has_expanded_item());
12704    }
12705
12706    #[test]
12707    fn test_cloudformation_empty_list_shows_page_1() {
12708        let mut app = test_app();
12709        app.current_service = Service::CloudFormationStacks;
12710        app.cfn_state.table.items = vec![];
12711
12712        let filtered = filtered_cloudformation_stacks(&app);
12713        assert_eq!(filtered.len(), 0);
12714
12715        // Pagination should still show [1] even with 0 items
12716        let page_size = app.cfn_state.table.page_size.value();
12717        let total_pages = filtered.len().div_ceil(page_size);
12718        assert_eq!(total_pages, 0);
12719
12720        // render_pagination_text(0, 0) should return "[1]"
12721        // This is tested in UI layer
12722    }
12723}
12724
12725impl App {
12726    pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
12727        let mut all = AwsRegion::all();
12728
12729        // Add latencies to regions
12730        for region in &mut all {
12731            region.latency_ms = self.region_latencies.get(region.code).copied();
12732        }
12733
12734        // Filter by search term
12735        let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
12736            all
12737        } else {
12738            let filter_lower = self.region_filter.to_lowercase();
12739            all.into_iter()
12740                .filter(|r| {
12741                    r.name.to_lowercase().contains(&filter_lower)
12742                        || r.code.to_lowercase().contains(&filter_lower)
12743                        || r.group.to_lowercase().contains(&filter_lower)
12744                })
12745                .collect()
12746        };
12747
12748        // Sort by latency (lowest first), treat None as 1000ms
12749        let mut sorted = filtered;
12750        sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
12751        sorted
12752    }
12753
12754    pub fn measure_region_latencies(&mut self) {
12755        use std::time::Instant;
12756        self.region_latencies.clear();
12757
12758        let regions = AwsRegion::all();
12759        let start_all = Instant::now();
12760        tracing::info!("Starting latency measurement for {} regions", regions.len());
12761
12762        let handles: Vec<_> = regions
12763            .iter()
12764            .map(|region| {
12765                let code = region.code.to_string();
12766                std::thread::spawn(move || {
12767                    // Use STS endpoint - fastest and most reliable
12768                    let endpoint = format!("https://sts.{}.amazonaws.com", code);
12769                    let start = Instant::now();
12770
12771                    match ureq::get(&endpoint)
12772                        .timeout(std::time::Duration::from_secs(2))
12773                        .call()
12774                    {
12775                        Ok(_) => {
12776                            let latency = start.elapsed().as_millis() as u64;
12777                            Some((code, latency))
12778                        }
12779                        Err(e) => {
12780                            tracing::debug!("Failed to measure {}: {}", code, e);
12781                            Some((code, 9999))
12782                        }
12783                    }
12784                })
12785            })
12786            .collect();
12787
12788        for handle in handles {
12789            if let Ok(Some((code, latency))) = handle.join() {
12790                self.region_latencies.insert(code, latency);
12791            }
12792        }
12793
12794        tracing::info!(
12795            "Measured {} regions in {:?}",
12796            self.region_latencies.len(),
12797            start_all.elapsed()
12798        );
12799    }
12800
12801    pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
12802        filter_profiles(&self.available_profiles, &self.profile_filter)
12803    }
12804
12805    pub fn get_filtered_sessions(&self) -> Vec<&Session> {
12806        if self.session_filter.is_empty() {
12807            return self.sessions.iter().collect();
12808        }
12809        let filter_lower = self.session_filter.to_lowercase();
12810        self.sessions
12811            .iter()
12812            .filter(|s| {
12813                s.profile.to_lowercase().contains(&filter_lower)
12814                    || s.region.to_lowercase().contains(&filter_lower)
12815                    || s.account_id.to_lowercase().contains(&filter_lower)
12816                    || s.role_arn.to_lowercase().contains(&filter_lower)
12817            })
12818            .collect()
12819    }
12820
12821    pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
12822        if self.tab_filter.is_empty() {
12823            return self.tabs.iter().enumerate().collect();
12824        }
12825        let filter_lower = self.tab_filter.to_lowercase();
12826        self.tabs
12827            .iter()
12828            .enumerate()
12829            .filter(|(_, tab)| {
12830                tab.title.to_lowercase().contains(&filter_lower)
12831                    || tab.breadcrumb.to_lowercase().contains(&filter_lower)
12832            })
12833            .collect()
12834    }
12835
12836    pub fn load_aws_profiles() -> Vec<AwsProfile> {
12837        AwsProfile::load_all()
12838    }
12839
12840    pub async fn fetch_profile_accounts(&mut self) {
12841        for profile in &mut self.available_profiles {
12842            if profile.account.is_none() {
12843                let region = profile
12844                    .region
12845                    .clone()
12846                    .unwrap_or_else(|| "us-east-1".to_string());
12847                if let Ok(account) =
12848                    rusticity_core::AwsConfig::get_account_for_profile(&profile.name, &region).await
12849                {
12850                    profile.account = Some(account);
12851                }
12852            }
12853        }
12854    }
12855
12856    fn save_current_session(&mut self) {
12857        // If no tabs, delete the session if it exists
12858        if self.tabs.is_empty() {
12859            if let Some(ref session) = self.current_session {
12860                let _ = session.delete();
12861                self.current_session = None;
12862            }
12863            return;
12864        }
12865
12866        let session = if let Some(ref mut current) = self.current_session {
12867            // Update existing session
12868            current.tabs = self
12869                .tabs
12870                .iter()
12871                .map(|t| SessionTab {
12872                    service: format!("{:?}", t.service),
12873                    title: t.title.clone(),
12874                    breadcrumb: t.breadcrumb.clone(),
12875                    filter: match t.service {
12876                        Service::CloudWatchLogGroups => {
12877                            Some(self.log_groups_state.log_groups.filter.clone())
12878                        }
12879                        _ => None,
12880                    },
12881                    selected_item: None,
12882                })
12883                .collect();
12884            current.clone()
12885        } else {
12886            // Create new session
12887            let mut session = Session::new(
12888                self.profile.clone(),
12889                self.region.clone(),
12890                self.config.account_id.clone(),
12891                self.config.role_arn.clone(),
12892            );
12893            session.tabs = self
12894                .tabs
12895                .iter()
12896                .map(|t| SessionTab {
12897                    service: format!("{:?}", t.service),
12898                    title: t.title.clone(),
12899                    breadcrumb: t.breadcrumb.clone(),
12900                    filter: match t.service {
12901                        Service::CloudWatchLogGroups => {
12902                            Some(self.log_groups_state.log_groups.filter.clone())
12903                        }
12904                        _ => None,
12905                    },
12906                    selected_item: None,
12907                })
12908                .collect();
12909            self.current_session = Some(session.clone());
12910            session
12911        };
12912
12913        let _ = session.save();
12914    }
12915}
12916
12917#[cfg(test)]
12918mod iam_policy_view_tests {
12919    use super::*;
12920    use test_helpers::*;
12921
12922    #[test]
12923    fn test_enter_opens_policy_view() {
12924        let mut app = test_app();
12925        app.current_service = Service::IamRoles;
12926        app.service_selected = true;
12927        app.mode = Mode::Normal;
12928        app.view_mode = ViewMode::Detail;
12929        app.iam_state.current_role = Some("TestRole".to_string());
12930        app.iam_state.policies.items = vec![IamPolicy {
12931            policy_name: "TestPolicy".to_string(),
12932            policy_type: "Inline".to_string(),
12933            attached_via: "Direct".to_string(),
12934            attached_entities: "1".to_string(),
12935            description: "Test".to_string(),
12936            creation_time: "2023-01-01".to_string(),
12937            edited_time: "2023-01-01".to_string(),
12938            policy_arn: None,
12939        }];
12940        app.iam_state.policies.reset();
12941
12942        app.handle_action(Action::Select);
12943
12944        assert_eq!(app.view_mode, ViewMode::PolicyView);
12945        assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
12946        assert_eq!(app.iam_state.policy_scroll, 0);
12947        assert!(app.iam_state.policies.loading);
12948    }
12949
12950    #[test]
12951    fn test_escape_closes_policy_view() {
12952        let mut app = test_app();
12953        app.current_service = Service::IamRoles;
12954        app.service_selected = true;
12955        app.mode = Mode::Normal;
12956        app.view_mode = ViewMode::PolicyView;
12957        app.iam_state.current_role = Some("TestRole".to_string());
12958        app.iam_state.current_policy = Some("TestPolicy".to_string());
12959        app.iam_state.policy_document = "{\n  \"test\": \"value\"\n}".to_string();
12960        app.iam_state.policy_scroll = 5;
12961
12962        app.handle_action(Action::PrevPane);
12963
12964        assert_eq!(app.view_mode, ViewMode::Detail);
12965        assert_eq!(app.iam_state.current_policy, None);
12966        assert_eq!(app.iam_state.policy_document, "");
12967        assert_eq!(app.iam_state.policy_scroll, 0);
12968    }
12969
12970    #[test]
12971    fn test_ctrl_d_scrolls_down_in_policy_view() {
12972        let mut app = test_app();
12973        app.current_service = Service::IamRoles;
12974        app.service_selected = true;
12975        app.mode = Mode::Normal;
12976        app.view_mode = ViewMode::PolicyView;
12977        app.iam_state.current_role = Some("TestRole".to_string());
12978        app.iam_state.current_policy = Some("TestPolicy".to_string());
12979        app.iam_state.policy_document = (0..100)
12980            .map(|i| format!("line {}", i))
12981            .collect::<Vec<_>>()
12982            .join("\n");
12983        app.iam_state.policy_scroll = 0;
12984
12985        app.handle_action(Action::ScrollDown);
12986
12987        assert_eq!(app.iam_state.policy_scroll, 10);
12988
12989        app.handle_action(Action::ScrollDown);
12990
12991        assert_eq!(app.iam_state.policy_scroll, 20);
12992    }
12993
12994    #[test]
12995    fn test_ctrl_u_scrolls_up_in_policy_view() {
12996        let mut app = test_app();
12997        app.current_service = Service::IamRoles;
12998        app.service_selected = true;
12999        app.mode = Mode::Normal;
13000        app.view_mode = ViewMode::PolicyView;
13001        app.iam_state.current_role = Some("TestRole".to_string());
13002        app.iam_state.current_policy = Some("TestPolicy".to_string());
13003        app.iam_state.policy_document = (0..100)
13004            .map(|i| format!("line {}", i))
13005            .collect::<Vec<_>>()
13006            .join("\n");
13007        app.iam_state.policy_scroll = 30;
13008
13009        app.handle_action(Action::ScrollUp);
13010
13011        assert_eq!(app.iam_state.policy_scroll, 20);
13012
13013        app.handle_action(Action::ScrollUp);
13014
13015        assert_eq!(app.iam_state.policy_scroll, 10);
13016    }
13017
13018    #[test]
13019    fn test_scroll_does_not_go_negative() {
13020        let mut app = test_app();
13021        app.current_service = Service::IamRoles;
13022        app.service_selected = true;
13023        app.mode = Mode::Normal;
13024        app.view_mode = ViewMode::PolicyView;
13025        app.iam_state.current_role = Some("TestRole".to_string());
13026        app.iam_state.current_policy = Some("TestPolicy".to_string());
13027        app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
13028        app.iam_state.policy_scroll = 0;
13029
13030        app.handle_action(Action::ScrollUp);
13031
13032        assert_eq!(app.iam_state.policy_scroll, 0);
13033    }
13034
13035    #[test]
13036    fn test_scroll_does_not_exceed_max() {
13037        let mut app = test_app();
13038        app.current_service = Service::IamRoles;
13039        app.service_selected = true;
13040        app.mode = Mode::Normal;
13041        app.view_mode = ViewMode::PolicyView;
13042        app.iam_state.current_role = Some("TestRole".to_string());
13043        app.iam_state.current_policy = Some("TestPolicy".to_string());
13044        app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
13045        app.iam_state.policy_scroll = 0;
13046
13047        app.handle_action(Action::ScrollDown);
13048
13049        assert_eq!(app.iam_state.policy_scroll, 2); // Max is 2 (3 lines - 1)
13050    }
13051
13052    #[test]
13053    fn test_policy_view_console_url() {
13054        let mut app = test_app();
13055        app.current_service = Service::IamRoles;
13056        app.service_selected = true;
13057        app.view_mode = ViewMode::PolicyView;
13058        app.iam_state.current_role = Some("TestRole".to_string());
13059        app.iam_state.current_policy = Some("TestPolicy".to_string());
13060
13061        let url = app.get_console_url();
13062
13063        assert!(url.contains("us-east-1.console.aws.amazon.com"));
13064        assert!(url.contains("/roles/details/TestRole"));
13065        assert!(url.contains("/editPolicy/TestPolicy"));
13066        assert!(url.contains("step=addPermissions"));
13067    }
13068
13069    #[test]
13070    fn test_esc_from_policy_view_goes_to_role_detail() {
13071        let mut app = test_app();
13072        app.current_service = Service::IamRoles;
13073        app.service_selected = true;
13074        app.mode = Mode::Normal;
13075        app.view_mode = ViewMode::PolicyView;
13076        app.iam_state.current_role = Some("TestRole".to_string());
13077        app.iam_state.current_policy = Some("TestPolicy".to_string());
13078        app.iam_state.policy_document = "test".to_string();
13079        app.iam_state.policy_scroll = 5;
13080
13081        app.handle_action(Action::GoBack);
13082
13083        assert_eq!(app.view_mode, ViewMode::Detail);
13084        assert_eq!(app.iam_state.current_policy, None);
13085        assert_eq!(app.iam_state.policy_document, "");
13086        assert_eq!(app.iam_state.policy_scroll, 0);
13087        assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
13088    }
13089
13090    #[test]
13091    fn test_esc_from_role_detail_goes_to_role_list() {
13092        let mut app = test_app();
13093        app.current_service = Service::IamRoles;
13094        app.service_selected = true;
13095        app.mode = Mode::Normal;
13096        app.view_mode = ViewMode::Detail;
13097        app.iam_state.current_role = Some("TestRole".to_string());
13098
13099        app.handle_action(Action::GoBack);
13100
13101        assert_eq!(app.iam_state.current_role, None);
13102    }
13103
13104    #[test]
13105    fn test_right_arrow_expands_policy_row() {
13106        let mut app = test_app();
13107        app.current_service = Service::IamRoles;
13108        app.service_selected = true;
13109        app.mode = Mode::Normal;
13110        app.view_mode = ViewMode::Detail;
13111        app.iam_state.current_role = Some("TestRole".to_string());
13112        app.iam_state.policies.items = vec![IamPolicy {
13113            policy_name: "TestPolicy".to_string(),
13114            policy_type: "Inline".to_string(),
13115            attached_via: "Direct".to_string(),
13116            attached_entities: "1".to_string(),
13117            description: "Test".to_string(),
13118            creation_time: "2023-01-01".to_string(),
13119            edited_time: "2023-01-01".to_string(),
13120            policy_arn: None,
13121        }];
13122        app.iam_state.policies.reset();
13123
13124        app.handle_action(Action::NextPane);
13125
13126        // Should expand, not drill down
13127        assert_eq!(app.view_mode, ViewMode::Detail);
13128        assert_eq!(app.iam_state.current_policy, None);
13129        assert_eq!(app.iam_state.policies.expanded_item, Some(0));
13130    }
13131}
13132
13133#[cfg(test)]
13134mod tab_filter_tests {
13135    use super::*;
13136    use test_helpers::*;
13137
13138    #[test]
13139    fn test_space_t_opens_tab_picker() {
13140        let mut app = test_app();
13141        app.tabs = vec![
13142            Tab {
13143                service: Service::CloudWatchLogGroups,
13144                title: "Tab 1".to_string(),
13145                breadcrumb: "CloudWatch > Log groups".to_string(),
13146            },
13147            Tab {
13148                service: Service::S3Buckets,
13149                title: "Tab 2".to_string(),
13150                breadcrumb: "S3 › Buckets".to_string(),
13151            },
13152        ];
13153        app.current_tab = 0;
13154
13155        app.handle_action(Action::OpenTabPicker);
13156
13157        assert_eq!(app.mode, Mode::TabPicker);
13158        assert_eq!(app.tab_picker_selected, 0);
13159    }
13160
13161    #[test]
13162    fn test_tab_filter_works() {
13163        let mut app = test_app();
13164        app.tabs = vec![
13165            Tab {
13166                service: Service::CloudWatchLogGroups,
13167                title: "CloudWatch Logs".to_string(),
13168                breadcrumb: "CloudWatch > Log groups".to_string(),
13169            },
13170            Tab {
13171                service: Service::S3Buckets,
13172                title: "S3 Buckets".to_string(),
13173                breadcrumb: "S3 › Buckets".to_string(),
13174            },
13175            Tab {
13176                service: Service::CloudWatchAlarms,
13177                title: "CloudWatch Alarms".to_string(),
13178                breadcrumb: "CloudWatch › Alarms".to_string(),
13179            },
13180        ];
13181        app.mode = Mode::TabPicker;
13182
13183        // Filter for "s3"
13184        app.handle_action(Action::FilterInput('s'));
13185        app.handle_action(Action::FilterInput('3'));
13186
13187        let filtered = app.get_filtered_tabs();
13188        assert_eq!(filtered.len(), 1);
13189        assert_eq!(filtered[0].1.title, "S3 Buckets");
13190    }
13191
13192    #[test]
13193    fn test_tab_filter_by_breadcrumb() {
13194        let mut app = test_app();
13195        app.tabs = vec![
13196            Tab {
13197                service: Service::CloudWatchLogGroups,
13198                title: "Tab 1".to_string(),
13199                breadcrumb: "CloudWatch > Log groups".to_string(),
13200            },
13201            Tab {
13202                service: Service::S3Buckets,
13203                title: "Tab 2".to_string(),
13204                breadcrumb: "S3 › Buckets".to_string(),
13205            },
13206        ];
13207        app.mode = Mode::TabPicker;
13208
13209        // Filter for "cloudwatch"
13210        app.handle_action(Action::FilterInput('c'));
13211        app.handle_action(Action::FilterInput('l'));
13212        app.handle_action(Action::FilterInput('o'));
13213        app.handle_action(Action::FilterInput('u'));
13214        app.handle_action(Action::FilterInput('d'));
13215
13216        let filtered = app.get_filtered_tabs();
13217        assert_eq!(filtered.len(), 1);
13218        assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
13219    }
13220
13221    #[test]
13222    fn test_tab_filter_backspace() {
13223        let mut app = test_app();
13224        app.tabs = vec![
13225            Tab {
13226                service: Service::CloudWatchLogGroups,
13227                title: "CloudWatch Logs".to_string(),
13228                breadcrumb: "CloudWatch > Log groups".to_string(),
13229            },
13230            Tab {
13231                service: Service::S3Buckets,
13232                title: "S3 Buckets".to_string(),
13233                breadcrumb: "S3 › Buckets".to_string(),
13234            },
13235        ];
13236        app.mode = Mode::TabPicker;
13237
13238        app.handle_action(Action::FilterInput('s'));
13239        app.handle_action(Action::FilterInput('3'));
13240        assert_eq!(app.tab_filter, "s3");
13241
13242        app.handle_action(Action::FilterBackspace);
13243        assert_eq!(app.tab_filter, "s");
13244
13245        let filtered = app.get_filtered_tabs();
13246        assert_eq!(filtered.len(), 2); // Both match "s"
13247    }
13248
13249    #[test]
13250    fn test_tab_selection_with_filter() {
13251        let mut app = test_app();
13252        app.tabs = vec![
13253            Tab {
13254                service: Service::CloudWatchLogGroups,
13255                title: "CloudWatch Logs".to_string(),
13256                breadcrumb: "CloudWatch > Log groups".to_string(),
13257            },
13258            Tab {
13259                service: Service::S3Buckets,
13260                title: "S3 Buckets".to_string(),
13261                breadcrumb: "S3 › Buckets".to_string(),
13262            },
13263        ];
13264        app.mode = Mode::TabPicker;
13265        app.current_tab = 0;
13266
13267        // Filter for "s3"
13268        app.handle_action(Action::FilterInput('s'));
13269        app.handle_action(Action::FilterInput('3'));
13270
13271        // Select the filtered tab
13272        app.handle_action(Action::Select);
13273
13274        assert_eq!(app.current_tab, 1); // Should select the S3 tab (index 1)
13275        assert_eq!(app.mode, Mode::Normal);
13276        assert_eq!(app.tab_filter, ""); // Filter should be cleared
13277    }
13278}
13279
13280#[cfg(test)]
13281mod region_latency_tests {
13282    use super::*;
13283    use test_helpers::*;
13284
13285    #[test]
13286    fn test_regions_sorted_by_latency() {
13287        let mut app = test_app();
13288
13289        // Add some latencies
13290        app.region_latencies.insert("us-west-2".to_string(), 50);
13291        app.region_latencies.insert("us-east-1".to_string(), 10);
13292        app.region_latencies.insert("eu-west-1".to_string(), 100);
13293
13294        let filtered = app.get_filtered_regions();
13295
13296        // Should be sorted by latency (lowest first)
13297        let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
13298
13299        assert!(with_latency.len() >= 3);
13300        assert_eq!(with_latency[0].code, "us-east-1");
13301        assert_eq!(with_latency[0].latency_ms, Some(10));
13302        assert_eq!(with_latency[1].code, "us-west-2");
13303        assert_eq!(with_latency[1].latency_ms, Some(50));
13304        assert_eq!(with_latency[2].code, "eu-west-1");
13305        assert_eq!(with_latency[2].latency_ms, Some(100));
13306    }
13307
13308    #[test]
13309    fn test_regions_with_latency_before_without() {
13310        let mut app = test_app();
13311
13312        // Only add latency for one region
13313        app.region_latencies.insert("eu-west-1".to_string(), 100);
13314
13315        let filtered = app.get_filtered_regions();
13316
13317        // Region with latency should come first
13318        assert_eq!(filtered[0].code, "eu-west-1");
13319        assert_eq!(filtered[0].latency_ms, Some(100));
13320
13321        // Rest should be sorted by name
13322        for region in &filtered[1..] {
13323            assert!(region.latency_ms.is_none());
13324        }
13325    }
13326
13327    #[test]
13328    fn test_region_filter_with_latency() {
13329        let mut app = test_app();
13330
13331        app.region_latencies.insert("us-east-1".to_string(), 10);
13332        app.region_latencies.insert("us-west-2".to_string(), 50);
13333        app.region_filter = "us".to_string();
13334
13335        let filtered = app.get_filtered_regions();
13336
13337        // Should only have US regions, sorted by latency
13338        assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
13339        assert_eq!(filtered[0].code, "us-east-1");
13340        assert_eq!(filtered[1].code, "us-west-2");
13341    }
13342
13343    #[test]
13344    fn test_latency_persists_across_filters() {
13345        let mut app = test_app();
13346
13347        app.region_latencies.insert("us-east-1".to_string(), 10);
13348
13349        // Filter to something else
13350        app.region_filter = "eu".to_string();
13351        let filtered = app.get_filtered_regions();
13352        assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
13353
13354        // Clear filter
13355        app.region_filter.clear();
13356        let all = app.get_filtered_regions();
13357
13358        // Latency should still be there
13359        let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
13360        assert_eq!(us_east.latency_ms, Some(10));
13361    }
13362
13363    #[test]
13364    fn test_measure_region_latencies_clears_previous() {
13365        let mut app = test_app();
13366
13367        // Add some fake latencies
13368        app.region_latencies.insert("us-east-1".to_string(), 100);
13369        app.region_latencies.insert("eu-west-1".to_string(), 200);
13370
13371        // Measure again (will fail to connect but should clear)
13372        app.measure_region_latencies();
13373
13374        // Old latencies should be cleared
13375        assert!(
13376            app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
13377        );
13378    }
13379
13380    #[test]
13381    fn test_regions_with_latency_sorted_first() {
13382        let mut app = test_app();
13383
13384        // Add latencies: one fast, one slow (>1000ms would be treated as >1s)
13385        app.region_latencies.insert("us-east-1".to_string(), 50);
13386        app.region_latencies.insert("eu-west-1".to_string(), 500);
13387
13388        let filtered = app.get_filtered_regions();
13389
13390        // Should show all regions
13391        assert!(filtered.len() > 2);
13392
13393        // Fast regions first
13394        assert_eq!(filtered[0].code, "us-east-1");
13395        assert_eq!(filtered[0].latency_ms, Some(50));
13396        assert_eq!(filtered[1].code, "eu-west-1");
13397        assert_eq!(filtered[1].latency_ms, Some(500));
13398
13399        // Regions without latency treated as 1000ms, so they come after 500ms
13400        for region in &filtered[2..] {
13401            assert!(region.latency_ms.is_none());
13402        }
13403    }
13404
13405    #[test]
13406    fn test_regions_without_latency_sorted_as_1000ms() {
13407        let mut app = test_app();
13408
13409        // Add one region with 1500ms (slower than default 1000ms)
13410        app.region_latencies
13411            .insert("ap-southeast-2".to_string(), 1500);
13412        // Add one region with 50ms (faster)
13413        app.region_latencies.insert("us-east-1".to_string(), 50);
13414
13415        let filtered = app.get_filtered_regions();
13416
13417        // Fast region first
13418        assert_eq!(filtered[0].code, "us-east-1");
13419        assert_eq!(filtered[0].latency_ms, Some(50));
13420
13421        // Regions without latency (treated as 1000ms) come before 1500ms
13422        let slow_region_idx = filtered
13423            .iter()
13424            .position(|r| r.code == "ap-southeast-2")
13425            .unwrap();
13426        assert!(slow_region_idx > 1); // Should be after fast region and regions without latency
13427
13428        // All regions between index 1 and slow_region_idx should have no latency
13429        for region in filtered.iter().take(slow_region_idx).skip(1) {
13430            assert!(region.latency_ms.is_none());
13431        }
13432    }
13433
13434    #[test]
13435    fn test_region_picker_opens_with_latencies() {
13436        let mut app = test_app();
13437
13438        // Simulate opening region picker
13439        app.region_filter.clear();
13440        app.region_picker_selected = 0;
13441        app.measure_region_latencies();
13442
13443        // Should have attempted to measure (even if all fail in test env)
13444        // The map should be initialized
13445        assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
13446    }
13447
13448    #[test]
13449    fn test_ecr_tab_next() {
13450        assert_eq!(EcrTab::Private.next(), EcrTab::Public);
13451        assert_eq!(EcrTab::Public.next(), EcrTab::Private);
13452    }
13453
13454    #[test]
13455    fn test_ecr_tab_switching() {
13456        let mut app = test_app();
13457        app.current_service = Service::EcrRepositories;
13458        app.service_selected = true;
13459        app.ecr_state.tab = EcrTab::Private;
13460
13461        app.handle_action(Action::NextDetailTab);
13462        assert_eq!(app.ecr_state.tab, EcrTab::Public);
13463        assert_eq!(app.ecr_state.repositories.selected, 0);
13464
13465        app.handle_action(Action::NextDetailTab);
13466        assert_eq!(app.ecr_state.tab, EcrTab::Private);
13467    }
13468
13469    #[test]
13470    fn test_ecr_navigation() {
13471        let mut app = test_app();
13472        app.current_service = Service::EcrRepositories;
13473        app.service_selected = true;
13474        app.mode = Mode::Normal;
13475        app.ecr_state.repositories.items = vec![
13476            EcrRepository {
13477                name: "repo1".to_string(),
13478                uri: "uri1".to_string(),
13479                created_at: "2023-01-01".to_string(),
13480                tag_immutability: "MUTABLE".to_string(),
13481                encryption_type: "AES256".to_string(),
13482            },
13483            EcrRepository {
13484                name: "repo2".to_string(),
13485                uri: "uri2".to_string(),
13486                created_at: "2023-01-02".to_string(),
13487                tag_immutability: "IMMUTABLE".to_string(),
13488                encryption_type: "KMS".to_string(),
13489            },
13490        ];
13491
13492        app.handle_action(Action::NextItem);
13493        assert_eq!(app.ecr_state.repositories.selected, 1);
13494
13495        app.handle_action(Action::PrevItem);
13496        assert_eq!(app.ecr_state.repositories.selected, 0);
13497    }
13498
13499    #[test]
13500    fn test_ecr_filter() {
13501        let mut app = test_app();
13502        app.current_service = Service::EcrRepositories;
13503        app.service_selected = true;
13504        app.ecr_state.repositories.items = vec![
13505            EcrRepository {
13506                name: "my-app".to_string(),
13507                uri: "uri1".to_string(),
13508                created_at: "2023-01-01".to_string(),
13509                tag_immutability: "MUTABLE".to_string(),
13510                encryption_type: "AES256".to_string(),
13511            },
13512            EcrRepository {
13513                name: "other-service".to_string(),
13514                uri: "uri2".to_string(),
13515                created_at: "2023-01-02".to_string(),
13516                tag_immutability: "IMMUTABLE".to_string(),
13517                encryption_type: "KMS".to_string(),
13518            },
13519        ];
13520
13521        app.ecr_state.repositories.filter = "app".to_string();
13522        let filtered = filtered_ecr_repositories(&app);
13523        assert_eq!(filtered.len(), 1);
13524        assert_eq!(filtered[0].name, "my-app");
13525    }
13526
13527    #[test]
13528    fn test_ecr_filter_input() {
13529        let mut app = test_app();
13530        app.current_service = Service::EcrRepositories;
13531        app.service_selected = true;
13532        app.mode = Mode::FilterInput;
13533
13534        app.handle_action(Action::FilterInput('t'));
13535        app.handle_action(Action::FilterInput('e'));
13536        app.handle_action(Action::FilterInput('s'));
13537        app.handle_action(Action::FilterInput('t'));
13538        assert_eq!(app.ecr_state.repositories.filter, "test");
13539
13540        app.handle_action(Action::FilterBackspace);
13541        assert_eq!(app.ecr_state.repositories.filter, "tes");
13542    }
13543
13544    #[test]
13545    fn test_ecr_filter_resets_selection() {
13546        let mut app = test_app();
13547        app.current_service = Service::EcrRepositories;
13548        app.service_selected = true;
13549        app.mode = Mode::FilterInput;
13550        app.ecr_state.repositories.items = vec![
13551            EcrRepository {
13552                name: "repo1".to_string(),
13553                uri: "uri1".to_string(),
13554                created_at: "2023-01-01".to_string(),
13555                tag_immutability: "MUTABLE".to_string(),
13556                encryption_type: "AES256".to_string(),
13557            },
13558            EcrRepository {
13559                name: "repo2".to_string(),
13560                uri: "uri2".to_string(),
13561                created_at: "2023-01-02".to_string(),
13562                tag_immutability: "IMMUTABLE".to_string(),
13563                encryption_type: "KMS".to_string(),
13564            },
13565            EcrRepository {
13566                name: "repo3".to_string(),
13567                uri: "uri3".to_string(),
13568                created_at: "2023-01-03".to_string(),
13569                tag_immutability: "MUTABLE".to_string(),
13570                encryption_type: "AES256".to_string(),
13571            },
13572        ];
13573
13574        // Move selection to second item
13575        app.ecr_state.repositories.selected = 2;
13576        assert_eq!(app.ecr_state.repositories.selected, 2);
13577
13578        // Apply filter - selection should reset to 0
13579        app.handle_action(Action::FilterInput('t'));
13580        assert_eq!(app.ecr_state.repositories.filter, "t");
13581        assert_eq!(app.ecr_state.repositories.selected, 0);
13582
13583        // Move selection again
13584        app.ecr_state.repositories.selected = 1;
13585
13586        // Backspace filter - selection should reset to 0
13587        app.handle_action(Action::FilterBackspace);
13588        assert_eq!(app.ecr_state.repositories.filter, "");
13589        assert_eq!(app.ecr_state.repositories.selected, 0);
13590    }
13591
13592    #[test]
13593    fn test_ecr_images_filter_resets_selection() {
13594        let mut app = test_app();
13595        app.current_service = Service::EcrRepositories;
13596        app.service_selected = true;
13597        app.mode = Mode::FilterInput;
13598        app.ecr_state.current_repository = Some("test-repo".to_string());
13599        app.ecr_state.images.items = vec![
13600            EcrImage {
13601                tag: "v1.0.0".to_string(),
13602                artifact_type: "container".to_string(),
13603                digest: "sha256:abc123".to_string(),
13604                pushed_at: "2023-01-01".to_string(),
13605                size_bytes: 1000,
13606                uri: "uri1".to_string(),
13607                last_pull_time: "".to_string(),
13608            },
13609            EcrImage {
13610                tag: "v2.0.0".to_string(),
13611                artifact_type: "container".to_string(),
13612                digest: "sha256:def456".to_string(),
13613                pushed_at: "2023-01-02".to_string(),
13614                size_bytes: 2000,
13615                uri: "uri2".to_string(),
13616                last_pull_time: "".to_string(),
13617            },
13618        ];
13619
13620        // Move selection to second item
13621        app.ecr_state.images.selected = 1;
13622        assert_eq!(app.ecr_state.images.selected, 1);
13623
13624        // Apply filter - selection should reset to 0
13625        app.handle_action(Action::FilterInput('v'));
13626        assert_eq!(app.ecr_state.images.filter, "v");
13627        assert_eq!(app.ecr_state.images.selected, 0);
13628    }
13629
13630    #[test]
13631    fn test_iam_users_filter_input() {
13632        let mut app = test_app();
13633        app.current_service = Service::IamUsers;
13634        app.service_selected = true;
13635        app.mode = Mode::FilterInput;
13636
13637        app.handle_action(Action::FilterInput('a'));
13638        app.handle_action(Action::FilterInput('d'));
13639        app.handle_action(Action::FilterInput('m'));
13640        app.handle_action(Action::FilterInput('i'));
13641        app.handle_action(Action::FilterInput('n'));
13642        assert_eq!(app.iam_state.users.filter, "admin");
13643
13644        app.handle_action(Action::FilterBackspace);
13645        assert_eq!(app.iam_state.users.filter, "admi");
13646    }
13647
13648    #[test]
13649    fn test_iam_policies_filter_input() {
13650        let mut app = test_app();
13651        app.current_service = Service::IamUsers;
13652        app.service_selected = true;
13653        app.iam_state.current_user = Some("testuser".to_string());
13654        app.mode = Mode::FilterInput;
13655
13656        app.handle_action(Action::FilterInput('r'));
13657        app.handle_action(Action::FilterInput('e'));
13658        app.handle_action(Action::FilterInput('a'));
13659        app.handle_action(Action::FilterInput('d'));
13660        assert_eq!(app.iam_state.policies.filter, "read");
13661
13662        app.handle_action(Action::FilterBackspace);
13663        assert_eq!(app.iam_state.policies.filter, "rea");
13664    }
13665
13666    #[test]
13667    fn test_iam_start_filter() {
13668        let mut app = test_app();
13669        app.current_service = Service::IamUsers;
13670        app.service_selected = true;
13671        app.mode = Mode::Normal;
13672
13673        app.handle_action(Action::StartFilter);
13674        assert_eq!(app.mode, Mode::FilterInput);
13675    }
13676
13677    #[test]
13678    fn test_iam_roles_filter_input() {
13679        let mut app = test_app();
13680        app.current_service = Service::IamRoles;
13681        app.service_selected = true;
13682        app.mode = Mode::FilterInput;
13683
13684        app.handle_action(Action::FilterInput('a'));
13685        app.handle_action(Action::FilterInput('d'));
13686        app.handle_action(Action::FilterInput('m'));
13687        app.handle_action(Action::FilterInput('i'));
13688        app.handle_action(Action::FilterInput('n'));
13689        assert_eq!(app.iam_state.roles.filter, "admin");
13690
13691        app.handle_action(Action::FilterBackspace);
13692        assert_eq!(app.iam_state.roles.filter, "admi");
13693    }
13694
13695    #[test]
13696    fn test_iam_roles_start_filter() {
13697        let mut app = test_app();
13698        app.current_service = Service::IamRoles;
13699        app.service_selected = true;
13700        app.mode = Mode::Normal;
13701
13702        app.handle_action(Action::StartFilter);
13703        assert_eq!(app.mode, Mode::FilterInput);
13704    }
13705
13706    #[test]
13707    fn test_iam_roles_navigation() {
13708        let mut app = test_app();
13709        app.current_service = Service::IamRoles;
13710        app.service_selected = true;
13711        app.mode = Mode::Normal;
13712        app.iam_state.roles.items = (0..10)
13713            .map(|i| IamRole {
13714                role_name: format!("role{}", i),
13715                path: "/".to_string(),
13716                trusted_entities: String::new(),
13717                last_activity: String::new(),
13718                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
13719                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
13720                description: String::new(),
13721                max_session_duration: Some(3600),
13722            })
13723            .collect();
13724
13725        assert_eq!(app.iam_state.roles.selected, 0);
13726
13727        app.handle_action(Action::NextItem);
13728        assert_eq!(app.iam_state.roles.selected, 1);
13729
13730        app.handle_action(Action::NextItem);
13731        assert_eq!(app.iam_state.roles.selected, 2);
13732
13733        app.handle_action(Action::PrevItem);
13734        assert_eq!(app.iam_state.roles.selected, 1);
13735    }
13736
13737    #[test]
13738    fn test_iam_roles_page_hotkey() {
13739        let mut app = test_app();
13740        app.current_service = Service::IamRoles;
13741        app.service_selected = true;
13742        app.mode = Mode::Normal;
13743        app.iam_state.roles.page_size = PageSize::Ten;
13744        app.iam_state.roles.items = (0..100)
13745            .map(|i| IamRole {
13746                role_name: format!("role{}", i),
13747                path: "/".to_string(),
13748                trusted_entities: String::new(),
13749                last_activity: String::new(),
13750                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
13751                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
13752                description: String::new(),
13753                max_session_duration: Some(3600),
13754            })
13755            .collect();
13756
13757        app.handle_action(Action::FilterInput('2'));
13758        app.handle_action(Action::OpenColumnSelector);
13759        assert_eq!(app.iam_state.roles.selected, 10); // Page 2 = index 10 (with page size 10)
13760    }
13761
13762    #[test]
13763    fn test_iam_users_page_hotkey() {
13764        let mut app = test_app();
13765        app.current_service = Service::IamUsers;
13766        app.service_selected = true;
13767        app.mode = Mode::Normal;
13768        app.iam_state.users.page_size = PageSize::Ten;
13769        app.iam_state.users.items = (0..100)
13770            .map(|i| IamUser {
13771                user_name: format!("user{}", i),
13772                path: "/".to_string(),
13773                groups: String::new(),
13774                last_activity: String::new(),
13775                mfa: String::new(),
13776                password_age: String::new(),
13777                console_last_sign_in: String::new(),
13778                access_key_id: String::new(),
13779                active_key_age: String::new(),
13780                access_key_last_used: String::new(),
13781                arn: format!("arn:aws:iam::123456789012:user/user{}", i),
13782                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
13783                console_access: String::new(),
13784                signing_certs: String::new(),
13785            })
13786            .collect();
13787
13788        app.handle_action(Action::FilterInput('3'));
13789        app.handle_action(Action::OpenColumnSelector);
13790        assert_eq!(app.iam_state.users.selected, 20); // Page 3 = index 20 (with page size 10)
13791    }
13792
13793    #[test]
13794    fn test_ecr_scroll_navigation() {
13795        let mut app = test_app();
13796        app.current_service = Service::EcrRepositories;
13797        app.service_selected = true;
13798        app.ecr_state.repositories.items = (0..20)
13799            .map(|i| EcrRepository {
13800                name: format!("repo{}", i),
13801                uri: format!("uri{}", i),
13802                created_at: "2023-01-01".to_string(),
13803                tag_immutability: "MUTABLE".to_string(),
13804                encryption_type: "AES256".to_string(),
13805            })
13806            .collect();
13807
13808        app.handle_action(Action::ScrollDown);
13809        assert_eq!(app.ecr_state.repositories.selected, 10);
13810
13811        app.handle_action(Action::ScrollUp);
13812        assert_eq!(app.ecr_state.repositories.selected, 0);
13813    }
13814
13815    #[test]
13816    fn test_ecr_tab_switching_triggers_reload() {
13817        let mut app = test_app();
13818        app.current_service = Service::EcrRepositories;
13819        app.service_selected = true;
13820        app.ecr_state.tab = EcrTab::Private;
13821        app.ecr_state.repositories.loading = false;
13822        app.ecr_state.repositories.items = vec![EcrRepository {
13823            name: "private-repo".to_string(),
13824            uri: "uri".to_string(),
13825            created_at: "2023-01-01".to_string(),
13826            tag_immutability: "MUTABLE".to_string(),
13827            encryption_type: "AES256".to_string(),
13828        }];
13829
13830        app.handle_action(Action::NextDetailTab);
13831        assert_eq!(app.ecr_state.tab, EcrTab::Public);
13832        assert!(app.ecr_state.repositories.loading);
13833        assert_eq!(app.ecr_state.repositories.selected, 0);
13834    }
13835
13836    #[test]
13837    fn test_ecr_tab_cycles_between_private_and_public() {
13838        let mut app = test_app();
13839        app.current_service = Service::EcrRepositories;
13840        app.service_selected = true;
13841        app.ecr_state.tab = EcrTab::Private;
13842
13843        app.handle_action(Action::NextDetailTab);
13844        assert_eq!(app.ecr_state.tab, EcrTab::Public);
13845
13846        app.handle_action(Action::NextDetailTab);
13847        assert_eq!(app.ecr_state.tab, EcrTab::Private);
13848    }
13849
13850    #[test]
13851    fn test_page_size_values() {
13852        assert_eq!(PageSize::Ten.value(), 10);
13853        assert_eq!(PageSize::TwentyFive.value(), 25);
13854        assert_eq!(PageSize::Fifty.value(), 50);
13855        assert_eq!(PageSize::OneHundred.value(), 100);
13856    }
13857
13858    #[test]
13859    fn test_page_size_next() {
13860        assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
13861        assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
13862        assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
13863        assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
13864    }
13865
13866    #[test]
13867    fn test_ecr_enter_drills_into_repository() {
13868        let mut app = test_app();
13869        app.current_service = Service::EcrRepositories;
13870        app.service_selected = true;
13871        app.mode = Mode::Normal;
13872        app.ecr_state.repositories.items = vec![EcrRepository {
13873            name: "my-repo".to_string(),
13874            uri: "uri".to_string(),
13875            created_at: "2023-01-01".to_string(),
13876            tag_immutability: "MUTABLE".to_string(),
13877            encryption_type: "AES256".to_string(),
13878        }];
13879
13880        app.handle_action(Action::Select);
13881        assert_eq!(
13882            app.ecr_state.current_repository,
13883            Some("my-repo".to_string())
13884        );
13885        assert!(app.ecr_state.repositories.loading);
13886    }
13887
13888    #[test]
13889    fn test_ecr_repository_expansion() {
13890        let mut app = test_app();
13891        app.current_service = Service::EcrRepositories;
13892        app.service_selected = true;
13893        app.ecr_state.repositories.items = vec![EcrRepository {
13894            name: "my-repo".to_string(),
13895            uri: "uri".to_string(),
13896            created_at: "2023-01-01".to_string(),
13897            tag_immutability: "MUTABLE".to_string(),
13898            encryption_type: "AES256".to_string(),
13899        }];
13900        app.ecr_state.repositories.selected = 0;
13901
13902        assert_eq!(app.ecr_state.repositories.expanded_item, None);
13903
13904        app.handle_action(Action::NextPane);
13905        assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
13906
13907        app.handle_action(Action::PrevPane);
13908        assert_eq!(app.ecr_state.repositories.expanded_item, None);
13909    }
13910
13911    #[test]
13912    fn test_ecr_ctrl_d_scrolls_down() {
13913        let mut app = test_app();
13914        app.current_service = Service::EcrRepositories;
13915        app.service_selected = true;
13916        app.mode = Mode::Normal;
13917        app.ecr_state.repositories.items = (0..30)
13918            .map(|i| EcrRepository {
13919                name: format!("repo{}", i),
13920                uri: format!("uri{}", i),
13921                created_at: "2023-01-01".to_string(),
13922                tag_immutability: "MUTABLE".to_string(),
13923                encryption_type: "AES256".to_string(),
13924            })
13925            .collect();
13926        app.ecr_state.repositories.selected = 0;
13927
13928        app.handle_action(Action::PageDown);
13929        assert_eq!(app.ecr_state.repositories.selected, 10);
13930    }
13931
13932    #[test]
13933    fn test_ecr_ctrl_u_scrolls_up() {
13934        let mut app = test_app();
13935        app.current_service = Service::EcrRepositories;
13936        app.service_selected = true;
13937        app.mode = Mode::Normal;
13938        app.ecr_state.repositories.items = (0..30)
13939            .map(|i| EcrRepository {
13940                name: format!("repo{}", i),
13941                uri: format!("uri{}", i),
13942                created_at: "2023-01-01".to_string(),
13943                tag_immutability: "MUTABLE".to_string(),
13944                encryption_type: "AES256".to_string(),
13945            })
13946            .collect();
13947        app.ecr_state.repositories.selected = 15;
13948
13949        app.handle_action(Action::PageUp);
13950        assert_eq!(app.ecr_state.repositories.selected, 5);
13951    }
13952
13953    #[test]
13954    fn test_ecr_images_ctrl_d_scrolls_down() {
13955        let mut app = test_app();
13956        app.current_service = Service::EcrRepositories;
13957        app.service_selected = true;
13958        app.mode = Mode::Normal;
13959        app.ecr_state.current_repository = Some("repo".to_string());
13960        app.ecr_state.images.items = (0..30)
13961            .map(|i| EcrImage {
13962                tag: format!("tag{}", i),
13963                artifact_type: "container".to_string(),
13964                pushed_at: "2023-01-01T12:00:00Z".to_string(),
13965                size_bytes: 104857600,
13966                uri: format!("uri{}", i),
13967                digest: format!("sha256:{}", i),
13968                last_pull_time: String::new(),
13969            })
13970            .collect();
13971        app.ecr_state.images.selected = 0;
13972
13973        app.handle_action(Action::PageDown);
13974        assert_eq!(app.ecr_state.images.selected, 10);
13975    }
13976
13977    #[test]
13978    fn test_ecr_esc_goes_back_from_images_to_repos() {
13979        let mut app = test_app();
13980        app.current_service = Service::EcrRepositories;
13981        app.service_selected = true;
13982        app.mode = Mode::Normal;
13983        app.ecr_state.current_repository = Some("my-repo".to_string());
13984        app.ecr_state.images.items = vec![EcrImage {
13985            tag: "latest".to_string(),
13986            artifact_type: "container".to_string(),
13987            pushed_at: "2023-01-01T12:00:00Z".to_string(),
13988            size_bytes: 104857600,
13989            uri: "uri".to_string(),
13990            digest: "sha256:abc".to_string(),
13991            last_pull_time: String::new(),
13992        }];
13993
13994        app.handle_action(Action::GoBack);
13995        assert_eq!(app.ecr_state.current_repository, None);
13996        assert!(app.ecr_state.images.items.is_empty());
13997    }
13998
13999    #[test]
14000    fn test_ecr_esc_collapses_expanded_image_first() {
14001        let mut app = test_app();
14002        app.current_service = Service::EcrRepositories;
14003        app.service_selected = true;
14004        app.mode = Mode::Normal;
14005        app.ecr_state.current_repository = Some("my-repo".to_string());
14006        app.ecr_state.images.expanded_item = Some(0);
14007
14008        app.handle_action(Action::GoBack);
14009        assert_eq!(app.ecr_state.images.expanded_item, None);
14010        assert_eq!(
14011            app.ecr_state.current_repository,
14012            Some("my-repo".to_string())
14013        );
14014    }
14015
14016    #[test]
14017    fn test_pagination_with_lowercase_p() {
14018        let mut app = test_app();
14019        app.current_service = Service::EcrRepositories;
14020        app.service_selected = true;
14021        app.mode = Mode::Normal;
14022        app.ecr_state.repositories.items = (0..100)
14023            .map(|i| EcrRepository {
14024                name: format!("repo{}", i),
14025                uri: format!("uri{}", i),
14026                created_at: "2023-01-01".to_string(),
14027                tag_immutability: "MUTABLE".to_string(),
14028                encryption_type: "AES256".to_string(),
14029            })
14030            .collect();
14031
14032        // Type "2" then "p" to go to page 2
14033        app.handle_action(Action::FilterInput('2'));
14034        assert_eq!(app.page_input, "2");
14035
14036        app.handle_action(Action::OpenColumnSelector); // 'p' key
14037        assert_eq!(app.ecr_state.repositories.selected, 50); // Page 2 starts at index 50
14038        assert_eq!(app.page_input, ""); // Should be cleared
14039    }
14040
14041    #[test]
14042    fn test_lowercase_p_without_number_opens_preferences() {
14043        let mut app = test_app();
14044        app.current_service = Service::EcrRepositories;
14045        app.service_selected = true;
14046        app.mode = Mode::Normal;
14047
14048        app.handle_action(Action::OpenColumnSelector); // 'p' key without number
14049        assert_eq!(app.mode, Mode::ColumnSelector);
14050    }
14051
14052    #[test]
14053    fn test_ctrl_o_generates_correct_console_url() {
14054        let mut app = test_app();
14055        app.current_service = Service::EcrRepositories;
14056        app.service_selected = true;
14057        app.mode = Mode::Normal;
14058        app.config.account_id = "123456789012".to_string();
14059
14060        // Test repository list URL
14061        let url = app.get_console_url();
14062        assert!(url.contains("ecr/private-registry/repositories"));
14063        assert!(url.contains("region=us-east-1"));
14064
14065        // Test images URL
14066        app.ecr_state.current_repository = Some("my-repo".to_string());
14067        let url = app.get_console_url();
14068        assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
14069        assert!(url.contains("region=us-east-1"));
14070    }
14071
14072    #[test]
14073    fn test_page_input_display_and_reset() {
14074        let mut app = test_app();
14075        app.current_service = Service::EcrRepositories;
14076        app.service_selected = true;
14077        app.mode = Mode::Normal;
14078        app.ecr_state.repositories.items = (0..100)
14079            .map(|i| EcrRepository {
14080                name: format!("repo{}", i),
14081                uri: format!("uri{}", i),
14082                created_at: "2023-01-01".to_string(),
14083                tag_immutability: "MUTABLE".to_string(),
14084                encryption_type: "AES256".to_string(),
14085            })
14086            .collect();
14087
14088        // Type "2"
14089        app.handle_action(Action::FilterInput('2'));
14090        assert_eq!(app.page_input, "2");
14091
14092        // Press 'p' to go to page 2
14093        app.handle_action(Action::OpenColumnSelector);
14094        assert_eq!(app.page_input, ""); // Should be cleared
14095        assert_eq!(app.ecr_state.repositories.selected, 50); // Page 2 starts at index 50
14096    }
14097
14098    #[test]
14099    fn test_page_navigation_updates_scroll_offset_for_cfn() {
14100        let mut app = test_app();
14101        app.current_service = Service::CloudFormationStacks;
14102        app.service_selected = true;
14103        app.mode = Mode::Normal;
14104        app.cfn_state.table.items = (0..100)
14105            .map(|i| CfnStack {
14106                name: format!("stack-{}", i),
14107                stack_id: format!(
14108                    "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
14109                    i
14110                ),
14111                status: "CREATE_COMPLETE".to_string(),
14112                created_time: "2023-01-01T00:00:00Z".to_string(),
14113                updated_time: "2023-01-01T00:00:00Z".to_string(),
14114                deleted_time: String::new(),
14115                drift_status: "IN_SYNC".to_string(),
14116                last_drift_check_time: String::new(),
14117                status_reason: String::new(),
14118                description: String::new(),
14119                detailed_status: String::new(),
14120                root_stack: String::new(),
14121                parent_stack: String::new(),
14122                termination_protection: false,
14123                iam_role: String::new(),
14124                tags: vec![],
14125                stack_policy: String::new(),
14126                rollback_monitoring_time: String::new(),
14127                rollback_alarms: vec![],
14128                notification_arns: vec![],
14129            })
14130            .collect();
14131
14132        // Type "2" then "p" to go to page 2
14133        app.handle_action(Action::FilterInput('2'));
14134        assert_eq!(app.page_input, "2");
14135
14136        app.handle_action(Action::OpenColumnSelector); // 'p' key
14137        assert_eq!(app.page_input, ""); // Should be cleared
14138
14139        // Verify both selected and scroll_offset are updated
14140        let page_size = app.cfn_state.table.page_size.value();
14141        let expected_offset = page_size; // Page 2 starts at page_size
14142        assert_eq!(app.cfn_state.table.selected, expected_offset);
14143        assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
14144
14145        // Verify pagination display shows page 2
14146        let current_page = app.cfn_state.table.scroll_offset / page_size;
14147        assert_eq!(
14148            current_page, 1,
14149            "2p should go to page 2 (0-indexed as 1), not page 3"
14150        ); // 0-indexed, so page 2 is index 1
14151    }
14152
14153    #[test]
14154    fn test_3p_goes_to_page_3_not_page_5() {
14155        let mut app = test_app();
14156        app.current_service = Service::CloudFormationStacks;
14157        app.service_selected = true;
14158        app.mode = Mode::Normal;
14159        app.cfn_state.table.items = (0..200)
14160            .map(|i| CfnStack {
14161                name: format!("stack-{}", i),
14162                stack_id: format!(
14163                    "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
14164                    i
14165                ),
14166                status: "CREATE_COMPLETE".to_string(),
14167                created_time: "2023-01-01T00:00:00Z".to_string(),
14168                updated_time: "2023-01-01T00:00:00Z".to_string(),
14169                deleted_time: String::new(),
14170                drift_status: "IN_SYNC".to_string(),
14171                last_drift_check_time: String::new(),
14172                status_reason: String::new(),
14173                description: String::new(),
14174                detailed_status: String::new(),
14175                root_stack: String::new(),
14176                parent_stack: String::new(),
14177                termination_protection: false,
14178                iam_role: String::new(),
14179                tags: vec![],
14180                stack_policy: String::new(),
14181                rollback_monitoring_time: String::new(),
14182                rollback_alarms: vec![],
14183                notification_arns: vec![],
14184            })
14185            .collect();
14186
14187        // Type "3" then "p" to go to page 3
14188        app.handle_action(Action::FilterInput('3'));
14189        app.handle_action(Action::OpenColumnSelector);
14190
14191        let page_size = app.cfn_state.table.page_size.value();
14192        let current_page = app.cfn_state.table.scroll_offset / page_size;
14193        assert_eq!(
14194            current_page, 2,
14195            "3p should go to page 3 (0-indexed as 2), not page 5"
14196        );
14197        assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
14198    }
14199
14200    #[test]
14201    fn test_log_streams_page_navigation_uses_correct_page_size() {
14202        let mut app = test_app();
14203        app.current_service = Service::CloudWatchLogGroups;
14204        app.view_mode = ViewMode::Detail;
14205        app.service_selected = true;
14206        app.mode = Mode::Normal;
14207        app.log_groups_state.log_streams = (0..100)
14208            .map(|i| LogStream {
14209                name: format!("stream-{}", i),
14210                creation_time: None,
14211                last_event_time: None,
14212            })
14213            .collect();
14214
14215        // Type "2" then "p" to go to page 2
14216        app.handle_action(Action::FilterInput('2'));
14217        app.handle_action(Action::OpenColumnSelector);
14218
14219        // Should go to page 2 (page index 1)
14220        assert_eq!(app.log_groups_state.stream_current_page, 1);
14221        assert_eq!(app.log_groups_state.selected_stream, 0);
14222
14223        // Verify pagination display shows page 2 (not page 3)
14224        assert_eq!(
14225            app.log_groups_state.stream_current_page, 1,
14226            "2p should go to page 2 (0-indexed as 1), not page 3"
14227        );
14228    }
14229
14230    #[test]
14231    fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
14232        let mut app = test_app();
14233        app.current_service = Service::EcrRepositories;
14234        app.service_selected = true;
14235        app.mode = Mode::Normal;
14236        app.ecr_state.repositories.page_size = PageSize::TwentyFive; // Set to 25
14237        app.ecr_state.repositories.items = (0..100)
14238            .map(|i| EcrRepository {
14239                name: format!("repo{}", i),
14240                uri: format!("uri{}", i),
14241                created_at: "2023-01-01".to_string(),
14242                tag_immutability: "MUTABLE".to_string(),
14243                encryption_type: "AES256".to_string(),
14244            })
14245            .collect();
14246
14247        // Type "3" then "p" to go to page 3
14248        app.handle_action(Action::FilterInput('3'));
14249        app.handle_action(Action::OpenColumnSelector);
14250
14251        // With page_size=25, page 3 starts at index 50
14252        assert_eq!(app.ecr_state.repositories.selected, 50);
14253
14254        let page_size = app.ecr_state.repositories.page_size.value();
14255        let current_page = app.ecr_state.repositories.selected / page_size;
14256        assert_eq!(
14257            current_page, 2,
14258            "3p with page_size=25 should go to page 3 (0-indexed as 2)"
14259        );
14260    }
14261
14262    #[test]
14263    fn test_page_navigation_updates_scroll_offset_for_alarms() {
14264        let mut app = test_app();
14265        app.current_service = Service::CloudWatchAlarms;
14266        app.service_selected = true;
14267        app.mode = Mode::Normal;
14268        app.alarms_state.table.items = (0..100)
14269            .map(|i| Alarm {
14270                name: format!("alarm-{}", i),
14271                state: "OK".to_string(),
14272                state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
14273                description: String::new(),
14274                metric_name: "CPUUtilization".to_string(),
14275                namespace: "AWS/EC2".to_string(),
14276                statistic: "Average".to_string(),
14277                period: 300,
14278                comparison_operator: "GreaterThanThreshold".to_string(),
14279                threshold: 80.0,
14280                actions_enabled: true,
14281                state_reason: String::new(),
14282                resource: String::new(),
14283                dimensions: String::new(),
14284                expression: String::new(),
14285                alarm_type: "MetricAlarm".to_string(),
14286                cross_account: String::new(),
14287            })
14288            .collect();
14289
14290        // Type "2" then "p" to go to page 2
14291        app.handle_action(Action::FilterInput('2'));
14292        app.handle_action(Action::OpenColumnSelector);
14293
14294        // Verify both selected and scroll_offset are updated
14295        let page_size = app.alarms_state.table.page_size.value();
14296        let expected_offset = page_size; // Page 2 starts at page_size
14297        assert_eq!(app.alarms_state.table.selected, expected_offset);
14298        assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
14299    }
14300
14301    #[test]
14302    fn test_ecr_pagination_with_65_repos() {
14303        let mut app = test_app();
14304        app.current_service = Service::EcrRepositories;
14305        app.service_selected = true;
14306        app.mode = Mode::Normal;
14307        app.ecr_state.repositories.items = (0..65)
14308            .map(|i| EcrRepository {
14309                name: format!("repo{:02}", i),
14310                uri: format!("uri{}", i),
14311                created_at: "2023-01-01".to_string(),
14312                tag_immutability: "MUTABLE".to_string(),
14313                encryption_type: "AES256".to_string(),
14314            })
14315            .collect();
14316
14317        // Page 1: items 0-49 (50 items)
14318        assert_eq!(app.ecr_state.repositories.selected, 0);
14319        let page_size = 50;
14320        let current_page = app.ecr_state.repositories.selected / page_size;
14321        assert_eq!(current_page, 0);
14322
14323        // Go to page 2
14324        app.handle_action(Action::FilterInput('2'));
14325        app.handle_action(Action::OpenColumnSelector);
14326        assert_eq!(app.ecr_state.repositories.selected, 50);
14327
14328        // Page 2: items 50-64 (15 items)
14329        let current_page = app.ecr_state.repositories.selected / page_size;
14330        assert_eq!(current_page, 1);
14331    }
14332
14333    #[test]
14334    fn test_ecr_repos_input_focus_tab_cycling() {
14335        let mut app = test_app();
14336        app.current_service = Service::EcrRepositories;
14337        app.service_selected = true;
14338        app.mode = Mode::FilterInput;
14339        app.ecr_state.input_focus = InputFocus::Filter;
14340
14341        // Tab should cycle to Pagination
14342        app.handle_action(Action::NextFilterFocus);
14343        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
14344
14345        // Tab again should cycle back to Input
14346        app.handle_action(Action::NextFilterFocus);
14347        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
14348
14349        // Shift+Tab should cycle backwards to Pagination
14350        app.handle_action(Action::PrevFilterFocus);
14351        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
14352
14353        // Shift+Tab again should cycle back to Input
14354        app.handle_action(Action::PrevFilterFocus);
14355        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
14356    }
14357
14358    #[test]
14359    fn test_ecr_images_column_toggle_not_off_by_one() {
14360        use crate::ecr::image::Column as ImageColumn;
14361        let mut app = test_app();
14362        app.current_service = Service::EcrRepositories;
14363        app.service_selected = true;
14364        app.mode = Mode::ColumnSelector;
14365        app.ecr_state.current_repository = Some("test-repo".to_string());
14366
14367        // Start with all columns visible
14368        app.ecr_image_visible_column_ids = ImageColumn::ids();
14369        let initial_count = app.ecr_image_visible_column_ids.len();
14370
14371        // Select first column (index 0) and toggle it
14372        app.column_selector_index = 0;
14373        app.handle_action(Action::ToggleColumn);
14374
14375        // First column should be removed
14376        assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
14377        assert!(!app
14378            .ecr_image_visible_column_ids
14379            .contains(&ImageColumn::Tag.id()));
14380
14381        // Toggle it back
14382        app.handle_action(Action::ToggleColumn);
14383        assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
14384        assert!(app
14385            .ecr_image_visible_column_ids
14386            .contains(&ImageColumn::Tag.id()));
14387    }
14388
14389    #[test]
14390    fn test_ecr_repos_column_toggle_works() {
14391        let mut app = test_app();
14392        app.current_service = Service::EcrRepositories;
14393        app.service_selected = true;
14394        app.mode = Mode::ColumnSelector;
14395        app.ecr_state.current_repository = None;
14396
14397        // Start with all columns visible
14398        app.ecr_repo_visible_column_ids = EcrColumn::ids();
14399        let initial_count = app.ecr_repo_visible_column_ids.len();
14400
14401        // Select first column (index 1, since 0 is header) and toggle it
14402        app.column_selector_index = 1;
14403        app.handle_action(Action::ToggleColumn);
14404
14405        // First column should be removed
14406        assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
14407        assert!(!app
14408            .ecr_repo_visible_column_ids
14409            .contains(&EcrColumn::Name.id()));
14410
14411        // Toggle it back
14412        app.handle_action(Action::ToggleColumn);
14413        assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
14414        assert!(app
14415            .ecr_repo_visible_column_ids
14416            .contains(&EcrColumn::Name.id()));
14417    }
14418
14419    #[test]
14420    fn test_ecr_repos_pagination_left_right_navigation() {
14421        use crate::ecr::repo::Repository as EcrRepository;
14422        let mut app = test_app();
14423        app.current_service = Service::EcrRepositories;
14424        app.service_selected = true;
14425        app.mode = Mode::FilterInput;
14426        app.ecr_state.input_focus = InputFocus::Pagination;
14427
14428        // Create 150 repos (3 pages with page size 50)
14429        app.ecr_state.repositories.items = (0..150)
14430            .map(|i| EcrRepository {
14431                name: format!("repo{:03}", i),
14432                uri: format!("uri{}", i),
14433                created_at: "2023-01-01".to_string(),
14434                tag_immutability: "MUTABLE".to_string(),
14435                encryption_type: "AES256".to_string(),
14436            })
14437            .collect();
14438
14439        // Start on page 1 (index 0)
14440        app.ecr_state.repositories.selected = 0;
14441        eprintln!(
14442            "Initial: selected={}, focus={:?}, mode={:?}",
14443            app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
14444        );
14445
14446        // Right arrow (PageDown) should go to page 2
14447        app.handle_action(Action::PageDown);
14448        eprintln!(
14449            "After PageDown: selected={}",
14450            app.ecr_state.repositories.selected
14451        );
14452        assert_eq!(app.ecr_state.repositories.selected, 50);
14453
14454        // Right arrow again should go to page 3
14455        app.handle_action(Action::PageDown);
14456        eprintln!(
14457            "After 2nd PageDown: selected={}",
14458            app.ecr_state.repositories.selected
14459        );
14460        assert_eq!(app.ecr_state.repositories.selected, 100);
14461
14462        // Right arrow at last page should stay at last page
14463        app.handle_action(Action::PageDown);
14464        eprintln!(
14465            "After 3rd PageDown: selected={}",
14466            app.ecr_state.repositories.selected
14467        );
14468        assert_eq!(app.ecr_state.repositories.selected, 100);
14469
14470        // Left arrow (PageUp) should go back to page 2
14471        app.handle_action(Action::PageUp);
14472        eprintln!(
14473            "After PageUp: selected={}",
14474            app.ecr_state.repositories.selected
14475        );
14476        assert_eq!(app.ecr_state.repositories.selected, 50);
14477
14478        // Left arrow again should go to page 1
14479        app.handle_action(Action::PageUp);
14480        eprintln!(
14481            "After 2nd PageUp: selected={}",
14482            app.ecr_state.repositories.selected
14483        );
14484        assert_eq!(app.ecr_state.repositories.selected, 0);
14485
14486        // Left arrow at first page should stay at first page
14487        app.handle_action(Action::PageUp);
14488        eprintln!(
14489            "After 3rd PageUp: selected={}",
14490            app.ecr_state.repositories.selected
14491        );
14492        assert_eq!(app.ecr_state.repositories.selected, 0);
14493    }
14494
14495    #[test]
14496    fn test_ecr_repos_filter_input_when_input_focused() {
14497        use crate::ecr::repo::Repository as EcrRepository;
14498        let mut app = test_app();
14499        app.current_service = Service::EcrRepositories;
14500        app.service_selected = true;
14501        app.mode = Mode::FilterInput;
14502        app.ecr_state.input_focus = InputFocus::Filter;
14503
14504        // Create some repos
14505        app.ecr_state.repositories.items = vec![
14506            EcrRepository {
14507                name: "test-repo".to_string(),
14508                uri: "uri1".to_string(),
14509                created_at: "2023-01-01".to_string(),
14510                tag_immutability: "MUTABLE".to_string(),
14511                encryption_type: "AES256".to_string(),
14512            },
14513            EcrRepository {
14514                name: "prod-repo".to_string(),
14515                uri: "uri2".to_string(),
14516                created_at: "2023-01-01".to_string(),
14517                tag_immutability: "MUTABLE".to_string(),
14518                encryption_type: "AES256".to_string(),
14519            },
14520        ];
14521
14522        // When input is focused, typing should add to filter
14523        assert_eq!(app.ecr_state.repositories.filter, "");
14524        app.handle_action(Action::FilterInput('t'));
14525        assert_eq!(app.ecr_state.repositories.filter, "t");
14526        app.handle_action(Action::FilterInput('e'));
14527        assert_eq!(app.ecr_state.repositories.filter, "te");
14528        app.handle_action(Action::FilterInput('s'));
14529        assert_eq!(app.ecr_state.repositories.filter, "tes");
14530        app.handle_action(Action::FilterInput('t'));
14531        assert_eq!(app.ecr_state.repositories.filter, "test");
14532    }
14533
14534    #[test]
14535    fn test_ecr_repos_digit_input_when_pagination_focused() {
14536        use crate::ecr::repo::Repository as EcrRepository;
14537        let mut app = test_app();
14538        app.current_service = Service::EcrRepositories;
14539        app.service_selected = true;
14540        app.mode = Mode::FilterInput;
14541        app.ecr_state.input_focus = InputFocus::Pagination;
14542
14543        // Create some repos
14544        app.ecr_state.repositories.items = vec![EcrRepository {
14545            name: "test-repo".to_string(),
14546            uri: "uri1".to_string(),
14547            created_at: "2023-01-01".to_string(),
14548            tag_immutability: "MUTABLE".to_string(),
14549            encryption_type: "AES256".to_string(),
14550        }];
14551
14552        // When pagination is focused, digits should go to page_input, not filter
14553        assert_eq!(app.ecr_state.repositories.filter, "");
14554        assert_eq!(app.page_input, "");
14555        app.handle_action(Action::FilterInput('2'));
14556        assert_eq!(app.ecr_state.repositories.filter, "");
14557        assert_eq!(app.page_input, "2");
14558
14559        // Non-digits should not be added to either
14560        app.handle_action(Action::FilterInput('a'));
14561        assert_eq!(app.ecr_state.repositories.filter, "");
14562        assert_eq!(app.page_input, "2");
14563    }
14564
14565    #[test]
14566    fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
14567        use crate::ecr::repo::Repository as EcrRepository;
14568        let mut app = test_app();
14569        app.current_service = Service::EcrRepositories;
14570        app.service_selected = true;
14571        app.mode = Mode::FilterInput;
14572        app.ecr_state.input_focus = InputFocus::Filter;
14573
14574        // Create 150 repos (3 pages)
14575        app.ecr_state.repositories.items = (0..150)
14576            .map(|i| EcrRepository {
14577                name: format!("repo{:03}", i),
14578                uri: format!("uri{}", i),
14579                created_at: "2023-01-01".to_string(),
14580                tag_immutability: "MUTABLE".to_string(),
14581                encryption_type: "AES256".to_string(),
14582            })
14583            .collect();
14584
14585        // Start on page 1
14586        app.ecr_state.repositories.selected = 0;
14587
14588        // When input is focused, left/right should scroll table (not change pages)
14589        app.handle_action(Action::PageDown);
14590        assert_eq!(
14591            app.ecr_state.repositories.selected, 10,
14592            "Should scroll down by 10"
14593        );
14594
14595        app.handle_action(Action::PageUp);
14596        assert_eq!(
14597            app.ecr_state.repositories.selected, 0,
14598            "Should scroll back up"
14599        );
14600    }
14601
14602    #[test]
14603    fn test_ecr_repos_pagination_control_actually_works() {
14604        use crate::ecr::repo::Repository as EcrRepository;
14605
14606        // Test that verifies the exact conditions needed for pagination to work
14607        let mut app = test_app();
14608        app.current_service = Service::EcrRepositories;
14609        app.service_selected = true;
14610        app.mode = Mode::FilterInput;
14611        app.ecr_state.current_repository = None;
14612        app.ecr_state.input_focus = InputFocus::Pagination;
14613
14614        // Create 100 repos (2 pages with page size 50)
14615        app.ecr_state.repositories.items = (0..100)
14616            .map(|i| EcrRepository {
14617                name: format!("repo{:03}", i),
14618                uri: format!("uri{}", i),
14619                created_at: "2023-01-01".to_string(),
14620                tag_immutability: "MUTABLE".to_string(),
14621                encryption_type: "AES256".to_string(),
14622            })
14623            .collect();
14624
14625        app.ecr_state.repositories.selected = 0;
14626
14627        // Verify all conditions are met
14628        assert_eq!(app.mode, Mode::FilterInput);
14629        assert_eq!(app.current_service, Service::EcrRepositories);
14630        assert_eq!(app.ecr_state.current_repository, None);
14631        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
14632
14633        // Now test pagination
14634        app.handle_action(Action::PageDown);
14635        assert_eq!(
14636            app.ecr_state.repositories.selected, 50,
14637            "PageDown should move to page 2"
14638        );
14639
14640        app.handle_action(Action::PageUp);
14641        assert_eq!(
14642            app.ecr_state.repositories.selected, 0,
14643            "PageUp should move back to page 1"
14644        );
14645    }
14646
14647    #[test]
14648    fn test_ecr_repos_start_filter_resets_focus_to_input() {
14649        let mut app = test_app();
14650        app.current_service = Service::EcrRepositories;
14651        app.service_selected = true;
14652        app.mode = Mode::Normal;
14653        app.ecr_state.current_repository = None;
14654
14655        // Set focus to Pagination
14656        app.ecr_state.input_focus = InputFocus::Pagination;
14657
14658        // Start filter mode
14659        app.handle_action(Action::StartFilter);
14660
14661        // Should reset to Input focus
14662        assert_eq!(app.mode, Mode::FilterInput);
14663        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
14664    }
14665
14666    #[test]
14667    fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
14668        use crate::ecr::repo::Repository as EcrRepository;
14669
14670        let mut app = test_app();
14671        app.current_service = Service::EcrRepositories;
14672        app.service_selected = true;
14673        app.mode = Mode::Normal;
14674        app.ecr_state.current_repository = None;
14675
14676        // Create 100 repos (2 pages)
14677        app.ecr_state.repositories.items = (0..100)
14678            .map(|i| EcrRepository {
14679                name: format!("repo{:03}", i),
14680                uri: format!("uri{}", i),
14681                created_at: "2023-01-01".to_string(),
14682                tag_immutability: "MUTABLE".to_string(),
14683                encryption_type: "AES256".to_string(),
14684            })
14685            .collect();
14686
14687        app.ecr_state.repositories.selected = 0;
14688
14689        // User presses 'i' to enter filter mode
14690        app.handle_action(Action::StartFilter);
14691        assert_eq!(app.mode, Mode::FilterInput);
14692        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
14693
14694        // User presses Tab to switch to pagination
14695        app.handle_action(Action::NextFilterFocus);
14696        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
14697
14698        // User presses right arrow (PageDown)
14699        eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
14700            app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
14701        app.handle_action(Action::PageDown);
14702        eprintln!(
14703            "After PageDown: selected={}",
14704            app.ecr_state.repositories.selected
14705        );
14706
14707        // Should move to page 2
14708        assert_eq!(
14709            app.ecr_state.repositories.selected, 50,
14710            "Right arrow should move to page 2"
14711        );
14712
14713        // User presses left arrow (PageUp)
14714        app.handle_action(Action::PageUp);
14715        assert_eq!(
14716            app.ecr_state.repositories.selected, 0,
14717            "Left arrow should move back to page 1"
14718        );
14719    }
14720
14721    #[test]
14722    fn test_apig_filter_input() {
14723        let mut app = test_app();
14724        app.current_service = Service::ApiGatewayApis;
14725        app.service_selected = true;
14726        app.mode = Mode::FilterInput;
14727
14728        app.handle_action(Action::FilterInput('t'));
14729        app.handle_action(Action::FilterInput('e'));
14730        app.handle_action(Action::FilterInput('s'));
14731        app.handle_action(Action::FilterInput('t'));
14732        assert_eq!(app.apig_state.apis.filter, "test");
14733
14734        app.handle_action(Action::FilterBackspace);
14735        assert_eq!(app.apig_state.apis.filter, "tes");
14736    }
14737
14738    #[test]
14739    fn test_apig_start_filter_enters_filter_mode() {
14740        let mut app = test_app();
14741        app.current_service = Service::ApiGatewayApis;
14742        app.service_selected = true;
14743        app.mode = Mode::Normal;
14744
14745        app.handle_action(Action::StartFilter);
14746        assert_eq!(app.mode, Mode::FilterInput);
14747        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
14748    }
14749
14750    #[test]
14751    fn test_apig_input_focus_cycles_with_tab() {
14752        let mut app = test_app();
14753        app.current_service = Service::ApiGatewayApis;
14754        app.service_selected = true;
14755        app.mode = Mode::FilterInput;
14756        app.apig_state.input_focus = InputFocus::Filter;
14757
14758        // Tab cycles from Filter to Pagination
14759        app.handle_action(Action::NextFilterFocus);
14760        assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
14761
14762        // Tab cycles back to Filter
14763        app.handle_action(Action::NextFilterFocus);
14764        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
14765    }
14766
14767    #[test]
14768    fn test_apig_input_focus_cycles_with_shift_tab() {
14769        let mut app = test_app();
14770        app.current_service = Service::ApiGatewayApis;
14771        app.service_selected = true;
14772        app.mode = Mode::FilterInput;
14773        app.apig_state.input_focus = InputFocus::Filter;
14774
14775        // Shift+Tab cycles from Filter to Pagination (backward)
14776        app.handle_action(Action::PrevFilterFocus);
14777        assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
14778
14779        // Shift+Tab cycles back to Filter
14780        app.handle_action(Action::PrevFilterFocus);
14781        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
14782    }
14783
14784    #[test]
14785    fn test_apig_exact_user_flow_i_tab_filter() {
14786        let mut app = test_app();
14787        app.current_service = Service::ApiGatewayApis;
14788        app.service_selected = true;
14789        app.mode = Mode::Normal;
14790
14791        // Create test APIs
14792        app.apig_state.apis.items = vec![
14793            crate::apig::api::RestApi {
14794                id: "api1".to_string(),
14795                name: "test-api".to_string(),
14796                description: "Test API".to_string(),
14797                created_date: "2023-01-01".to_string(),
14798                api_key_source: "HEADER".to_string(),
14799                endpoint_configuration: "REGIONAL".to_string(),
14800                protocol_type: "REST".to_string(),
14801                disable_execute_api_endpoint: false,
14802                status: "AVAILABLE".to_string(),
14803            },
14804            crate::apig::api::RestApi {
14805                id: "api2".to_string(),
14806                name: "prod-api".to_string(),
14807                description: "Production API".to_string(),
14808                created_date: "2023-01-02".to_string(),
14809                api_key_source: "HEADER".to_string(),
14810                endpoint_configuration: "REGIONAL".to_string(),
14811                protocol_type: "REST".to_string(),
14812                disable_execute_api_endpoint: false,
14813                status: "AVAILABLE".to_string(),
14814            },
14815        ];
14816
14817        // User presses 'i' to enter filter mode
14818        app.handle_action(Action::StartFilter);
14819        assert_eq!(app.mode, Mode::FilterInput);
14820        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
14821
14822        // User types "test"
14823        app.handle_action(Action::FilterInput('t'));
14824        app.handle_action(Action::FilterInput('e'));
14825        app.handle_action(Action::FilterInput('s'));
14826        app.handle_action(Action::FilterInput('t'));
14827        assert_eq!(app.apig_state.apis.filter, "test");
14828
14829        // User presses Tab to switch to pagination
14830        app.handle_action(Action::NextFilterFocus);
14831        assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
14832
14833        // User presses Tab again to go back to filter
14834        app.handle_action(Action::NextFilterFocus);
14835        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
14836    }
14837
14838    #[test]
14839    fn test_apig_row_expansion() {
14840        let mut app = test_app();
14841        app.current_service = Service::ApiGatewayApis;
14842        app.service_selected = true;
14843        app.apig_state.apis.items = vec![crate::apig::api::RestApi {
14844            id: "api1".to_string(),
14845            name: "test-api".to_string(),
14846            description: "Test API".to_string(),
14847            created_date: "2023-01-01".to_string(),
14848            api_key_source: "HEADER".to_string(),
14849            endpoint_configuration: "REGIONAL".to_string(),
14850            protocol_type: "REST".to_string(),
14851            disable_execute_api_endpoint: false,
14852            status: "AVAILABLE".to_string(),
14853        }];
14854        app.apig_state.apis.selected = 0;
14855
14856        assert_eq!(app.apig_state.apis.expanded_item, None);
14857
14858        // Right arrow (NextPane) should NOT expand rows
14859        app.handle_action(Action::NextPane);
14860        assert_eq!(app.apig_state.apis.expanded_item, None);
14861
14862        // Manually expand using ExpandRow action
14863        app.handle_action(Action::ExpandRow);
14864        assert_eq!(app.apig_state.apis.expanded_item, Some(0));
14865
14866        // Right arrow should NOT collapse expanded row
14867        app.handle_action(Action::NextPane);
14868        assert_eq!(app.apig_state.apis.expanded_item, Some(0));
14869
14870        // Left arrow (PrevPane) should collapse
14871        app.handle_action(Action::PrevPane);
14872        assert_eq!(app.apig_state.apis.expanded_item, None);
14873    }
14874
14875    #[test]
14876    fn test_apig_column_preferences() {
14877        let mut app = test_app();
14878        app.current_service = Service::ApiGatewayApis;
14879        app.service_selected = true;
14880
14881        // All columns should be visible by default
14882        let initial_count = app.apig_api_visible_column_ids.len();
14883        assert_eq!(initial_count, app.apig_api_column_ids.len());
14884
14885        // Open preferences
14886        app.handle_action(Action::OpenColumnSelector);
14887        assert_eq!(app.mode, Mode::ColumnSelector);
14888
14889        // Toggle first column (index 1, since 0 is header)
14890        app.column_selector_index = 1;
14891        let first_col = app.apig_api_column_ids[0];
14892        assert!(app.apig_api_visible_column_ids.contains(&first_col));
14893
14894        app.handle_action(Action::ToggleColumn);
14895
14896        // First column should now be hidden
14897        assert!(!app.apig_api_visible_column_ids.contains(&first_col));
14898        assert_eq!(app.apig_api_visible_column_ids.len(), initial_count - 1);
14899
14900        // Toggle it back
14901        app.handle_action(Action::ToggleColumn);
14902        assert!(app.apig_api_visible_column_ids.contains(&first_col));
14903        assert_eq!(app.apig_api_visible_column_ids.len(), initial_count);
14904    }
14905
14906    #[test]
14907    fn test_apig_page_size_preferences() {
14908        use crate::common::PageSize;
14909
14910        let mut app = test_app();
14911        app.current_service = Service::ApiGatewayApis;
14912        app.service_selected = true;
14913
14914        // Default page size
14915        assert_eq!(app.apig_state.apis.page_size, PageSize::Fifty);
14916
14917        // Open preferences
14918        app.handle_action(Action::OpenColumnSelector);
14919
14920        // Page size options start after: header (1) + columns (8) + blank line (1) + "Page Size" header (1) = 11
14921        // So indices are: 11 (header), 12 (10), 13 (25), 14 (50), 15 (100)
14922        // But the code uses: column_count + 3, +4, +5, +6
14923        // With 8 columns: 11, 12, 13, 14
14924        app.column_selector_index = app.apig_api_column_ids.len() + 3;
14925        app.handle_action(Action::ToggleColumn);
14926        assert_eq!(app.apig_state.apis.page_size, PageSize::Ten);
14927
14928        // Select page size 100
14929        app.column_selector_index = app.apig_api_column_ids.len() + 6;
14930        app.handle_action(Action::ToggleColumn);
14931        assert_eq!(app.apig_state.apis.page_size, PageSize::OneHundred);
14932    }
14933
14934    #[test]
14935    fn test_apig_preferences_skip_blank_row() {
14936        let mut app = test_app();
14937        app.current_service = Service::ApiGatewayApis;
14938        app.service_selected = true;
14939        app.mode = Mode::ColumnSelector;
14940
14941        // Start at last column (index 8)
14942        app.column_selector_index = 8;
14943
14944        // Next should skip blank row (9) and go to page size header (10)
14945        app.handle_action(Action::NextItem);
14946        assert_eq!(app.column_selector_index, 10);
14947
14948        // Prev should skip blank row (9) and go back to last column (8)
14949        app.handle_action(Action::PrevItem);
14950        assert_eq!(app.column_selector_index, 8);
14951    }
14952
14953    #[test]
14954    fn test_apig_preferences_ctrl_d_skip_blank_row() {
14955        let mut app = test_app();
14956        app.current_service = Service::ApiGatewayApis;
14957        app.service_selected = true;
14958        app.mode = Mode::ColumnSelector;
14959
14960        // Start at index 0
14961        app.column_selector_index = 0;
14962
14963        // Ctrl+D (PageDown) by 10 would land on blank row (9), should skip to 10
14964        app.handle_action(Action::PageDown);
14965        assert_eq!(app.column_selector_index, 10);
14966
14967        // Ctrl+U (PageUp) by 10 would land on 0
14968        app.handle_action(Action::PageUp);
14969        assert_eq!(app.column_selector_index, 0);
14970    }
14971
14972    #[test]
14973    fn test_apig_preferences_ctrl_u_skip_blank_row() {
14974        let mut app = test_app();
14975        app.current_service = Service::ApiGatewayApis;
14976        app.service_selected = true;
14977        app.mode = Mode::ColumnSelector;
14978
14979        // Start at page size header (index 10)
14980        app.column_selector_index = 10;
14981
14982        // Ctrl+U (PageUp) by 10 would land on 0
14983        app.handle_action(Action::PageUp);
14984        assert_eq!(app.column_selector_index, 0);
14985
14986        // Go to index 19 (if it exists, otherwise use 14 which is max)
14987        app.column_selector_index = 14; // Max index for APIG
14988
14989        // Ctrl+U (PageUp) by 10 would land on 4
14990        app.handle_action(Action::PageUp);
14991        assert_eq!(app.column_selector_index, 4);
14992    }
14993
14994    #[test]
14995    fn test_apig_preferences_tab_cycles_sections() {
14996        let mut app = test_app();
14997        app.current_service = Service::ApiGatewayApis;
14998        app.service_selected = true;
14999        app.mode = Mode::ColumnSelector;
15000
15001        // Start at columns header (0)
15002        app.column_selector_index = 0;
15003
15004        // Tab should jump to page size section (column_count + 2 = 10)
15005        app.handle_action(Action::NextPreferences);
15006        assert_eq!(app.column_selector_index, 10);
15007
15008        // Tab again should wrap back to columns (0)
15009        app.handle_action(Action::NextPreferences);
15010        assert_eq!(app.column_selector_index, 0);
15011    }
15012
15013    #[test]
15014    fn test_apig_preferences_shift_tab_cycles_sections() {
15015        let mut app = test_app();
15016        app.current_service = Service::ApiGatewayApis;
15017        app.service_selected = true;
15018        app.mode = Mode::ColumnSelector;
15019
15020        // Start at columns header (0)
15021        app.column_selector_index = 0;
15022
15023        // Shift+Tab should jump to page size section (10)
15024        app.handle_action(Action::PrevPreferences);
15025        assert_eq!(app.column_selector_index, 10);
15026
15027        // Shift+Tab again should wrap back to columns (0)
15028        app.handle_action(Action::PrevPreferences);
15029        assert_eq!(app.column_selector_index, 0);
15030    }
15031
15032    #[test]
15033    fn test_apig_arrow_navigation() {
15034        let mut app = test_app();
15035        app.current_service = Service::ApiGatewayApis;
15036        app.service_selected = true;
15037        app.mode = Mode::Normal; // Ensure we're in Normal mode
15038        app.apig_state.apis.filter = String::new(); // Ensure filter is empty
15039        app.apig_state.apis.items = vec![
15040            crate::apig::api::RestApi {
15041                id: "api1".to_string(),
15042                name: "test-api-1".to_string(),
15043                description: "Test API 1".to_string(),
15044                created_date: "2023-01-01".to_string(),
15045                api_key_source: "HEADER".to_string(),
15046                endpoint_configuration: "REGIONAL".to_string(),
15047                protocol_type: "REST".to_string(),
15048                disable_execute_api_endpoint: false,
15049                status: "AVAILABLE".to_string(),
15050            },
15051            crate::apig::api::RestApi {
15052                id: "api2".to_string(),
15053                name: "test-api-2".to_string(),
15054                description: "Test API 2".to_string(),
15055                created_date: "2023-01-02".to_string(),
15056                api_key_source: "HEADER".to_string(),
15057                endpoint_configuration: "REGIONAL".to_string(),
15058                protocol_type: "HTTP".to_string(),
15059                disable_execute_api_endpoint: false,
15060                status: "AVAILABLE".to_string(),
15061            },
15062        ];
15063        app.apig_state.apis.selected = 0;
15064
15065        // Down arrow should move to next item
15066        app.handle_action(Action::NextItem);
15067        assert_eq!(app.apig_state.apis.selected, 1);
15068
15069        // Up arrow should move to previous item
15070        app.handle_action(Action::PrevItem);
15071        assert_eq!(app.apig_state.apis.selected, 0);
15072    }
15073
15074    #[test]
15075    fn test_apig_collapse_row() {
15076        let mut app = test_app();
15077        app.current_service = Service::ApiGatewayApis;
15078        app.service_selected = true;
15079        app.apig_state.apis.items = vec![crate::apig::api::RestApi {
15080            id: "api1".to_string(),
15081            name: "test-api".to_string(),
15082            description: "Test API".to_string(),
15083            created_date: "2023-01-01".to_string(),
15084            api_key_source: "HEADER".to_string(),
15085            endpoint_configuration: "REGIONAL".to_string(),
15086            protocol_type: "REST".to_string(),
15087            disable_execute_api_endpoint: false,
15088            status: "AVAILABLE".to_string(),
15089        }];
15090        app.apig_state.apis.selected = 0;
15091        app.apig_state.apis.expanded_item = Some(0);
15092
15093        // Left arrow should collapse
15094        app.handle_action(Action::CollapseRow);
15095        assert_eq!(app.apig_state.apis.expanded_item, None);
15096    }
15097
15098    #[test]
15099    fn test_apig_filter_resets_selection() {
15100        let mut app = test_app();
15101        app.current_service = Service::ApiGatewayApis;
15102        app.service_selected = true;
15103        app.mode = Mode::FilterInput;
15104        app.apig_state.input_focus = InputFocus::Filter;
15105        app.apig_state.apis.items = vec![
15106            crate::apig::api::RestApi {
15107                id: "api1".to_string(),
15108                name: "alpha-api".to_string(),
15109                description: "Alpha API".to_string(),
15110                created_date: "2023-01-01".to_string(),
15111                api_key_source: "HEADER".to_string(),
15112                endpoint_configuration: "REGIONAL".to_string(),
15113                protocol_type: "REST".to_string(),
15114                disable_execute_api_endpoint: false,
15115                status: "AVAILABLE".to_string(),
15116            },
15117            crate::apig::api::RestApi {
15118                id: "api2".to_string(),
15119                name: "beta-api".to_string(),
15120                description: "Beta API".to_string(),
15121                created_date: "2023-01-02".to_string(),
15122                api_key_source: "HEADER".to_string(),
15123                endpoint_configuration: "REGIONAL".to_string(),
15124                protocol_type: "HTTP".to_string(),
15125                disable_execute_api_endpoint: false,
15126                status: "AVAILABLE".to_string(),
15127            },
15128        ];
15129
15130        // Select second item and expand it
15131        app.apig_state.apis.selected = 1;
15132        app.apig_state.apis.expanded_item = Some(1);
15133
15134        // Type a filter character
15135        app.handle_action(Action::FilterInput('a'));
15136
15137        // Selection and expansion should be reset
15138        assert_eq!(app.apig_state.apis.selected, 0);
15139        assert_eq!(app.apig_state.apis.expanded_item, None);
15140        assert_eq!(app.apig_state.apis.filter, "a");
15141    }
15142
15143    #[test]
15144    fn test_service_picker_starts_in_normal_mode() {
15145        let app = test_app();
15146        assert_eq!(app.mode, Mode::ServicePicker);
15147        assert!(!app.service_picker.filter_active);
15148    }
15149
15150    #[test]
15151    fn test_service_picker_i_key_activates_filter() {
15152        let mut app = test_app();
15153
15154        // Start in ServicePicker mode (Normal mode - filter not active)
15155        assert_eq!(app.mode, Mode::ServicePicker);
15156        assert!(!app.service_picker.filter_active);
15157        assert!(app.service_picker.filter.is_empty());
15158
15159        // Press 'i' to enter filter mode
15160        app.handle_action(Action::EnterFilterMode);
15161
15162        // Should still be in ServicePicker mode but filter should be active
15163        assert_eq!(app.mode, Mode::ServicePicker);
15164        assert!(app.service_picker.filter_active);
15165        assert!(app.service_picker.filter.is_empty());
15166
15167        // Now typing should work
15168        app.handle_action(Action::FilterInput('s'));
15169        assert_eq!(app.service_picker.filter, "s");
15170    }
15171
15172    #[test]
15173    fn test_service_picker_esc_exits_filter_mode() {
15174        let mut app = test_app();
15175        assert_eq!(app.mode, Mode::ServicePicker);
15176        assert!(!app.service_picker.filter_active);
15177
15178        // Enter filter mode
15179        app.handle_action(Action::EnterFilterMode);
15180        assert!(app.service_picker.filter_active);
15181        assert_eq!(app.mode, Mode::ServicePicker);
15182
15183        // Type something
15184        app.handle_action(Action::FilterInput('s'));
15185        assert_eq!(app.service_picker.filter, "s");
15186
15187        // Esc should exit filter mode (not close menu)
15188        app.handle_action(Action::ExitFilterMode);
15189        assert!(!app.service_picker.filter_active);
15190        assert_eq!(app.mode, Mode::ServicePicker); // Still in picker
15191
15192        // Second Esc should close menu
15193        app.handle_action(Action::ExitFilterMode);
15194        assert_eq!(app.mode, Mode::Normal);
15195    }
15196
15197    #[test]
15198    fn test_service_picker_navigation_only_works_in_normal_mode() {
15199        let mut app = test_app();
15200        app.service_picker.selected = 0;
15201
15202        // In normal mode, navigation should work
15203        assert!(!app.service_picker.filter_active);
15204        app.handle_action(Action::NextItem);
15205        assert_eq!(app.service_picker.selected, 1);
15206
15207        // Enter filter mode
15208        app.handle_action(Action::EnterFilterMode);
15209        assert!(app.service_picker.filter_active);
15210
15211        // In filter mode, navigation should NOT work
15212        let prev_selected = app.service_picker.selected;
15213        app.handle_action(Action::NextItem);
15214        assert_eq!(app.service_picker.selected, prev_selected); // Unchanged
15215
15216        // Exit filter mode
15217        app.handle_action(Action::ExitFilterMode);
15218        assert!(!app.service_picker.filter_active);
15219
15220        // Navigation should work again
15221        app.handle_action(Action::NextItem);
15222        assert_eq!(app.service_picker.selected, prev_selected + 1);
15223    }
15224
15225    #[test]
15226    fn test_service_picker_typing_filters_services() {
15227        let mut app = test_app();
15228
15229        // Start in ServicePicker mode
15230        assert_eq!(app.mode, Mode::ServicePicker);
15231        assert!(!app.service_picker.filter_active);
15232
15233        // Enter filter mode
15234        app.handle_action(Action::EnterFilterMode);
15235        assert!(app.service_picker.filter_active);
15236
15237        // Type "s3" to filter
15238        app.handle_action(Action::FilterInput('s'));
15239        app.handle_action(Action::FilterInput('3'));
15240
15241        assert_eq!(app.service_picker.filter, "s3");
15242        assert_eq!(app.mode, Mode::ServicePicker);
15243    }
15244
15245    #[test]
15246    fn test_service_picker_resets_on_open() {
15247        let mut app = test_app();
15248
15249        // Select a service to get into Normal mode
15250        app.service_selected = true;
15251        app.mode = Mode::Normal;
15252
15253        // Simulate having previous filter and selection
15254        app.service_picker.filter = "previous".to_string();
15255        app.service_picker.filter_active = true;
15256        app.service_picker.selected = 5;
15257
15258        // Open space menu (service picker)
15259        app.handle_action(Action::OpenSpaceMenu);
15260
15261        // Filter, filter_active, and selection should be reset
15262        assert_eq!(app.mode, Mode::SpaceMenu);
15263        assert!(app.service_picker.filter.is_empty());
15264        assert!(!app.service_picker.filter_active);
15265        assert_eq!(app.service_picker.selected, 0);
15266    }
15267
15268    #[test]
15269    fn test_no_pii_in_test_data() {
15270        // Ensure test data uses placeholder account IDs, not real ones
15271        let test_repo = EcrRepository {
15272            name: "test-repo".to_string(),
15273            uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
15274            created_at: "2024-01-01".to_string(),
15275            tag_immutability: "MUTABLE".to_string(),
15276            encryption_type: "AES256".to_string(),
15277        };
15278
15279        // Verify placeholder account ID is used
15280        assert!(test_repo.uri.starts_with("123456789012"));
15281        assert!(!test_repo.uri.contains("123456789013")); // Not a real account
15282    }
15283
15284    #[test]
15285    fn test_lambda_versions_tab_triggers_loading() {
15286        let mut app = test_app();
15287        app.current_service = Service::LambdaFunctions;
15288        app.service_selected = true;
15289
15290        // Simulate selecting a function
15291        app.lambda_state.current_function = Some("test-function".to_string());
15292        app.lambda_state.detail_tab = LambdaDetailTab::Code;
15293
15294        // Initially no versions
15295        assert!(app.lambda_state.version_table.items.is_empty());
15296
15297        // Switch to Versions tab
15298        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15299
15300        // The main loop should detect this change and load versions
15301        // We verify the state is set up correctly for loading
15302        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
15303        assert!(app.lambda_state.current_function.is_some());
15304    }
15305
15306    #[test]
15307    fn test_lambda_versions_navigation() {
15308        let mut app = test_app();
15309        app.current_service = Service::LambdaFunctions;
15310        app.service_selected = true;
15311        app.lambda_state.current_function = Some("test-function".to_string());
15312        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15313
15314        // Add test versions
15315        app.lambda_state.version_table.items = vec![
15316            LambdaVersion {
15317                version: "3".to_string(),
15318                aliases: "prod".to_string(),
15319                description: "".to_string(),
15320                last_modified: "".to_string(),
15321                architecture: "X86_64".to_string(),
15322            },
15323            LambdaVersion {
15324                version: "2".to_string(),
15325                aliases: "".to_string(),
15326                description: "".to_string(),
15327                last_modified: "".to_string(),
15328                architecture: "X86_64".to_string(),
15329            },
15330            LambdaVersion {
15331                version: "1".to_string(),
15332                aliases: "".to_string(),
15333                description: "".to_string(),
15334                last_modified: "".to_string(),
15335                architecture: "X86_64".to_string(),
15336            },
15337        ];
15338
15339        // Verify versions are loaded
15340        assert_eq!(app.lambda_state.version_table.items.len(), 3);
15341        assert_eq!(app.lambda_state.version_table.items[0].version, "3");
15342        assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
15343
15344        // Verify selection can be changed
15345        app.lambda_state.version_table.selected = 1;
15346        assert_eq!(app.lambda_state.version_table.selected, 1);
15347    }
15348
15349    #[test]
15350    fn test_lambda_versions_with_aliases() {
15351        let version = LambdaVersion {
15352            version: "35".to_string(),
15353            aliases: "prod, staging".to_string(),
15354            description: "Production version".to_string(),
15355            last_modified: "2024-01-01".to_string(),
15356            architecture: "X86_64".to_string(),
15357        };
15358
15359        assert_eq!(version.aliases, "prod, staging");
15360        assert!(!version.aliases.is_empty());
15361    }
15362
15363    #[test]
15364    fn test_lambda_versions_expansion() {
15365        let mut app = test_app();
15366        app.current_service = Service::LambdaFunctions;
15367        app.service_selected = true;
15368        app.lambda_state.current_function = Some("test-function".to_string());
15369        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15370
15371        // Add test versions
15372        app.lambda_state.version_table.items = vec![
15373            LambdaVersion {
15374                version: "2".to_string(),
15375                aliases: "prod".to_string(),
15376                description: "Production".to_string(),
15377                last_modified: "2024-01-01".to_string(),
15378                architecture: "X86_64".to_string(),
15379            },
15380            LambdaVersion {
15381                version: "1".to_string(),
15382                aliases: "".to_string(),
15383                description: "".to_string(),
15384                last_modified: "2024-01-01".to_string(),
15385                architecture: "Arm64".to_string(),
15386            },
15387        ];
15388
15389        app.lambda_state.version_table.selected = 0;
15390
15391        // Verify expansion can be set
15392        app.lambda_state.version_table.expanded_item = Some(0);
15393        assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
15394
15395        // Select different version
15396        app.lambda_state.version_table.selected = 1;
15397        app.lambda_state.version_table.expanded_item = Some(1);
15398        assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
15399    }
15400
15401    #[test]
15402    fn test_lambda_versions_page_navigation() {
15403        let mut app = test_app();
15404        app.current_service = Service::LambdaFunctions;
15405        app.service_selected = true;
15406        app.lambda_state.current_function = Some("test-function".to_string());
15407        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15408
15409        // Add 30 test versions
15410        app.lambda_state.version_table.items = (1..=30)
15411            .map(|i| LambdaVersion {
15412                version: i.to_string(),
15413                aliases: "".to_string(),
15414                description: "".to_string(),
15415                last_modified: "".to_string(),
15416                architecture: "X86_64".to_string(),
15417            })
15418            .collect();
15419
15420        app.lambda_state.version_table.page_size = PageSize::Ten;
15421        app.lambda_state.version_table.selected = 0;
15422
15423        // Go to page 2
15424        app.page_input = "2".to_string();
15425        app.handle_action(Action::OpenColumnSelector);
15426
15427        // Should be at index 10 (start of page 2)
15428        assert_eq!(app.lambda_state.version_table.selected, 10);
15429    }
15430
15431    #[test]
15432    fn test_lambda_versions_pagination_arrow_keys() {
15433        let mut app = test_app();
15434        app.current_service = Service::LambdaFunctions;
15435        app.service_selected = true;
15436        app.lambda_state.current_function = Some("test-function".to_string());
15437        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15438        app.mode = Mode::FilterInput;
15439        app.lambda_state.version_input_focus = InputFocus::Pagination;
15440
15441        // Add 30 test versions
15442        app.lambda_state.version_table.items = (1..=30)
15443            .map(|i| LambdaVersion {
15444                version: i.to_string(),
15445                aliases: "".to_string(),
15446                description: "".to_string(),
15447                last_modified: "".to_string(),
15448                architecture: "X86_64".to_string(),
15449            })
15450            .collect();
15451
15452        app.lambda_state.version_table.page_size = PageSize::Ten;
15453        app.lambda_state.version_table.selected = 0;
15454
15455        // Right arrow (PageDown) should go to next page
15456        app.handle_action(Action::PageDown);
15457        assert_eq!(app.lambda_state.version_table.selected, 10);
15458
15459        // Left arrow (PageUp) should go back
15460        app.handle_action(Action::PageUp);
15461        assert_eq!(app.lambda_state.version_table.selected, 0);
15462    }
15463
15464    #[test]
15465    fn test_lambda_versions_page_input_in_filter_mode() {
15466        let mut app = test_app();
15467        app.current_service = Service::LambdaFunctions;
15468        app.service_selected = true;
15469        app.lambda_state.current_function = Some("test-function".to_string());
15470        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15471        app.mode = Mode::FilterInput;
15472        app.lambda_state.version_input_focus = InputFocus::Pagination;
15473
15474        // Add 30 test versions
15475        app.lambda_state.version_table.items = (1..=30)
15476            .map(|i| LambdaVersion {
15477                version: i.to_string(),
15478                aliases: "".to_string(),
15479                description: "".to_string(),
15480                last_modified: "".to_string(),
15481                architecture: "X86_64".to_string(),
15482            })
15483            .collect();
15484
15485        app.lambda_state.version_table.page_size = PageSize::Ten;
15486        app.lambda_state.version_table.selected = 0;
15487
15488        // Type "2" when focused on Pagination
15489        app.handle_action(Action::FilterInput('2'));
15490        assert_eq!(app.page_input, "2");
15491        assert_eq!(app.lambda_state.version_table.filter, ""); // Should not go to filter
15492
15493        // Press 'p' to go to page 2
15494        app.handle_action(Action::OpenColumnSelector);
15495        assert_eq!(app.lambda_state.version_table.selected, 10);
15496        assert_eq!(app.page_input, ""); // Should be cleared
15497    }
15498
15499    #[test]
15500    fn test_lambda_versions_filter_input() {
15501        let mut app = test_app();
15502        app.current_service = Service::LambdaFunctions;
15503        app.service_selected = true;
15504        app.lambda_state.current_function = Some("test-function".to_string());
15505        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15506        app.mode = Mode::FilterInput;
15507        app.lambda_state.version_input_focus = InputFocus::Filter;
15508
15509        // Add test versions
15510        app.lambda_state.version_table.items = vec![
15511            LambdaVersion {
15512                version: "1".to_string(),
15513                aliases: "prod".to_string(),
15514                description: "Production".to_string(),
15515                last_modified: "".to_string(),
15516                architecture: "X86_64".to_string(),
15517            },
15518            LambdaVersion {
15519                version: "2".to_string(),
15520                aliases: "staging".to_string(),
15521                description: "Staging".to_string(),
15522                last_modified: "".to_string(),
15523                architecture: "X86_64".to_string(),
15524            },
15525        ];
15526
15527        // Type filter text
15528        app.handle_action(Action::FilterInput('p'));
15529        app.handle_action(Action::FilterInput('r'));
15530        app.handle_action(Action::FilterInput('o'));
15531        app.handle_action(Action::FilterInput('d'));
15532        assert_eq!(app.lambda_state.version_table.filter, "prod");
15533
15534        // Backspace should work
15535        app.handle_action(Action::FilterBackspace);
15536        assert_eq!(app.lambda_state.version_table.filter, "pro");
15537    }
15538
15539    #[test]
15540    fn test_lambda_aliases_table_expansion() {
15541        use crate::lambda::Alias;
15542
15543        let mut app = test_app();
15544        app.current_service = Service::LambdaFunctions;
15545        app.service_selected = true;
15546        app.lambda_state.current_function = Some("test-function".to_string());
15547        app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
15548        app.mode = Mode::Normal;
15549
15550        app.lambda_state.alias_table.items = vec![
15551            Alias {
15552                name: "prod".to_string(),
15553                versions: "1".to_string(),
15554                description: "Production alias".to_string(),
15555            },
15556            Alias {
15557                name: "staging".to_string(),
15558                versions: "2".to_string(),
15559                description: "Staging alias".to_string(),
15560            },
15561        ];
15562
15563        app.lambda_state.alias_table.selected = 0;
15564
15565        // Select first alias - should open alias detail view (no tab change)
15566        app.handle_action(Action::Select);
15567        assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
15568        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
15569
15570        // Go back
15571        app.handle_action(Action::GoBack);
15572        assert_eq!(app.lambda_state.current_alias, None);
15573        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
15574
15575        // Select second alias
15576        app.lambda_state.alias_table.selected = 1;
15577        app.handle_action(Action::Select);
15578        assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
15579    }
15580
15581    #[test]
15582    fn test_lambda_versions_arrow_key_expansion() {
15583        let mut app = test_app();
15584        app.current_service = Service::LambdaFunctions;
15585        app.service_selected = true;
15586        app.lambda_state.current_function = Some("test-function".to_string());
15587        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15588        app.mode = Mode::Normal;
15589
15590        app.lambda_state.version_table.items = vec![LambdaVersion {
15591            version: "1".to_string(),
15592            aliases: "prod".to_string(),
15593            description: "Production".to_string(),
15594            last_modified: "2024-01-01".to_string(),
15595            architecture: "X86_64".to_string(),
15596        }];
15597
15598        app.lambda_state.version_table.selected = 0;
15599
15600        // Right arrow expands
15601        app.handle_action(Action::NextPane);
15602        assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
15603
15604        // Left arrow collapses
15605        app.handle_action(Action::PrevPane);
15606        assert_eq!(app.lambda_state.version_table.expanded_item, None);
15607    }
15608
15609    #[test]
15610    fn test_lambda_version_detail_view() {
15611        use crate::lambda::Function;
15612
15613        let mut app = test_app();
15614        app.current_service = Service::LambdaFunctions;
15615        app.service_selected = true;
15616        app.lambda_state.current_function = Some("test-function".to_string());
15617        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
15618        app.mode = Mode::Normal;
15619
15620        app.lambda_state.table.items = vec![Function {
15621            name: "test-function".to_string(),
15622            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
15623            application: None,
15624            description: "Test".to_string(),
15625            package_type: "Zip".to_string(),
15626            runtime: "python3.12".to_string(),
15627            architecture: "X86_64".to_string(),
15628            code_size: 1024,
15629            code_sha256: "hash".to_string(),
15630            memory_mb: 128,
15631            timeout_seconds: 30,
15632            last_modified: "2024-01-01".to_string(),
15633            layers: vec![],
15634        }];
15635
15636        app.lambda_state.version_table.items = vec![LambdaVersion {
15637            version: "1".to_string(),
15638            aliases: "prod".to_string(),
15639            description: "Production".to_string(),
15640            last_modified: "2024-01-01".to_string(),
15641            architecture: "X86_64".to_string(),
15642        }];
15643
15644        app.lambda_state.version_table.selected = 0;
15645
15646        // Select version to open detail view
15647        app.handle_action(Action::Select);
15648        assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
15649        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
15650
15651        // GoBack should go back to versions list
15652        app.handle_action(Action::GoBack);
15653        assert_eq!(app.lambda_state.current_version, None);
15654        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
15655    }
15656
15657    #[test]
15658    fn test_lambda_version_detail_tabs() {
15659        use crate::lambda::Function;
15660
15661        let mut app = test_app();
15662        app.current_service = Service::LambdaFunctions;
15663        app.service_selected = true;
15664        app.lambda_state.current_function = Some("test-function".to_string());
15665        app.lambda_state.current_version = Some("1".to_string());
15666        app.lambda_state.detail_tab = LambdaDetailTab::Code;
15667        app.mode = Mode::Normal;
15668
15669        app.lambda_state.table.items = vec![Function {
15670            name: "test-function".to_string(),
15671            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
15672            application: None,
15673            description: "Test".to_string(),
15674            package_type: "Zip".to_string(),
15675            runtime: "python3.12".to_string(),
15676            architecture: "X86_64".to_string(),
15677            code_size: 1024,
15678            code_sha256: "hash".to_string(),
15679            memory_mb: 128,
15680            timeout_seconds: 30,
15681            last_modified: "2024-01-01".to_string(),
15682            layers: vec![],
15683        }];
15684
15685        // Tab should cycle between Code, Monitor, and Configuration
15686        app.handle_action(Action::NextDetailTab);
15687        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
15688
15689        app.handle_action(Action::NextDetailTab);
15690        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
15691
15692        app.handle_action(Action::NextDetailTab);
15693        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
15694
15695        // BackTab should cycle backward
15696        app.handle_action(Action::PrevDetailTab);
15697        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
15698
15699        app.handle_action(Action::PrevDetailTab);
15700        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
15701    }
15702
15703    #[test]
15704    fn test_lambda_aliases_arrow_key_expansion() {
15705        use crate::lambda::Alias;
15706
15707        let mut app = test_app();
15708        app.current_service = Service::LambdaFunctions;
15709        app.service_selected = true;
15710        app.lambda_state.current_function = Some("test-function".to_string());
15711        app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
15712        app.mode = Mode::Normal;
15713
15714        app.lambda_state.alias_table.items = vec![Alias {
15715            name: "prod".to_string(),
15716            versions: "1".to_string(),
15717            description: "Production alias".to_string(),
15718        }];
15719
15720        app.lambda_state.alias_table.selected = 0;
15721
15722        // Right arrow expands
15723        app.handle_action(Action::NextPane);
15724        assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
15725
15726        // Left arrow collapses
15727        app.handle_action(Action::PrevPane);
15728        assert_eq!(app.lambda_state.alias_table.expanded_item, None);
15729    }
15730
15731    #[test]
15732    fn test_lambda_functions_arrow_key_expansion() {
15733        use crate::lambda::Function;
15734
15735        let mut app = test_app();
15736        app.current_service = Service::LambdaFunctions;
15737        app.service_selected = true;
15738        app.mode = Mode::Normal;
15739
15740        app.lambda_state.table.items = vec![Function {
15741            name: "test-function".to_string(),
15742            arn: "arn".to_string(),
15743            application: None,
15744            description: "Test".to_string(),
15745            package_type: "Zip".to_string(),
15746            runtime: "python3.12".to_string(),
15747            architecture: "X86_64".to_string(),
15748            code_size: 1024,
15749            code_sha256: "hash".to_string(),
15750            memory_mb: 128,
15751            timeout_seconds: 30,
15752            last_modified: "2024-01-01".to_string(),
15753            layers: vec![],
15754        }];
15755
15756        app.lambda_state.table.selected = 0;
15757
15758        // Right arrow expands
15759        app.handle_action(Action::NextPane);
15760        assert_eq!(app.lambda_state.table.expanded_item, Some(0));
15761
15762        // Left arrow collapses
15763        app.handle_action(Action::PrevPane);
15764        assert_eq!(app.lambda_state.table.expanded_item, None);
15765    }
15766
15767    #[test]
15768    fn test_lambda_version_detail_with_application() {
15769        use crate::lambda::Function;
15770
15771        let mut app = test_app();
15772        app.current_service = Service::LambdaFunctions;
15773        app.service_selected = true;
15774        app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
15775        app.lambda_state.current_version = Some("1".to_string());
15776        app.lambda_state.detail_tab = LambdaDetailTab::Code;
15777        app.mode = Mode::Normal;
15778
15779        app.lambda_state.table.items = vec![Function {
15780            name: "storefront-studio-beta-api".to_string(),
15781            arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
15782                .to_string(),
15783            application: Some("storefront-studio-beta".to_string()),
15784            description: "API function".to_string(),
15785            package_type: "Zip".to_string(),
15786            runtime: "python3.12".to_string(),
15787            architecture: "X86_64".to_string(),
15788            code_size: 1024,
15789            code_sha256: "hash".to_string(),
15790            memory_mb: 128,
15791            timeout_seconds: 30,
15792            last_modified: "2024-01-01".to_string(),
15793            layers: vec![],
15794        }];
15795
15796        // Verify function has application extracted
15797        assert_eq!(
15798            app.lambda_state.table.items[0].application,
15799            Some("storefront-studio-beta".to_string())
15800        );
15801        assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
15802    }
15803
15804    #[test]
15805    fn test_lambda_layer_navigation() {
15806        use crate::lambda::{Function, Layer};
15807
15808        let mut app = test_app();
15809        app.current_service = Service::LambdaFunctions;
15810        app.service_selected = true;
15811        app.lambda_state.current_function = Some("test-function".to_string());
15812        app.lambda_state.detail_tab = LambdaDetailTab::Code;
15813        app.mode = Mode::Normal;
15814
15815        app.lambda_state.table.items = vec![Function {
15816            name: "test-function".to_string(),
15817            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
15818            application: None,
15819            description: "Test".to_string(),
15820            package_type: "Zip".to_string(),
15821            runtime: "python3.12".to_string(),
15822            architecture: "X86_64".to_string(),
15823            code_size: 1024,
15824            code_sha256: "hash".to_string(),
15825            memory_mb: 128,
15826            timeout_seconds: 30,
15827            last_modified: "2024-01-01".to_string(),
15828            layers: vec![
15829                Layer {
15830                    merge_order: "1".to_string(),
15831                    name: "layer1".to_string(),
15832                    layer_version: "1".to_string(),
15833                    compatible_runtimes: "python3.9".to_string(),
15834                    compatible_architectures: "x86_64".to_string(),
15835                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
15836                },
15837                Layer {
15838                    merge_order: "2".to_string(),
15839                    name: "layer2".to_string(),
15840                    layer_version: "2".to_string(),
15841                    compatible_runtimes: "python3.9".to_string(),
15842                    compatible_architectures: "x86_64".to_string(),
15843                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
15844                },
15845                Layer {
15846                    merge_order: "3".to_string(),
15847                    name: "layer3".to_string(),
15848                    layer_version: "3".to_string(),
15849                    compatible_runtimes: "python3.9".to_string(),
15850                    compatible_architectures: "x86_64".to_string(),
15851                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
15852                },
15853            ],
15854        }];
15855
15856        assert_eq!(app.lambda_state.layer_selected, 0);
15857
15858        app.handle_action(Action::NextItem);
15859        assert_eq!(app.lambda_state.layer_selected, 1);
15860
15861        app.handle_action(Action::NextItem);
15862        assert_eq!(app.lambda_state.layer_selected, 2);
15863
15864        app.handle_action(Action::NextItem);
15865        assert_eq!(app.lambda_state.layer_selected, 2);
15866
15867        app.handle_action(Action::PrevItem);
15868        assert_eq!(app.lambda_state.layer_selected, 1);
15869
15870        app.handle_action(Action::PrevItem);
15871        assert_eq!(app.lambda_state.layer_selected, 0);
15872
15873        app.handle_action(Action::PrevItem);
15874        assert_eq!(app.lambda_state.layer_selected, 0);
15875    }
15876
15877    #[test]
15878    fn test_lambda_layer_expansion() {
15879        use crate::lambda::{Function, Layer};
15880
15881        let mut app = test_app();
15882        app.current_service = Service::LambdaFunctions;
15883        app.service_selected = true;
15884        app.lambda_state.current_function = Some("test-function".to_string());
15885        app.lambda_state.detail_tab = LambdaDetailTab::Code;
15886        app.mode = Mode::Normal;
15887
15888        app.lambda_state.table.items = vec![Function {
15889            name: "test-function".to_string(),
15890            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
15891            application: None,
15892            description: "Test".to_string(),
15893            package_type: "Zip".to_string(),
15894            runtime: "python3.12".to_string(),
15895            architecture: "X86_64".to_string(),
15896            code_size: 1024,
15897            code_sha256: "hash".to_string(),
15898            memory_mb: 128,
15899            timeout_seconds: 30,
15900            last_modified: "2024-01-01".to_string(),
15901            layers: vec![Layer {
15902                merge_order: "1".to_string(),
15903                name: "test-layer".to_string(),
15904                layer_version: "1".to_string(),
15905                compatible_runtimes: "python3.9".to_string(),
15906                compatible_architectures: "x86_64".to_string(),
15907                version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
15908            }],
15909        }];
15910
15911        assert_eq!(app.lambda_state.layer_expanded, None);
15912
15913        app.handle_action(Action::NextPane);
15914        assert_eq!(app.lambda_state.layer_expanded, Some(0));
15915
15916        app.handle_action(Action::PrevPane);
15917        assert_eq!(app.lambda_state.layer_expanded, None);
15918
15919        app.handle_action(Action::NextPane);
15920        assert_eq!(app.lambda_state.layer_expanded, Some(0));
15921
15922        app.handle_action(Action::NextPane);
15923        assert_eq!(app.lambda_state.layer_expanded, None);
15924    }
15925
15926    #[test]
15927    fn test_lambda_layer_selection_and_expansion_workflow() {
15928        use crate::lambda::{Function, Layer};
15929
15930        let mut app = test_app();
15931        app.current_service = Service::LambdaFunctions;
15932        app.service_selected = true;
15933        app.lambda_state.current_function = Some("test-function".to_string());
15934        app.lambda_state.detail_tab = LambdaDetailTab::Code;
15935        app.mode = Mode::Normal;
15936
15937        app.lambda_state.table.items = vec![Function {
15938            name: "test-function".to_string(),
15939            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
15940            application: None,
15941            description: "Test".to_string(),
15942            package_type: "Zip".to_string(),
15943            runtime: "python3.12".to_string(),
15944            architecture: "X86_64".to_string(),
15945            code_size: 1024,
15946            code_sha256: "hash".to_string(),
15947            memory_mb: 128,
15948            timeout_seconds: 30,
15949            last_modified: "2024-01-01".to_string(),
15950            layers: vec![
15951                Layer {
15952                    merge_order: "1".to_string(),
15953                    name: "layer1".to_string(),
15954                    layer_version: "1".to_string(),
15955                    compatible_runtimes: "python3.9".to_string(),
15956                    compatible_architectures: "x86_64".to_string(),
15957                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
15958                },
15959                Layer {
15960                    merge_order: "2".to_string(),
15961                    name: "layer2".to_string(),
15962                    layer_version: "2".to_string(),
15963                    compatible_runtimes: "python3.9".to_string(),
15964                    compatible_architectures: "x86_64".to_string(),
15965                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
15966                },
15967            ],
15968        }];
15969
15970        // Start at layer 0
15971        assert_eq!(app.lambda_state.layer_selected, 0);
15972        assert_eq!(app.lambda_state.layer_expanded, None);
15973
15974        // Expand layer 0
15975        app.handle_action(Action::NextPane);
15976        assert_eq!(app.lambda_state.layer_selected, 0);
15977        assert_eq!(app.lambda_state.layer_expanded, Some(0));
15978
15979        // Navigate to layer 1 while layer 0 is expanded
15980        app.handle_action(Action::NextItem);
15981        assert_eq!(app.lambda_state.layer_selected, 1);
15982        assert_eq!(app.lambda_state.layer_expanded, Some(0)); // Still expanded
15983
15984        // Expand layer 1 (should collapse layer 0 and expand layer 1)
15985        app.handle_action(Action::NextPane);
15986        assert_eq!(app.lambda_state.layer_selected, 1);
15987        assert_eq!(app.lambda_state.layer_expanded, Some(1));
15988
15989        // Collapse layer 1
15990        app.handle_action(Action::PrevPane);
15991        assert_eq!(app.lambda_state.layer_selected, 1);
15992        assert_eq!(app.lambda_state.layer_expanded, None);
15993
15994        // Navigate back to layer 0
15995        app.handle_action(Action::PrevItem);
15996        assert_eq!(app.lambda_state.layer_selected, 0);
15997        assert_eq!(app.lambda_state.layer_expanded, None);
15998    }
15999
16000    #[test]
16001    fn test_backtab_cycles_detail_tabs_backward() {
16002        let mut app = test_app();
16003        app.mode = Mode::Normal;
16004
16005        // Test Lambda detail tabs
16006        app.current_service = Service::LambdaFunctions;
16007        app.service_selected = true;
16008        app.lambda_state.current_function = Some("test-function".to_string());
16009        app.lambda_state.detail_tab = LambdaDetailTab::Code;
16010
16011        app.handle_action(Action::PrevDetailTab);
16012        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
16013
16014        app.handle_action(Action::PrevDetailTab);
16015        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
16016
16017        // Test IAM Roles detail tabs
16018        app.current_service = Service::IamRoles;
16019        app.iam_state.current_role = Some("test-role".to_string());
16020        app.iam_state.role_tab = RoleTab::Permissions;
16021
16022        app.handle_action(Action::PrevDetailTab);
16023        assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
16024
16025        // Test IAM Users detail tabs
16026        app.current_service = Service::IamUsers;
16027        app.iam_state.current_user = Some("test-user".to_string());
16028        app.iam_state.user_tab = UserTab::Permissions;
16029
16030        app.handle_action(Action::PrevDetailTab);
16031        assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
16032
16033        // Test IAM Groups detail tabs
16034        app.current_service = Service::IamUserGroups;
16035        app.iam_state.current_group = Some("test-group".to_string());
16036        app.iam_state.group_tab = GroupTab::Permissions;
16037
16038        app.handle_action(Action::PrevDetailTab);
16039        assert_eq!(app.iam_state.group_tab, GroupTab::Users);
16040
16041        // Test S3 object tabs
16042        app.current_service = Service::S3Buckets;
16043        app.s3_state.current_bucket = Some("test-bucket".to_string());
16044        app.s3_state.object_tab = S3ObjectTab::Properties;
16045
16046        app.handle_action(Action::PrevDetailTab);
16047        assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
16048
16049        // Test ECR repository tabs (Private/Public)
16050        app.current_service = Service::EcrRepositories;
16051        app.ecr_state.current_repository = None;
16052        app.ecr_state.tab = EcrTab::Private;
16053
16054        app.handle_action(Action::PrevDetailTab);
16055        assert_eq!(app.ecr_state.tab, EcrTab::Public);
16056
16057        // Test CloudFormation detail tabs
16058        app.current_service = Service::CloudFormationStacks;
16059        app.cfn_state.current_stack = Some("test-stack".to_string());
16060        app.cfn_state.detail_tab = CfnDetailTab::Resources;
16061    }
16062
16063    #[test]
16064    fn test_cloudformation_status_filter_active() {
16065        let filter = CfnStatusFilter::Active;
16066        assert!(filter.matches("CREATE_IN_PROGRESS"));
16067        assert!(filter.matches("UPDATE_IN_PROGRESS"));
16068        assert!(!filter.matches("CREATE_COMPLETE"));
16069        assert!(!filter.matches("DELETE_COMPLETE"));
16070        assert!(!filter.matches("CREATE_FAILED"));
16071    }
16072
16073    #[test]
16074    fn test_cloudformation_status_filter_complete() {
16075        let filter = CfnStatusFilter::Complete;
16076        assert!(filter.matches("CREATE_COMPLETE"));
16077        assert!(filter.matches("UPDATE_COMPLETE"));
16078        assert!(!filter.matches("DELETE_COMPLETE"));
16079        assert!(!filter.matches("CREATE_IN_PROGRESS"));
16080    }
16081
16082    #[test]
16083    fn test_cloudformation_status_filter_failed() {
16084        let filter = CfnStatusFilter::Failed;
16085        assert!(filter.matches("CREATE_FAILED"));
16086        assert!(filter.matches("UPDATE_FAILED"));
16087        assert!(!filter.matches("CREATE_COMPLETE"));
16088    }
16089
16090    #[test]
16091    fn test_cloudformation_status_filter_deleted() {
16092        let filter = CfnStatusFilter::Deleted;
16093        assert!(filter.matches("DELETE_COMPLETE"));
16094        assert!(filter.matches("DELETE_IN_PROGRESS"));
16095        assert!(!filter.matches("CREATE_COMPLETE"));
16096    }
16097
16098    #[test]
16099    fn test_cloudformation_status_filter_in_progress() {
16100        let filter = CfnStatusFilter::InProgress;
16101        assert!(filter.matches("CREATE_IN_PROGRESS"));
16102        assert!(filter.matches("UPDATE_IN_PROGRESS"));
16103        assert!(filter.matches("DELETE_IN_PROGRESS"));
16104        assert!(!filter.matches("CREATE_COMPLETE"));
16105    }
16106
16107    #[test]
16108    fn test_cloudformation_status_filter_cycle() {
16109        let filter = CfnStatusFilter::All;
16110        assert_eq!(filter.next(), CfnStatusFilter::Active);
16111        assert_eq!(filter.next().next(), CfnStatusFilter::Complete);
16112        assert_eq!(filter.next().next().next(), CfnStatusFilter::Failed);
16113        assert_eq!(filter.next().next().next().next(), CfnStatusFilter::Deleted);
16114        assert_eq!(
16115            filter.next().next().next().next().next(),
16116            CfnStatusFilter::InProgress
16117        );
16118        assert_eq!(
16119            filter.next().next().next().next().next().next(),
16120            CfnStatusFilter::All
16121        );
16122    }
16123
16124    #[test]
16125    fn test_cloudformation_default_columns() {
16126        let app = test_app();
16127        assert_eq!(app.cfn_visible_column_ids.len(), 4);
16128        assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
16129        assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
16130        assert!(app
16131            .cfn_visible_column_ids
16132            .contains(&CfnColumn::CreatedTime.id()));
16133        assert!(app
16134            .cfn_visible_column_ids
16135            .contains(&CfnColumn::Description.id()));
16136    }
16137
16138    #[test]
16139    fn test_cloudformation_all_columns() {
16140        let app = test_app();
16141        assert_eq!(app.cfn_column_ids.len(), 10);
16142    }
16143
16144    #[test]
16145    fn test_cloudformation_filter_by_name() {
16146        let mut app = test_app();
16147        app.cfn_state.status_filter = CfnStatusFilter::Complete;
16148        app.cfn_state.table.items = vec![
16149            CfnStack {
16150                name: "my-stack".to_string(),
16151                stack_id: "id1".to_string(),
16152                status: "CREATE_COMPLETE".to_string(),
16153                created_time: "2024-01-01".to_string(),
16154                updated_time: String::new(),
16155                deleted_time: String::new(),
16156                drift_status: String::new(),
16157                last_drift_check_time: String::new(),
16158                status_reason: String::new(),
16159                description: String::new(),
16160                detailed_status: String::new(),
16161                root_stack: String::new(),
16162                parent_stack: String::new(),
16163                termination_protection: false,
16164                iam_role: String::new(),
16165                tags: Vec::new(),
16166                stack_policy: String::new(),
16167                rollback_monitoring_time: String::new(),
16168                rollback_alarms: Vec::new(),
16169                notification_arns: Vec::new(),
16170            },
16171            CfnStack {
16172                name: "other-stack".to_string(),
16173                stack_id: "id2".to_string(),
16174                status: "CREATE_COMPLETE".to_string(),
16175                created_time: "2024-01-02".to_string(),
16176                updated_time: String::new(),
16177                deleted_time: String::new(),
16178                drift_status: String::new(),
16179                last_drift_check_time: String::new(),
16180                status_reason: String::new(),
16181                description: String::new(),
16182                detailed_status: String::new(),
16183                root_stack: String::new(),
16184                parent_stack: String::new(),
16185                termination_protection: false,
16186                iam_role: String::new(),
16187                tags: Vec::new(),
16188                stack_policy: String::new(),
16189                rollback_monitoring_time: String::new(),
16190                rollback_alarms: Vec::new(),
16191                notification_arns: Vec::new(),
16192            },
16193        ];
16194
16195        app.cfn_state.table.filter = "my".to_string();
16196        let filtered = filtered_cloudformation_stacks(&app);
16197        assert_eq!(filtered.len(), 1);
16198        assert_eq!(filtered[0].name, "my-stack");
16199    }
16200
16201    #[test]
16202    fn test_cloudformation_filter_by_description() {
16203        let mut app = test_app();
16204        app.cfn_state.status_filter = CfnStatusFilter::Complete;
16205        app.cfn_state.table.items = vec![CfnStack {
16206            name: "stack1".to_string(),
16207            stack_id: "id1".to_string(),
16208            status: "CREATE_COMPLETE".to_string(),
16209            created_time: "2024-01-01".to_string(),
16210            updated_time: String::new(),
16211            deleted_time: String::new(),
16212            drift_status: String::new(),
16213            last_drift_check_time: String::new(),
16214            status_reason: String::new(),
16215            description: "production stack".to_string(),
16216            detailed_status: String::new(),
16217            root_stack: String::new(),
16218            parent_stack: String::new(),
16219            termination_protection: false,
16220            iam_role: String::new(),
16221            tags: Vec::new(),
16222            stack_policy: String::new(),
16223            rollback_monitoring_time: String::new(),
16224            rollback_alarms: Vec::new(),
16225            notification_arns: Vec::new(),
16226        }];
16227
16228        app.cfn_state.table.filter = "production".to_string();
16229        let filtered = filtered_cloudformation_stacks(&app);
16230        assert_eq!(filtered.len(), 1);
16231    }
16232
16233    #[test]
16234    fn test_cloudformation_status_filter_applied() {
16235        let mut app = test_app();
16236        app.cfn_state.table.items = vec![
16237            CfnStack {
16238                name: "complete-stack".to_string(),
16239                stack_id: "id1".to_string(),
16240                status: "CREATE_COMPLETE".to_string(),
16241                created_time: "2024-01-01".to_string(),
16242                updated_time: String::new(),
16243                deleted_time: String::new(),
16244                drift_status: String::new(),
16245                last_drift_check_time: String::new(),
16246                status_reason: String::new(),
16247                description: String::new(),
16248                detailed_status: String::new(),
16249                root_stack: String::new(),
16250                parent_stack: String::new(),
16251                termination_protection: false,
16252                iam_role: String::new(),
16253                tags: Vec::new(),
16254                stack_policy: String::new(),
16255                rollback_monitoring_time: String::new(),
16256                rollback_alarms: Vec::new(),
16257                notification_arns: Vec::new(),
16258            },
16259            CfnStack {
16260                name: "failed-stack".to_string(),
16261                stack_id: "id2".to_string(),
16262                status: "CREATE_FAILED".to_string(),
16263                created_time: "2024-01-02".to_string(),
16264                updated_time: String::new(),
16265                deleted_time: String::new(),
16266                drift_status: String::new(),
16267                last_drift_check_time: String::new(),
16268                status_reason: String::new(),
16269                description: String::new(),
16270                detailed_status: String::new(),
16271                root_stack: String::new(),
16272                parent_stack: String::new(),
16273                termination_protection: false,
16274                iam_role: String::new(),
16275                tags: Vec::new(),
16276                stack_policy: String::new(),
16277                rollback_monitoring_time: String::new(),
16278                rollback_alarms: Vec::new(),
16279                notification_arns: Vec::new(),
16280            },
16281        ];
16282
16283        app.cfn_state.status_filter = CfnStatusFilter::Complete;
16284        let filtered = filtered_cloudformation_stacks(&app);
16285        assert_eq!(filtered.len(), 1);
16286        assert_eq!(filtered[0].name, "complete-stack");
16287
16288        app.cfn_state.status_filter = CfnStatusFilter::Failed;
16289        let filtered = filtered_cloudformation_stacks(&app);
16290        assert_eq!(filtered.len(), 1);
16291        assert_eq!(filtered[0].name, "failed-stack");
16292    }
16293
16294    #[test]
16295    fn test_cloudformation_default_page_size() {
16296        let app = test_app();
16297        assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
16298    }
16299
16300    #[test]
16301    fn test_cloudformation_default_status_filter() {
16302        let app = test_app();
16303        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
16304    }
16305
16306    #[test]
16307    fn test_cloudformation_view_nested_default_false() {
16308        let app = test_app();
16309        assert!(!app.cfn_state.view_nested);
16310    }
16311
16312    #[test]
16313    fn test_cloudformation_pagination_hotkeys() {
16314        let mut app = test_app();
16315        app.current_service = Service::CloudFormationStacks;
16316        app.service_selected = true;
16317        app.cfn_state.status_filter = CfnStatusFilter::All;
16318
16319        // Add 150 stacks
16320        for i in 0..150 {
16321            app.cfn_state.table.items.push(CfnStack {
16322                name: format!("stack-{}", i),
16323                stack_id: format!("id-{}", i),
16324                status: "CREATE_COMPLETE".to_string(),
16325                created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
16326                updated_time: String::new(),
16327                deleted_time: String::new(),
16328                drift_status: String::new(),
16329                last_drift_check_time: String::new(),
16330                status_reason: String::new(),
16331                description: String::new(),
16332                detailed_status: String::new(),
16333                root_stack: String::new(),
16334                parent_stack: String::new(),
16335                termination_protection: false,
16336                iam_role: String::new(),
16337                tags: vec![],
16338                stack_policy: String::new(),
16339                rollback_monitoring_time: String::new(),
16340                rollback_alarms: vec![],
16341                notification_arns: vec![],
16342            });
16343        }
16344
16345        // Go to page 2
16346        app.go_to_page(2);
16347        assert_eq!(app.cfn_state.table.selected, 50);
16348
16349        // Go to page 3
16350        app.go_to_page(3);
16351        assert_eq!(app.cfn_state.table.selected, 100);
16352
16353        // Go to page 1
16354        app.go_to_page(1);
16355        assert_eq!(app.cfn_state.table.selected, 0);
16356    }
16357
16358    #[test]
16359    fn test_cloudformation_tab_cycling_in_filter_mode() {
16360        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
16361        let mut app = test_app();
16362        app.current_service = Service::CloudFormationStacks;
16363        app.service_selected = true;
16364        app.mode = Mode::FilterInput;
16365        app.cfn_state.input_focus = InputFocus::Filter;
16366
16367        // Tab to StatusFilter
16368        app.handle_action(Action::NextFilterFocus);
16369        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
16370
16371        // Tab to ViewNested
16372        app.handle_action(Action::NextFilterFocus);
16373        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
16374
16375        // Tab to Pagination
16376        app.handle_action(Action::NextFilterFocus);
16377        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
16378
16379        // Tab back to Filter
16380        app.handle_action(Action::NextFilterFocus);
16381        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16382    }
16383
16384    #[test]
16385    fn test_cloudformation_timestamp_format_includes_utc() {
16386        let stack = CfnStack {
16387            name: "test-stack".to_string(),
16388            stack_id: "id-123".to_string(),
16389            status: "CREATE_COMPLETE".to_string(),
16390            created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
16391            updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
16392            deleted_time: String::new(),
16393            drift_status: String::new(),
16394            last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
16395            status_reason: String::new(),
16396            description: String::new(),
16397            detailed_status: String::new(),
16398            root_stack: String::new(),
16399            parent_stack: String::new(),
16400            termination_protection: false,
16401            iam_role: String::new(),
16402            tags: vec![],
16403            stack_policy: String::new(),
16404            rollback_monitoring_time: String::new(),
16405            rollback_alarms: vec![],
16406            notification_arns: vec![],
16407        };
16408
16409        assert!(stack.created_time.contains("(UTC)"));
16410        assert!(stack.updated_time.contains("(UTC)"));
16411        assert!(stack.last_drift_check_time.contains("(UTC)"));
16412        assert_eq!(stack.created_time.len(), 25);
16413    }
16414
16415    #[test]
16416    fn test_cloudformation_enter_drills_into_stack_view() {
16417        let mut app = test_app();
16418        app.current_service = Service::CloudFormationStacks;
16419        app.service_selected = true;
16420        app.mode = Mode::Normal;
16421        app.cfn_state.status_filter = CfnStatusFilter::All;
16422        app.tabs = vec![Tab {
16423            service: Service::CloudFormationStacks,
16424            title: "CloudFormation › Stacks".to_string(),
16425            breadcrumb: "CloudFormation › Stacks".to_string(),
16426        }];
16427        app.current_tab = 0;
16428
16429        app.cfn_state.table.items.push(CfnStack {
16430            name: "test-stack".to_string(),
16431            stack_id: "id-123".to_string(),
16432            status: "CREATE_COMPLETE".to_string(),
16433            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
16434            updated_time: String::new(),
16435            deleted_time: String::new(),
16436            drift_status: String::new(),
16437            last_drift_check_time: String::new(),
16438            status_reason: String::new(),
16439            description: String::new(),
16440            detailed_status: String::new(),
16441            root_stack: String::new(),
16442            parent_stack: String::new(),
16443            termination_protection: false,
16444            iam_role: String::new(),
16445            tags: vec![],
16446            stack_policy: String::new(),
16447            rollback_monitoring_time: String::new(),
16448            rollback_alarms: vec![],
16449            notification_arns: vec![],
16450        });
16451
16452        app.cfn_state.table.reset();
16453        assert_eq!(app.cfn_state.current_stack, None);
16454
16455        // Press Enter - should drill into stack detail view
16456        app.handle_action(Action::Select);
16457        assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
16458    }
16459
16460    #[test]
16461    fn test_cloudformation_arrow_keys_expand_collapse() {
16462        let mut app = test_app();
16463        app.current_service = Service::CloudFormationStacks;
16464        app.service_selected = true;
16465        app.mode = Mode::Normal;
16466        app.cfn_state.status_filter = CfnStatusFilter::All;
16467
16468        app.cfn_state.table.items.push(CfnStack {
16469            name: "test-stack".to_string(),
16470            stack_id: "id-123".to_string(),
16471            status: "CREATE_COMPLETE".to_string(),
16472            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
16473            updated_time: String::new(),
16474            deleted_time: String::new(),
16475            drift_status: String::new(),
16476            last_drift_check_time: String::new(),
16477            status_reason: String::new(),
16478            description: String::new(),
16479            detailed_status: String::new(),
16480            root_stack: String::new(),
16481            parent_stack: String::new(),
16482            termination_protection: false,
16483            iam_role: String::new(),
16484            tags: vec![],
16485            stack_policy: String::new(),
16486            rollback_monitoring_time: String::new(),
16487            rollback_alarms: vec![],
16488            notification_arns: vec![],
16489        });
16490
16491        app.cfn_state.table.reset();
16492        assert_eq!(app.cfn_state.table.expanded_item, None);
16493
16494        // Right arrow - should expand
16495        app.handle_action(Action::NextPane);
16496        assert_eq!(app.cfn_state.table.expanded_item, Some(0));
16497
16498        // Left arrow - should collapse
16499        app.handle_action(Action::PrevPane);
16500        assert_eq!(app.cfn_state.table.expanded_item, None);
16501
16502        // Verify current_stack is still None (not drilled in)
16503        assert_eq!(app.cfn_state.current_stack, None);
16504    }
16505
16506    #[test]
16507    fn test_cloudformation_tab_cycling() {
16508        use crate::ui::cfn::DetailTab;
16509        let mut app = test_app();
16510        app.current_service = Service::CloudFormationStacks;
16511        app.service_selected = true;
16512        app.mode = Mode::Normal;
16513        app.cfn_state.status_filter = CfnStatusFilter::All;
16514        app.cfn_state.current_stack = Some("test-stack".to_string());
16515
16516        assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
16517    }
16518
16519    #[test]
16520    fn test_cloudformation_console_url() {
16521        use crate::ui::cfn::DetailTab;
16522        let mut app = test_app();
16523        app.current_service = Service::CloudFormationStacks;
16524        app.service_selected = true;
16525        app.cfn_state.status_filter = CfnStatusFilter::All;
16526
16527        app.cfn_state.table.items.push(CfnStack {
16528            name: "test-stack".to_string(),
16529            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
16530                .to_string(),
16531            status: "CREATE_COMPLETE".to_string(),
16532            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
16533            updated_time: String::new(),
16534            deleted_time: String::new(),
16535            drift_status: String::new(),
16536            last_drift_check_time: String::new(),
16537            status_reason: String::new(),
16538            description: String::new(),
16539            detailed_status: String::new(),
16540            root_stack: String::new(),
16541            parent_stack: String::new(),
16542            termination_protection: false,
16543            iam_role: String::new(),
16544            tags: vec![],
16545            stack_policy: String::new(),
16546            rollback_monitoring_time: String::new(),
16547            rollback_alarms: vec![],
16548            notification_arns: vec![],
16549        });
16550
16551        app.cfn_state.current_stack = Some("test-stack".to_string());
16552
16553        // Stack info URL
16554        app.cfn_state.detail_tab = DetailTab::StackInfo;
16555        let url = app.get_console_url();
16556        assert!(url.contains("stackinfo"));
16557        assert!(url.contains("arn%3Aaws%3Acloudformation"));
16558
16559        // Events URL
16560        app.cfn_state.detail_tab = DetailTab::Events;
16561        let url = app.get_console_url();
16562        assert!(url.contains("events"));
16563        assert!(url.contains("arn%3Aaws%3Acloudformation"));
16564    }
16565
16566    #[test]
16567    fn test_iam_role_select() {
16568        let mut app = test_app();
16569        app.current_service = Service::IamRoles;
16570        app.service_selected = true;
16571        app.mode = Mode::Normal;
16572
16573        app.iam_state.roles.items = vec![
16574            IamRole {
16575                role_name: "role1".to_string(),
16576                path: "/".to_string(),
16577                trusted_entities: "AWS Service: ec2".to_string(),
16578                last_activity: "-".to_string(),
16579                arn: "arn:aws:iam::123456789012:role/role1".to_string(),
16580                creation_time: "2025-01-01".to_string(),
16581                description: "Test role 1".to_string(),
16582                max_session_duration: Some(3600),
16583            },
16584            IamRole {
16585                role_name: "role2".to_string(),
16586                path: "/".to_string(),
16587                trusted_entities: "AWS Service: lambda".to_string(),
16588                last_activity: "-".to_string(),
16589                arn: "arn:aws:iam::123456789012:role/role2".to_string(),
16590                creation_time: "2025-01-02".to_string(),
16591                description: "Test role 2".to_string(),
16592                max_session_duration: Some(7200),
16593            },
16594        ];
16595
16596        // Select first role
16597        app.iam_state.roles.selected = 0;
16598        app.handle_action(Action::Select);
16599
16600        assert_eq!(
16601            app.iam_state.current_role,
16602            Some("role1".to_string()),
16603            "Should open role detail view"
16604        );
16605        assert_eq!(
16606            app.iam_state.role_tab,
16607            RoleTab::Permissions,
16608            "Should default to Permissions tab"
16609        );
16610    }
16611
16612    #[test]
16613    fn test_iam_role_back_navigation() {
16614        let mut app = test_app();
16615        app.current_service = Service::IamRoles;
16616        app.service_selected = true;
16617        app.iam_state.current_role = Some("test-role".to_string());
16618
16619        app.handle_action(Action::GoBack);
16620
16621        assert_eq!(
16622            app.iam_state.current_role, None,
16623            "Should return to roles list"
16624        );
16625    }
16626
16627    #[test]
16628    fn test_iam_role_tab_navigation() {
16629        let mut app = test_app();
16630        app.current_service = Service::IamRoles;
16631        app.service_selected = true;
16632        app.iam_state.current_role = Some("test-role".to_string());
16633        app.iam_state.role_tab = RoleTab::Permissions;
16634
16635        app.handle_action(Action::NextDetailTab);
16636
16637        assert_eq!(
16638            app.iam_state.role_tab,
16639            RoleTab::TrustRelationships,
16640            "Should move to next tab"
16641        );
16642    }
16643
16644    #[test]
16645    fn test_iam_role_tab_cycle_order() {
16646        let mut app = test_app();
16647        app.current_service = Service::IamRoles;
16648        app.service_selected = true;
16649        app.iam_state.current_role = Some("test-role".to_string());
16650        app.iam_state.role_tab = RoleTab::Permissions;
16651
16652        app.handle_action(Action::NextDetailTab);
16653        assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
16654
16655        app.handle_action(Action::NextDetailTab);
16656        assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
16657
16658        app.handle_action(Action::NextDetailTab);
16659        assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
16660
16661        app.handle_action(Action::NextDetailTab);
16662        assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
16663
16664        app.handle_action(Action::NextDetailTab);
16665        assert_eq!(
16666            app.iam_state.role_tab,
16667            RoleTab::Permissions,
16668            "Should cycle back to first tab"
16669        );
16670    }
16671
16672    #[test]
16673    fn test_iam_role_pagination() {
16674        let mut app = test_app();
16675        app.current_service = Service::IamRoles;
16676        app.service_selected = true;
16677        app.iam_state.roles.page_size = PageSize::Ten;
16678
16679        app.iam_state.roles.items = (0..25)
16680            .map(|i| IamRole {
16681                role_name: format!("role{}", i),
16682                path: "/".to_string(),
16683                trusted_entities: "AWS Service: ec2".to_string(),
16684                last_activity: "-".to_string(),
16685                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
16686                creation_time: "2025-01-01".to_string(),
16687                description: format!("Test role {}", i),
16688                max_session_duration: Some(3600),
16689            })
16690            .collect();
16691
16692        // Jump to page 2
16693        app.go_to_page(2);
16694
16695        assert_eq!(
16696            app.iam_state.roles.selected, 10,
16697            "Should select first item of page 2"
16698        );
16699        assert_eq!(
16700            app.iam_state.roles.scroll_offset, 10,
16701            "Should update scroll offset"
16702        );
16703    }
16704
16705    #[test]
16706    fn test_tags_table_populated_on_role_detail() {
16707        let mut app = test_app();
16708        app.current_service = Service::IamRoles;
16709        app.service_selected = true;
16710        app.mode = Mode::Normal;
16711        app.iam_state.roles.items = vec![IamRole {
16712            role_name: "TestRole".to_string(),
16713            path: "/".to_string(),
16714            trusted_entities: String::new(),
16715            last_activity: String::new(),
16716            arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
16717            creation_time: "2025-01-01".to_string(),
16718            description: String::new(),
16719            max_session_duration: Some(3600),
16720        }];
16721
16722        // Manually populate tags to test table rendering
16723        app.iam_state.tags.items = vec![
16724            IamRoleTag {
16725                key: "Environment".to_string(),
16726                value: "Production".to_string(),
16727            },
16728            IamRoleTag {
16729                key: "Team".to_string(),
16730                value: "Platform".to_string(),
16731            },
16732        ];
16733
16734        assert_eq!(app.iam_state.tags.items.len(), 2);
16735        assert_eq!(app.iam_state.tags.items[0].key, "Environment");
16736        assert_eq!(app.iam_state.tags.items[0].value, "Production");
16737        assert_eq!(app.iam_state.tags.selected, 0);
16738    }
16739
16740    #[test]
16741    fn test_tags_table_navigation() {
16742        let mut app = test_app();
16743        app.current_service = Service::IamRoles;
16744        app.service_selected = true;
16745        app.mode = Mode::Normal;
16746        app.iam_state.current_role = Some("TestRole".to_string());
16747        app.iam_state.role_tab = RoleTab::Tags;
16748        app.iam_state.tags.items = vec![
16749            IamRoleTag {
16750                key: "Tag1".to_string(),
16751                value: "Value1".to_string(),
16752            },
16753            IamRoleTag {
16754                key: "Tag2".to_string(),
16755                value: "Value2".to_string(),
16756            },
16757        ];
16758
16759        app.handle_action(Action::NextItem);
16760        assert_eq!(app.iam_state.tags.selected, 1);
16761
16762        app.handle_action(Action::PrevItem);
16763        assert_eq!(app.iam_state.tags.selected, 0);
16764    }
16765
16766    #[test]
16767    fn test_last_accessed_table_navigation() {
16768        let mut app = test_app();
16769        app.current_service = Service::IamRoles;
16770        app.service_selected = true;
16771        app.mode = Mode::Normal;
16772        app.iam_state.current_role = Some("TestRole".to_string());
16773        app.iam_state.role_tab = RoleTab::LastAccessed;
16774        app.iam_state.last_accessed_services.items = vec![
16775            LastAccessedService {
16776                service: "S3".to_string(),
16777                policies_granting: "Policy1".to_string(),
16778                last_accessed: "2025-01-01".to_string(),
16779            },
16780            LastAccessedService {
16781                service: "EC2".to_string(),
16782                policies_granting: "Policy2".to_string(),
16783                last_accessed: "2025-01-02".to_string(),
16784            },
16785        ];
16786
16787        app.handle_action(Action::NextItem);
16788        assert_eq!(app.iam_state.last_accessed_services.selected, 1);
16789
16790        app.handle_action(Action::PrevItem);
16791        assert_eq!(app.iam_state.last_accessed_services.selected, 0);
16792    }
16793
16794    #[test]
16795    fn test_cfn_input_focus_next() {
16796        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
16797        let mut app = test_app();
16798        app.current_service = Service::CloudFormationStacks;
16799        app.mode = Mode::FilterInput;
16800        app.cfn_state.input_focus = InputFocus::Filter;
16801
16802        app.handle_action(Action::NextFilterFocus);
16803        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
16804
16805        app.handle_action(Action::NextFilterFocus);
16806        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
16807
16808        app.handle_action(Action::NextFilterFocus);
16809        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
16810
16811        app.handle_action(Action::NextFilterFocus);
16812        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16813    }
16814
16815    #[test]
16816    fn test_cfn_input_focus_prev() {
16817        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
16818        let mut app = test_app();
16819        app.current_service = Service::CloudFormationStacks;
16820        app.mode = Mode::FilterInput;
16821        app.cfn_state.input_focus = InputFocus::Filter;
16822
16823        app.handle_action(Action::PrevFilterFocus);
16824        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
16825
16826        app.handle_action(Action::PrevFilterFocus);
16827        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
16828
16829        app.handle_action(Action::PrevFilterFocus);
16830        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
16831
16832        app.handle_action(Action::PrevFilterFocus);
16833        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16834    }
16835
16836    #[test]
16837    fn test_cw_logs_input_focus_prev() {
16838        let mut app = test_app();
16839        app.current_service = Service::CloudWatchLogGroups;
16840        app.mode = Mode::FilterInput;
16841        app.view_mode = ViewMode::Detail;
16842        app.log_groups_state.detail_tab = CwLogsDetailTab::LogStreams;
16843        app.log_groups_state.input_focus = InputFocus::Filter;
16844
16845        app.handle_action(Action::PrevFilterFocus);
16846        assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
16847
16848        app.handle_action(Action::PrevFilterFocus);
16849        assert_eq!(
16850            app.log_groups_state.input_focus,
16851            InputFocus::Checkbox("ShowExpired")
16852        );
16853
16854        app.handle_action(Action::PrevFilterFocus);
16855        assert_eq!(
16856            app.log_groups_state.input_focus,
16857            InputFocus::Checkbox("ExactMatch")
16858        );
16859
16860        app.handle_action(Action::PrevFilterFocus);
16861        assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
16862    }
16863
16864    #[test]
16865    fn test_cw_events_input_focus_prev() {
16866        use crate::ui::cw::logs::EventFilterFocus;
16867        let mut app = test_app();
16868        app.mode = Mode::EventFilterInput;
16869        app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
16870
16871        app.handle_action(Action::PrevFilterFocus);
16872        assert_eq!(
16873            app.log_groups_state.event_input_focus,
16874            EventFilterFocus::DateRange
16875        );
16876
16877        app.handle_action(Action::PrevFilterFocus);
16878        assert_eq!(
16879            app.log_groups_state.event_input_focus,
16880            EventFilterFocus::Filter
16881        );
16882    }
16883
16884    #[test]
16885    fn test_cfn_input_focus_cycle_complete() {
16886        let mut app = test_app();
16887        app.current_service = Service::CloudFormationStacks;
16888        app.mode = Mode::FilterInput;
16889        app.cfn_state.input_focus = InputFocus::Filter;
16890
16891        // Cycle forward through all controls
16892        for _ in 0..4 {
16893            app.handle_action(Action::NextFilterFocus);
16894        }
16895        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16896
16897        // Cycle backward through all controls
16898        for _ in 0..4 {
16899            app.handle_action(Action::PrevFilterFocus);
16900        }
16901        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16902    }
16903
16904    #[test]
16905    fn test_cfn_filter_status_arrow_keys() {
16906        use crate::ui::cfn::STATUS_FILTER;
16907        let mut app = test_app();
16908        app.current_service = Service::CloudFormationStacks;
16909        app.mode = Mode::FilterInput;
16910        app.cfn_state.input_focus = STATUS_FILTER;
16911        app.cfn_state.status_filter = CfnStatusFilter::All;
16912
16913        app.handle_action(Action::NextItem);
16914        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
16915
16916        app.handle_action(Action::PrevItem);
16917        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
16918    }
16919
16920    #[test]
16921    fn test_cfn_filter_shift_tab_cycles_backward() {
16922        use crate::ui::cfn::STATUS_FILTER;
16923        let mut app = test_app();
16924        app.current_service = Service::CloudFormationStacks;
16925        app.mode = Mode::FilterInput;
16926        app.cfn_state.input_focus = STATUS_FILTER;
16927
16928        // Shift+Tab should go backward
16929        app.handle_action(Action::PrevFilterFocus);
16930        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16931
16932        // From Input, Shift+Tab should wrap to Pagination
16933        app.handle_action(Action::PrevFilterFocus);
16934        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
16935    }
16936
16937    #[test]
16938    fn test_cfn_pagination_arrow_keys() {
16939        let mut app = test_app();
16940        app.current_service = Service::CloudFormationStacks;
16941        app.mode = Mode::FilterInput;
16942        app.cfn_state.input_focus = InputFocus::Pagination;
16943        app.cfn_state.table.scroll_offset = 0;
16944        app.cfn_state.table.page_size = PageSize::Ten;
16945
16946        // Add some test stacks
16947        app.cfn_state.table.items = (0..30)
16948            .map(|i| CfnStack {
16949                name: format!("stack-{}", i),
16950                stack_id: format!("id-{}", i),
16951                status: "CREATE_COMPLETE".to_string(),
16952                created_time: "2024-01-01".to_string(),
16953                updated_time: String::new(),
16954                deleted_time: String::new(),
16955                drift_status: String::new(),
16956                last_drift_check_time: String::new(),
16957                status_reason: String::new(),
16958                description: String::new(),
16959                detailed_status: String::new(),
16960                root_stack: String::new(),
16961                parent_stack: String::new(),
16962                termination_protection: false,
16963                iam_role: String::new(),
16964                tags: Vec::new(),
16965                stack_policy: String::new(),
16966                rollback_monitoring_time: String::new(),
16967                rollback_alarms: Vec::new(),
16968                notification_arns: Vec::new(),
16969            })
16970            .collect();
16971
16972        // Right arrow should page forward
16973        app.handle_action(Action::PageDown);
16974        assert_eq!(app.cfn_state.table.scroll_offset, 10);
16975        // Verify page number calculation
16976        let page_size = app.cfn_state.table.page_size.value();
16977        let current_page = app.cfn_state.table.scroll_offset / page_size;
16978        assert_eq!(current_page, 1);
16979
16980        // Left arrow should page backward
16981        app.handle_action(Action::PageUp);
16982        assert_eq!(app.cfn_state.table.scroll_offset, 0);
16983        let current_page = app.cfn_state.table.scroll_offset / page_size;
16984        assert_eq!(current_page, 0);
16985    }
16986
16987    #[test]
16988    fn test_cfn_page_navigation_updates_selection() {
16989        let mut app = test_app();
16990        app.current_service = Service::CloudFormationStacks;
16991        app.mode = Mode::Normal;
16992
16993        // Add 30 test stacks
16994        app.cfn_state.table.items = (0..30)
16995            .map(|i| CfnStack {
16996                name: format!("stack-{}", i),
16997                stack_id: format!("id-{}", i),
16998                status: "CREATE_COMPLETE".to_string(),
16999                created_time: "2024-01-01".to_string(),
17000                updated_time: String::new(),
17001                deleted_time: String::new(),
17002                drift_status: String::new(),
17003                last_drift_check_time: String::new(),
17004                status_reason: String::new(),
17005                description: String::new(),
17006                detailed_status: String::new(),
17007                root_stack: String::new(),
17008                parent_stack: String::new(),
17009                termination_protection: false,
17010                iam_role: String::new(),
17011                tags: Vec::new(),
17012                stack_policy: String::new(),
17013                rollback_monitoring_time: String::new(),
17014                rollback_alarms: Vec::new(),
17015                notification_arns: Vec::new(),
17016            })
17017            .collect();
17018
17019        app.cfn_state.table.reset();
17020        app.cfn_state.table.scroll_offset = 0;
17021
17022        // Page down should update selection
17023        app.handle_action(Action::PageDown);
17024        assert_eq!(app.cfn_state.table.selected, 10);
17025
17026        // Page down again
17027        app.handle_action(Action::PageDown);
17028        assert_eq!(app.cfn_state.table.selected, 20);
17029
17030        // Page up should update selection
17031        app.handle_action(Action::PageUp);
17032        assert_eq!(app.cfn_state.table.selected, 10);
17033    }
17034
17035    #[test]
17036    fn test_cfn_filter_input_only_when_focused() {
17037        use crate::ui::cfn::STATUS_FILTER;
17038        let mut app = test_app();
17039        app.current_service = Service::CloudFormationStacks;
17040        app.mode = Mode::FilterInput;
17041        app.cfn_state.input_focus = STATUS_FILTER;
17042        app.cfn_state.table.filter = String::new();
17043
17044        // Typing should not add to filter when focus is not on Input
17045        app.handle_action(Action::FilterInput('t'));
17046        app.handle_action(Action::FilterInput('e'));
17047        app.handle_action(Action::FilterInput('s'));
17048        app.handle_action(Action::FilterInput('t'));
17049        assert_eq!(app.cfn_state.table.filter, "");
17050
17051        // Switch to Input focus
17052        app.cfn_state.input_focus = InputFocus::Filter;
17053        app.handle_action(Action::FilterInput('t'));
17054        app.handle_action(Action::FilterInput('e'));
17055        app.handle_action(Action::FilterInput('s'));
17056        app.handle_action(Action::FilterInput('t'));
17057        assert_eq!(app.cfn_state.table.filter, "test");
17058    }
17059
17060    #[test]
17061    fn test_cfn_input_focus_resets_on_start() {
17062        let mut app = test_app();
17063        app.current_service = Service::CloudFormationStacks;
17064        app.service_selected = true;
17065        app.mode = Mode::Normal;
17066        app.cfn_state.input_focus = InputFocus::Pagination;
17067
17068        // Start filter should reset focus to Input
17069        app.handle_action(Action::StartFilter);
17070        assert_eq!(app.mode, Mode::FilterInput);
17071        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
17072    }
17073
17074    #[test]
17075    fn test_iam_roles_input_focus_cycles_forward() {
17076        let mut app = test_app();
17077        app.current_service = Service::IamRoles;
17078        app.mode = Mode::FilterInput;
17079        app.iam_state.role_input_focus = InputFocus::Filter;
17080
17081        app.handle_action(Action::NextFilterFocus);
17082        assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
17083
17084        app.handle_action(Action::NextFilterFocus);
17085        assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
17086    }
17087
17088    #[test]
17089    fn test_iam_roles_input_focus_cycles_backward() {
17090        let mut app = test_app();
17091        app.current_service = Service::IamRoles;
17092        app.mode = Mode::FilterInput;
17093        app.iam_state.role_input_focus = InputFocus::Filter;
17094
17095        app.handle_action(Action::PrevFilterFocus);
17096        assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
17097
17098        app.handle_action(Action::PrevFilterFocus);
17099        assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
17100    }
17101
17102    #[test]
17103    fn test_iam_roles_filter_input_only_when_focused() {
17104        let mut app = test_app();
17105        app.current_service = Service::IamRoles;
17106        app.mode = Mode::FilterInput;
17107        app.iam_state.role_input_focus = InputFocus::Pagination;
17108        app.iam_state.roles.filter = String::new();
17109
17110        // Typing should not add to filter when focus is on Pagination
17111        app.handle_action(Action::FilterInput('t'));
17112        app.handle_action(Action::FilterInput('e'));
17113        app.handle_action(Action::FilterInput('s'));
17114        app.handle_action(Action::FilterInput('t'));
17115        assert_eq!(app.iam_state.roles.filter, "");
17116
17117        // Switch to Input focus
17118        app.iam_state.role_input_focus = InputFocus::Filter;
17119        app.handle_action(Action::FilterInput('t'));
17120        app.handle_action(Action::FilterInput('e'));
17121        app.handle_action(Action::FilterInput('s'));
17122        app.handle_action(Action::FilterInput('t'));
17123        assert_eq!(app.iam_state.roles.filter, "test");
17124    }
17125
17126    #[test]
17127    fn test_iam_roles_page_down_updates_scroll_offset() {
17128        let mut app = test_app();
17129        app.current_service = Service::IamRoles;
17130        app.mode = Mode::Normal;
17131        app.iam_state.roles.items = (0..50)
17132            .map(|i| IamRole {
17133                role_name: format!("role-{}", i),
17134                path: "/".to_string(),
17135                trusted_entities: "AWS Service".to_string(),
17136                last_activity: "N/A".to_string(),
17137                arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
17138                creation_time: "2024-01-01".to_string(),
17139                description: String::new(),
17140                max_session_duration: Some(3600),
17141            })
17142            .collect();
17143
17144        app.iam_state.roles.selected = 0;
17145        app.iam_state.roles.scroll_offset = 0;
17146
17147        // Page down should update both selected and scroll_offset
17148        app.handle_action(Action::PageDown);
17149        assert_eq!(app.iam_state.roles.selected, 10);
17150        // scroll_offset should be updated to keep selection visible
17151        assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
17152
17153        // Page down again
17154        app.handle_action(Action::PageDown);
17155        assert_eq!(app.iam_state.roles.selected, 20);
17156        assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
17157    }
17158
17159    #[test]
17160    fn test_application_selection_and_deployments_tab() {
17161        use crate::lambda::Application as LambdaApplication;
17162        use LambdaApplicationDetailTab;
17163
17164        let mut app = test_app();
17165        app.current_service = Service::LambdaApplications;
17166        app.service_selected = true;
17167        app.mode = Mode::Normal;
17168
17169        app.lambda_application_state.table.items = vec![LambdaApplication {
17170            name: "test-app".to_string(),
17171            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
17172            description: "Test application".to_string(),
17173            status: "CREATE_COMPLETE".to_string(),
17174            last_modified: "2024-01-01".to_string(),
17175        }];
17176
17177        // Select application
17178        app.handle_action(Action::Select);
17179        assert_eq!(
17180            app.lambda_application_state.current_application,
17181            Some("test-app".to_string())
17182        );
17183        assert_eq!(
17184            app.lambda_application_state.detail_tab,
17185            LambdaApplicationDetailTab::Overview
17186        );
17187
17188        // Switch to Deployments tab
17189        app.handle_action(Action::NextDetailTab);
17190        assert_eq!(
17191            app.lambda_application_state.detail_tab,
17192            LambdaApplicationDetailTab::Deployments
17193        );
17194
17195        // Go back
17196        app.handle_action(Action::GoBack);
17197        assert_eq!(app.lambda_application_state.current_application, None);
17198    }
17199
17200    #[test]
17201    fn test_application_resources_filter_and_pagination() {
17202        use crate::lambda::Application as LambdaApplication;
17203        use LambdaApplicationDetailTab;
17204
17205        let mut app = test_app();
17206        app.current_service = Service::LambdaApplications;
17207        app.service_selected = true;
17208        app.mode = Mode::Normal;
17209
17210        app.lambda_application_state.table.items = vec![LambdaApplication {
17211            name: "test-app".to_string(),
17212            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
17213            description: "Test application".to_string(),
17214            status: "CREATE_COMPLETE".to_string(),
17215            last_modified: "2024-01-01".to_string(),
17216        }];
17217
17218        // Select application
17219        app.handle_action(Action::Select);
17220        assert_eq!(
17221            app.lambda_application_state.detail_tab,
17222            LambdaApplicationDetailTab::Overview
17223        );
17224
17225        // Verify resources were loaded
17226        assert!(!app.lambda_application_state.resources.items.is_empty());
17227
17228        // Test filter focus cycling
17229        app.mode = Mode::FilterInput;
17230        assert_eq!(
17231            app.lambda_application_state.resource_input_focus,
17232            InputFocus::Filter
17233        );
17234
17235        app.handle_action(Action::NextFilterFocus);
17236        assert_eq!(
17237            app.lambda_application_state.resource_input_focus,
17238            InputFocus::Pagination
17239        );
17240
17241        app.handle_action(Action::PrevFilterFocus);
17242        assert_eq!(
17243            app.lambda_application_state.resource_input_focus,
17244            InputFocus::Filter
17245        );
17246    }
17247
17248    #[test]
17249    fn test_application_deployments_filter_and_pagination() {
17250        use crate::lambda::Application as LambdaApplication;
17251        use LambdaApplicationDetailTab;
17252
17253        let mut app = test_app();
17254        app.current_service = Service::LambdaApplications;
17255        app.service_selected = true;
17256        app.mode = Mode::Normal;
17257
17258        app.lambda_application_state.table.items = vec![LambdaApplication {
17259            name: "test-app".to_string(),
17260            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
17261            description: "Test application".to_string(),
17262            status: "CREATE_COMPLETE".to_string(),
17263            last_modified: "2024-01-01".to_string(),
17264        }];
17265
17266        // Select application and switch to Deployments tab
17267        app.handle_action(Action::Select);
17268        app.handle_action(Action::NextDetailTab);
17269        assert_eq!(
17270            app.lambda_application_state.detail_tab,
17271            LambdaApplicationDetailTab::Deployments
17272        );
17273
17274        // Verify deployments were loaded
17275        assert!(!app.lambda_application_state.deployments.items.is_empty());
17276
17277        // Test filter focus cycling
17278        app.mode = Mode::FilterInput;
17279        assert_eq!(
17280            app.lambda_application_state.deployment_input_focus,
17281            InputFocus::Filter
17282        );
17283
17284        app.handle_action(Action::NextFilterFocus);
17285        assert_eq!(
17286            app.lambda_application_state.deployment_input_focus,
17287            InputFocus::Pagination
17288        );
17289
17290        app.handle_action(Action::PrevFilterFocus);
17291        assert_eq!(
17292            app.lambda_application_state.deployment_input_focus,
17293            InputFocus::Filter
17294        );
17295    }
17296
17297    #[test]
17298    fn test_application_resource_expansion() {
17299        use crate::lambda::Application as LambdaApplication;
17300        use LambdaApplicationDetailTab;
17301
17302        let mut app = test_app();
17303        app.current_service = Service::LambdaApplications;
17304        app.service_selected = true;
17305        app.mode = Mode::Normal;
17306
17307        app.lambda_application_state.table.items = vec![LambdaApplication {
17308            name: "test-app".to_string(),
17309            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
17310            description: "Test application".to_string(),
17311            status: "CREATE_COMPLETE".to_string(),
17312            last_modified: "2024-01-01".to_string(),
17313        }];
17314
17315        // Select application (Overview tab by default)
17316        app.handle_action(Action::Select);
17317        assert_eq!(
17318            app.lambda_application_state.detail_tab,
17319            LambdaApplicationDetailTab::Overview
17320        );
17321
17322        // Expand resource
17323        app.handle_action(Action::NextPane);
17324        assert_eq!(
17325            app.lambda_application_state.resources.expanded_item,
17326            Some(0)
17327        );
17328
17329        // Collapse resource
17330        app.handle_action(Action::PrevPane);
17331        assert_eq!(app.lambda_application_state.resources.expanded_item, None);
17332    }
17333
17334    #[test]
17335    fn test_application_deployment_expansion() {
17336        use crate::lambda::Application as LambdaApplication;
17337        use LambdaApplicationDetailTab;
17338
17339        let mut app = test_app();
17340        app.current_service = Service::LambdaApplications;
17341        app.service_selected = true;
17342        app.mode = Mode::Normal;
17343
17344        app.lambda_application_state.table.items = vec![LambdaApplication {
17345            name: "test-app".to_string(),
17346            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
17347            description: "Test application".to_string(),
17348            status: "CREATE_COMPLETE".to_string(),
17349            last_modified: "2024-01-01".to_string(),
17350        }];
17351
17352        // Select application and switch to Deployments tab
17353        app.handle_action(Action::Select);
17354        app.handle_action(Action::NextDetailTab);
17355        assert_eq!(
17356            app.lambda_application_state.detail_tab,
17357            LambdaApplicationDetailTab::Deployments
17358        );
17359
17360        // Expand deployment
17361        app.handle_action(Action::NextPane);
17362        assert_eq!(
17363            app.lambda_application_state.deployments.expanded_item,
17364            Some(0)
17365        );
17366
17367        // Collapse deployment
17368        app.handle_action(Action::PrevPane);
17369        assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
17370    }
17371
17372    #[test]
17373    fn test_s3_nested_prefix_expansion() {
17374        use crate::s3::Bucket;
17375        use S3Object;
17376
17377        let mut app = test_app();
17378        app.current_service = Service::S3Buckets;
17379        app.service_selected = true;
17380        app.mode = Mode::Normal;
17381
17382        // Setup bucket with nested prefixes (2 levels)
17383        app.s3_state.buckets.items = vec![Bucket {
17384            name: "test-bucket".to_string(),
17385            region: "us-east-1".to_string(),
17386            creation_date: "2024-01-01".to_string(),
17387        }];
17388
17389        // Level 1: bucket preview
17390        app.s3_state.bucket_preview.insert(
17391            "test-bucket".to_string(),
17392            vec![S3Object {
17393                key: "level1/".to_string(),
17394                size: 0,
17395                last_modified: "".to_string(),
17396                is_prefix: true,
17397                storage_class: "".to_string(),
17398            }],
17399        );
17400
17401        // Level 2: nested prefix
17402        app.s3_state.prefix_preview.insert(
17403            "level1/".to_string(),
17404            vec![S3Object {
17405                key: "level1/level2/".to_string(),
17406                size: 0,
17407                last_modified: "".to_string(),
17408                is_prefix: true,
17409                storage_class: "".to_string(),
17410            }],
17411        );
17412
17413        // Expand bucket (row 0)
17414        app.s3_state.selected_row = 0;
17415        app.handle_action(Action::NextPane);
17416        assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
17417
17418        // Expand level1/ (row 1)
17419        app.s3_state.selected_row = 1;
17420        app.handle_action(Action::NextPane);
17421        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
17422
17423        // Expand level2/ (row 2) - verifies nested expansion works
17424        app.s3_state.selected_row = 2;
17425        app.handle_action(Action::NextPane);
17426        assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
17427
17428        // Verify all are still expanded
17429        assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
17430        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
17431    }
17432
17433    #[test]
17434    fn test_s3_nested_prefix_collapse() {
17435        use crate::s3::Bucket;
17436        use S3Object;
17437
17438        let mut app = test_app();
17439        app.current_service = Service::S3Buckets;
17440        app.service_selected = true;
17441        app.mode = Mode::Normal;
17442
17443        app.s3_state.buckets.items = vec![Bucket {
17444            name: "test-bucket".to_string(),
17445            region: "us-east-1".to_string(),
17446            creation_date: "2024-01-01".to_string(),
17447        }];
17448
17449        app.s3_state.bucket_preview.insert(
17450            "test-bucket".to_string(),
17451            vec![S3Object {
17452                key: "level1/".to_string(),
17453                size: 0,
17454                last_modified: "".to_string(),
17455                is_prefix: true,
17456                storage_class: "".to_string(),
17457            }],
17458        );
17459
17460        app.s3_state.prefix_preview.insert(
17461            "level1/".to_string(),
17462            vec![S3Object {
17463                key: "level1/level2/".to_string(),
17464                size: 0,
17465                last_modified: "".to_string(),
17466                is_prefix: true,
17467                storage_class: "".to_string(),
17468            }],
17469        );
17470
17471        // Pre-expand all levels
17472        app.s3_state
17473            .expanded_prefixes
17474            .insert("test-bucket".to_string());
17475        app.s3_state.expanded_prefixes.insert("level1/".to_string());
17476        app.s3_state
17477            .expanded_prefixes
17478            .insert("level1/level2/".to_string());
17479
17480        // Collapse level2/ (row 2)
17481        app.s3_state.selected_row = 2;
17482        app.handle_action(Action::PrevPane);
17483        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
17484        assert!(app.s3_state.expanded_prefixes.contains("level1/")); // Parent still expanded
17485
17486        // Collapse level1/ (row 1)
17487        app.s3_state.selected_row = 1;
17488        app.handle_action(Action::PrevPane);
17489        assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
17490        assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); // Bucket still expanded
17491
17492        // Collapse bucket (row 0)
17493        app.s3_state.selected_row = 0;
17494        app.handle_action(Action::PrevPane);
17495        assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
17496    }
17497}
17498
17499#[cfg(test)]
17500mod sqs_tests {
17501    use super::*;
17502    use test_helpers::*;
17503
17504    #[test]
17505    fn test_sqs_filter_input() {
17506        let mut app = test_app();
17507        app.current_service = Service::SqsQueues;
17508        app.service_selected = true;
17509        app.mode = Mode::FilterInput;
17510
17511        app.handle_action(Action::FilterInput('t'));
17512        app.handle_action(Action::FilterInput('e'));
17513        app.handle_action(Action::FilterInput('s'));
17514        app.handle_action(Action::FilterInput('t'));
17515        assert_eq!(app.sqs_state.queues.filter, "test");
17516
17517        app.handle_action(Action::FilterBackspace);
17518        assert_eq!(app.sqs_state.queues.filter, "tes");
17519    }
17520
17521    #[test]
17522    fn test_sqs_start_filter() {
17523        let mut app = test_app();
17524        app.current_service = Service::SqsQueues;
17525        app.service_selected = true;
17526        app.mode = Mode::Normal;
17527
17528        app.handle_action(Action::StartFilter);
17529        assert_eq!(app.mode, Mode::FilterInput);
17530        assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
17531    }
17532
17533    #[test]
17534    fn test_sqs_filter_focus_cycling() {
17535        let mut app = test_app();
17536        app.current_service = Service::SqsQueues;
17537        app.service_selected = true;
17538        app.mode = Mode::FilterInput;
17539        app.sqs_state.input_focus = InputFocus::Filter;
17540
17541        app.handle_action(Action::NextFilterFocus);
17542        assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
17543
17544        app.handle_action(Action::NextFilterFocus);
17545        assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
17546
17547        app.handle_action(Action::PrevFilterFocus);
17548        assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
17549    }
17550
17551    #[test]
17552    fn test_sqs_navigation() {
17553        let mut app = test_app();
17554        app.current_service = Service::SqsQueues;
17555        app.service_selected = true;
17556        app.mode = Mode::Normal;
17557        app.sqs_state.queues.items = (0..10)
17558            .map(|i| SqsQueue {
17559                name: format!("queue{}", i),
17560                url: String::new(),
17561                queue_type: "Standard".to_string(),
17562                created_timestamp: String::new(),
17563                messages_available: "0".to_string(),
17564                messages_in_flight: "0".to_string(),
17565                encryption: "Disabled".to_string(),
17566                content_based_deduplication: "Disabled".to_string(),
17567                last_modified_timestamp: String::new(),
17568                visibility_timeout: String::new(),
17569                message_retention_period: String::new(),
17570                maximum_message_size: String::new(),
17571                delivery_delay: String::new(),
17572                receive_message_wait_time: String::new(),
17573                high_throughput_fifo: "N/A".to_string(),
17574                deduplication_scope: "N/A".to_string(),
17575                fifo_throughput_limit: "N/A".to_string(),
17576                dead_letter_queue: "-".to_string(),
17577                messages_delayed: "0".to_string(),
17578                redrive_allow_policy: "-".to_string(),
17579                redrive_policy: "".to_string(),
17580                redrive_task_id: "-".to_string(),
17581                redrive_task_start_time: "-".to_string(),
17582                redrive_task_status: "-".to_string(),
17583                redrive_task_percent: "-".to_string(),
17584                redrive_task_destination: "-".to_string(),
17585            })
17586            .collect();
17587
17588        app.handle_action(Action::NextItem);
17589        assert_eq!(app.sqs_state.queues.selected, 1);
17590
17591        app.handle_action(Action::PrevItem);
17592        assert_eq!(app.sqs_state.queues.selected, 0);
17593    }
17594
17595    #[test]
17596    fn test_sqs_page_navigation() {
17597        let mut app = test_app();
17598        app.current_service = Service::SqsQueues;
17599        app.service_selected = true;
17600        app.mode = Mode::Normal;
17601        app.sqs_state.queues.items = (0..100)
17602            .map(|i| SqsQueue {
17603                name: format!("queue{}", i),
17604                url: String::new(),
17605                queue_type: "Standard".to_string(),
17606                created_timestamp: String::new(),
17607                messages_available: "0".to_string(),
17608                messages_in_flight: "0".to_string(),
17609                encryption: "Disabled".to_string(),
17610                content_based_deduplication: "Disabled".to_string(),
17611                last_modified_timestamp: String::new(),
17612                visibility_timeout: String::new(),
17613                message_retention_period: String::new(),
17614                maximum_message_size: String::new(),
17615                delivery_delay: String::new(),
17616                receive_message_wait_time: String::new(),
17617                high_throughput_fifo: "N/A".to_string(),
17618                deduplication_scope: "N/A".to_string(),
17619                fifo_throughput_limit: "N/A".to_string(),
17620                dead_letter_queue: "-".to_string(),
17621                messages_delayed: "0".to_string(),
17622                redrive_allow_policy: "-".to_string(),
17623                redrive_policy: "".to_string(),
17624                redrive_task_id: "-".to_string(),
17625                redrive_task_start_time: "-".to_string(),
17626                redrive_task_status: "-".to_string(),
17627                redrive_task_percent: "-".to_string(),
17628                redrive_task_destination: "-".to_string(),
17629            })
17630            .collect();
17631
17632        app.handle_action(Action::PageDown);
17633        assert_eq!(app.sqs_state.queues.selected, 10);
17634
17635        app.handle_action(Action::PageUp);
17636        assert_eq!(app.sqs_state.queues.selected, 0);
17637    }
17638
17639    #[test]
17640    fn test_sqs_queue_expansion() {
17641        let mut app = test_app();
17642        app.current_service = Service::SqsQueues;
17643        app.service_selected = true;
17644        app.sqs_state.queues.items = vec![SqsQueue {
17645            name: "my-queue".to_string(),
17646            url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
17647            queue_type: "Standard".to_string(),
17648            created_timestamp: "2023-01-01".to_string(),
17649            messages_available: "5".to_string(),
17650            messages_in_flight: "2".to_string(),
17651            encryption: "Enabled".to_string(),
17652            content_based_deduplication: "Disabled".to_string(),
17653            last_modified_timestamp: "2023-01-02".to_string(),
17654            visibility_timeout: "30".to_string(),
17655            message_retention_period: "345600".to_string(),
17656            maximum_message_size: "262144".to_string(),
17657            delivery_delay: "0".to_string(),
17658            receive_message_wait_time: "0".to_string(),
17659            high_throughput_fifo: "N/A".to_string(),
17660            deduplication_scope: "N/A".to_string(),
17661            fifo_throughput_limit: "N/A".to_string(),
17662            dead_letter_queue: "-".to_string(),
17663            messages_delayed: "0".to_string(),
17664            redrive_allow_policy: "-".to_string(),
17665            redrive_policy: "".to_string(),
17666            redrive_task_id: "-".to_string(),
17667            redrive_task_start_time: "-".to_string(),
17668            redrive_task_status: "-".to_string(),
17669            redrive_task_percent: "-".to_string(),
17670            redrive_task_destination: "-".to_string(),
17671        }];
17672        app.sqs_state.queues.selected = 0;
17673
17674        assert_eq!(app.sqs_state.queues.expanded_item, None);
17675
17676        // Right arrow expands
17677        app.handle_action(Action::NextPane);
17678        assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
17679
17680        // Right arrow again keeps it expanded
17681        app.handle_action(Action::NextPane);
17682        assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
17683
17684        // Left arrow collapses
17685        app.handle_action(Action::PrevPane);
17686        assert_eq!(app.sqs_state.queues.expanded_item, None);
17687
17688        // Left arrow again keeps it collapsed
17689        app.handle_action(Action::PrevPane);
17690        assert_eq!(app.sqs_state.queues.expanded_item, None);
17691    }
17692
17693    #[test]
17694    fn test_sqs_column_toggle() {
17695        use crate::sqs::queue::Column as SqsColumn;
17696        let mut app = test_app();
17697        app.current_service = Service::SqsQueues;
17698        app.service_selected = true;
17699        app.mode = Mode::ColumnSelector;
17700
17701        // Start with all columns visible
17702        app.sqs_visible_column_ids = SqsColumn::ids();
17703        let initial_count = app.sqs_visible_column_ids.len();
17704
17705        // Select first column (index 0) and toggle it
17706        app.column_selector_index = 0;
17707        app.handle_action(Action::ToggleColumn);
17708
17709        // First column should be removed
17710        assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
17711        assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
17712
17713        // Toggle it back
17714        app.handle_action(Action::ToggleColumn);
17715        assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
17716        assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
17717    }
17718
17719    #[test]
17720    fn test_sqs_column_selector_navigation() {
17721        let mut app = test_app();
17722        app.current_service = Service::SqsQueues;
17723        app.service_selected = true;
17724        app.mode = Mode::ColumnSelector;
17725        app.column_selector_index = 0;
17726
17727        // Should be able to navigate through all columns
17728        let max_index = app.sqs_column_ids.len() - 1;
17729
17730        // Navigate to last column
17731        for _ in 0..max_index {
17732            app.handle_action(Action::NextItem);
17733        }
17734        assert_eq!(app.column_selector_index, max_index);
17735
17736        // Navigate back to first
17737        for _ in 0..max_index {
17738            app.handle_action(Action::PrevItem);
17739        }
17740        assert_eq!(app.column_selector_index, 0);
17741    }
17742
17743    #[test]
17744    fn test_sqs_queue_selection() {
17745        let mut app = test_app();
17746        app.current_service = Service::SqsQueues;
17747        app.service_selected = true;
17748        app.mode = Mode::Normal;
17749        app.sqs_state.queues.items = vec![SqsQueue {
17750            name: "my-queue".to_string(),
17751            url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
17752            queue_type: "Standard".to_string(),
17753            created_timestamp: "2023-01-01".to_string(),
17754            messages_available: "5".to_string(),
17755            messages_in_flight: "2".to_string(),
17756            encryption: "Enabled".to_string(),
17757            content_based_deduplication: "Disabled".to_string(),
17758            last_modified_timestamp: "2023-01-02".to_string(),
17759            visibility_timeout: "30".to_string(),
17760            message_retention_period: "345600".to_string(),
17761            maximum_message_size: "262144".to_string(),
17762            delivery_delay: "0".to_string(),
17763            receive_message_wait_time: "0".to_string(),
17764            high_throughput_fifo: "N/A".to_string(),
17765            deduplication_scope: "N/A".to_string(),
17766            fifo_throughput_limit: "N/A".to_string(),
17767            dead_letter_queue: "-".to_string(),
17768            messages_delayed: "0".to_string(),
17769            redrive_allow_policy: "-".to_string(),
17770            redrive_policy: "".to_string(),
17771            redrive_task_id: "-".to_string(),
17772            redrive_task_start_time: "-".to_string(),
17773            redrive_task_status: "-".to_string(),
17774            redrive_task_percent: "-".to_string(),
17775            redrive_task_destination: "-".to_string(),
17776        }];
17777        app.sqs_state.queues.selected = 0;
17778
17779        assert_eq!(app.sqs_state.current_queue, None);
17780
17781        // Select queue
17782        app.handle_action(Action::Select);
17783        assert_eq!(
17784            app.sqs_state.current_queue,
17785            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
17786        );
17787
17788        // Go back
17789        app.handle_action(Action::GoBack);
17790        assert_eq!(app.sqs_state.current_queue, None);
17791    }
17792
17793    #[test]
17794    fn test_sqs_lambda_triggers_expand_collapse() {
17795        let mut app = test_app();
17796        app.current_service = Service::SqsQueues;
17797        app.service_selected = true;
17798        app.sqs_state.current_queue =
17799            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
17800        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17801        app.sqs_state.triggers.items = vec![LambdaTrigger {
17802            uuid: "test-uuid".to_string(),
17803            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
17804            status: "Enabled".to_string(),
17805            last_modified: "2024-01-01T00:00:00Z".to_string(),
17806        }];
17807        app.sqs_state.triggers.selected = 0;
17808
17809        assert_eq!(app.sqs_state.triggers.expanded_item, None);
17810
17811        // Right arrow expands
17812        app.handle_action(Action::NextPane);
17813        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
17814
17815        // Left arrow collapses
17816        app.handle_action(Action::PrevPane);
17817        assert_eq!(app.sqs_state.triggers.expanded_item, None);
17818    }
17819
17820    #[test]
17821    fn test_sqs_lambda_triggers_expand_toggle() {
17822        let mut app = test_app();
17823        app.current_service = Service::SqsQueues;
17824        app.service_selected = true;
17825        app.sqs_state.current_queue =
17826            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
17827        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17828        app.sqs_state.triggers.items = vec![LambdaTrigger {
17829            uuid: "test-uuid".to_string(),
17830            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
17831            status: "Enabled".to_string(),
17832            last_modified: "2024-01-01T00:00:00Z".to_string(),
17833        }];
17834        app.sqs_state.triggers.selected = 0;
17835
17836        // Expand
17837        app.handle_action(Action::NextPane);
17838        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
17839
17840        // Toggle collapses
17841        app.handle_action(Action::NextPane);
17842        assert_eq!(app.sqs_state.triggers.expanded_item, None);
17843
17844        // Toggle expands again
17845        app.handle_action(Action::NextPane);
17846        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
17847    }
17848
17849    #[test]
17850    fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
17851        use crate::ui::sqs::filtered_lambda_triggers;
17852
17853        let mut app = test_app();
17854        app.current_service = Service::SqsQueues;
17855        app.service_selected = true;
17856        app.sqs_state.current_queue =
17857            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
17858        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17859        app.sqs_state.triggers.items = vec![
17860            LambdaTrigger {
17861                uuid: "uuid-3".to_string(),
17862                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
17863                status: "Enabled".to_string(),
17864                last_modified: "2024-03-01T00:00:00Z".to_string(),
17865            },
17866            LambdaTrigger {
17867                uuid: "uuid-1".to_string(),
17868                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
17869                status: "Enabled".to_string(),
17870                last_modified: "2024-01-01T00:00:00Z".to_string(),
17871            },
17872            LambdaTrigger {
17873                uuid: "uuid-2".to_string(),
17874                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
17875                status: "Enabled".to_string(),
17876                last_modified: "2024-02-01T00:00:00Z".to_string(),
17877            },
17878        ];
17879
17880        let sorted = filtered_lambda_triggers(&app);
17881
17882        // Should be sorted by last_modified ASC
17883        assert_eq!(sorted.len(), 3);
17884        assert_eq!(sorted[0].uuid, "uuid-1");
17885        assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
17886        assert_eq!(sorted[1].uuid, "uuid-2");
17887        assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
17888        assert_eq!(sorted[2].uuid, "uuid-3");
17889        assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
17890    }
17891
17892    #[test]
17893    fn test_sqs_lambda_triggers_filter_input() {
17894        let mut app = test_app();
17895        app.current_service = Service::SqsQueues;
17896        app.service_selected = true;
17897        app.mode = Mode::FilterInput;
17898        app.sqs_state.current_queue =
17899            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
17900        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17901        app.sqs_state.input_focus = InputFocus::Filter;
17902
17903        assert_eq!(app.sqs_state.triggers.filter, "");
17904
17905        // Type characters
17906        app.handle_action(Action::FilterInput('t'));
17907        assert_eq!(app.sqs_state.triggers.filter, "t");
17908
17909        app.handle_action(Action::FilterInput('e'));
17910        assert_eq!(app.sqs_state.triggers.filter, "te");
17911
17912        app.handle_action(Action::FilterInput('s'));
17913        assert_eq!(app.sqs_state.triggers.filter, "tes");
17914
17915        app.handle_action(Action::FilterInput('t'));
17916        assert_eq!(app.sqs_state.triggers.filter, "test");
17917
17918        // Backspace
17919        app.handle_action(Action::FilterBackspace);
17920        assert_eq!(app.sqs_state.triggers.filter, "tes");
17921    }
17922
17923    #[test]
17924    fn test_sqs_lambda_triggers_filter_applied() {
17925        use crate::ui::sqs::filtered_lambda_triggers;
17926
17927        let mut app = test_app();
17928        app.current_service = Service::SqsQueues;
17929        app.service_selected = true;
17930        app.sqs_state.current_queue =
17931            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
17932        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17933        app.sqs_state.triggers.items = vec![
17934            LambdaTrigger {
17935                uuid: "uuid-1".to_string(),
17936                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
17937                status: "Enabled".to_string(),
17938                last_modified: "2024-01-01T00:00:00Z".to_string(),
17939            },
17940            LambdaTrigger {
17941                uuid: "uuid-2".to_string(),
17942                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
17943                status: "Enabled".to_string(),
17944                last_modified: "2024-02-01T00:00:00Z".to_string(),
17945            },
17946            LambdaTrigger {
17947                uuid: "uuid-3".to_string(),
17948                arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
17949                status: "Enabled".to_string(),
17950                last_modified: "2024-03-01T00:00:00Z".to_string(),
17951            },
17952        ];
17953
17954        // No filter - all items
17955        let filtered = filtered_lambda_triggers(&app);
17956        assert_eq!(filtered.len(), 3);
17957
17958        // Filter by "alpha"
17959        app.sqs_state.triggers.filter = "alpha".to_string();
17960        let filtered = filtered_lambda_triggers(&app);
17961        assert_eq!(filtered.len(), 1);
17962        assert_eq!(
17963            filtered[0].arn,
17964            "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
17965        );
17966
17967        // Filter by "test" - matches 2
17968        app.sqs_state.triggers.filter = "test".to_string();
17969        let filtered = filtered_lambda_triggers(&app);
17970        assert_eq!(filtered.len(), 2);
17971        assert_eq!(
17972            filtered[0].arn,
17973            "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
17974        );
17975        assert_eq!(
17976            filtered[1].arn,
17977            "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
17978        );
17979
17980        // Filter by uuid
17981        app.sqs_state.triggers.filter = "uuid-3".to_string();
17982        let filtered = filtered_lambda_triggers(&app);
17983        assert_eq!(filtered.len(), 1);
17984        assert_eq!(filtered[0].uuid, "uuid-3");
17985    }
17986
17987    #[test]
17988    fn test_sqs_triggers_navigation() {
17989        let mut app = test_app();
17990        app.service_selected = true;
17991        app.mode = Mode::Normal;
17992        app.current_service = Service::SqsQueues;
17993        app.sqs_state.current_queue = Some("test-queue".to_string());
17994        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
17995        app.sqs_state.triggers.items = vec![
17996            LambdaTrigger {
17997                uuid: "1".to_string(),
17998                arn: "arn1".to_string(),
17999                status: "Enabled".to_string(),
18000                last_modified: "2024-01-01".to_string(),
18001            },
18002            LambdaTrigger {
18003                uuid: "2".to_string(),
18004                arn: "arn2".to_string(),
18005                status: "Enabled".to_string(),
18006                last_modified: "2024-01-02".to_string(),
18007            },
18008        ];
18009
18010        assert_eq!(app.sqs_state.triggers.selected, 0);
18011        app.next_item();
18012        assert_eq!(app.sqs_state.triggers.selected, 1);
18013        app.prev_item();
18014        assert_eq!(app.sqs_state.triggers.selected, 0);
18015    }
18016
18017    #[test]
18018    fn test_sqs_pipes_navigation() {
18019        let mut app = test_app();
18020        app.service_selected = true;
18021        app.mode = Mode::Normal;
18022        app.current_service = Service::SqsQueues;
18023        app.sqs_state.current_queue = Some("test-queue".to_string());
18024        app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
18025        app.sqs_state.pipes.items = vec![
18026            EventBridgePipe {
18027                name: "pipe1".to_string(),
18028                status: "RUNNING".to_string(),
18029                target: "target1".to_string(),
18030                last_modified: "2024-01-01".to_string(),
18031            },
18032            EventBridgePipe {
18033                name: "pipe2".to_string(),
18034                status: "RUNNING".to_string(),
18035                target: "target2".to_string(),
18036                last_modified: "2024-01-02".to_string(),
18037            },
18038        ];
18039
18040        assert_eq!(app.sqs_state.pipes.selected, 0);
18041        app.next_item();
18042        assert_eq!(app.sqs_state.pipes.selected, 1);
18043        app.prev_item();
18044        assert_eq!(app.sqs_state.pipes.selected, 0);
18045    }
18046
18047    #[test]
18048    fn test_sqs_tags_navigation() {
18049        let mut app = test_app();
18050        app.service_selected = true;
18051        app.mode = Mode::Normal;
18052        app.current_service = Service::SqsQueues;
18053        app.sqs_state.current_queue = Some("test-queue".to_string());
18054        app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
18055        app.sqs_state.tags.items = vec![
18056            SqsQueueTag {
18057                key: "Env".to_string(),
18058                value: "prod".to_string(),
18059            },
18060            SqsQueueTag {
18061                key: "Team".to_string(),
18062                value: "backend".to_string(),
18063            },
18064        ];
18065
18066        assert_eq!(app.sqs_state.tags.selected, 0);
18067        app.next_item();
18068        assert_eq!(app.sqs_state.tags.selected, 1);
18069        app.prev_item();
18070        assert_eq!(app.sqs_state.tags.selected, 0);
18071    }
18072
18073    #[test]
18074    fn test_sqs_queues_navigation() {
18075        let mut app = test_app();
18076        app.service_selected = true;
18077        app.mode = Mode::Normal;
18078        app.current_service = Service::SqsQueues;
18079        app.sqs_state.queues.items = vec![
18080            SqsQueue {
18081                name: "queue1".to_string(),
18082                url: "url1".to_string(),
18083                queue_type: "Standard".to_string(),
18084                created_timestamp: "".to_string(),
18085                messages_available: "0".to_string(),
18086                messages_in_flight: "0".to_string(),
18087                encryption: "Disabled".to_string(),
18088                content_based_deduplication: "Disabled".to_string(),
18089                last_modified_timestamp: "".to_string(),
18090                visibility_timeout: "".to_string(),
18091                message_retention_period: "".to_string(),
18092                maximum_message_size: "".to_string(),
18093                delivery_delay: "".to_string(),
18094                receive_message_wait_time: "".to_string(),
18095                high_throughput_fifo: "-".to_string(),
18096                deduplication_scope: "-".to_string(),
18097                fifo_throughput_limit: "-".to_string(),
18098                dead_letter_queue: "-".to_string(),
18099                messages_delayed: "0".to_string(),
18100                redrive_allow_policy: "-".to_string(),
18101                redrive_policy: "".to_string(),
18102                redrive_task_id: "-".to_string(),
18103                redrive_task_start_time: "-".to_string(),
18104                redrive_task_status: "-".to_string(),
18105                redrive_task_percent: "-".to_string(),
18106                redrive_task_destination: "-".to_string(),
18107            },
18108            SqsQueue {
18109                name: "queue2".to_string(),
18110                url: "url2".to_string(),
18111                queue_type: "Standard".to_string(),
18112                created_timestamp: "".to_string(),
18113                messages_available: "0".to_string(),
18114                messages_in_flight: "0".to_string(),
18115                encryption: "Disabled".to_string(),
18116                content_based_deduplication: "Disabled".to_string(),
18117                last_modified_timestamp: "".to_string(),
18118                visibility_timeout: "".to_string(),
18119                message_retention_period: "".to_string(),
18120                maximum_message_size: "".to_string(),
18121                delivery_delay: "".to_string(),
18122                receive_message_wait_time: "".to_string(),
18123                high_throughput_fifo: "-".to_string(),
18124                deduplication_scope: "-".to_string(),
18125                fifo_throughput_limit: "-".to_string(),
18126                dead_letter_queue: "-".to_string(),
18127                messages_delayed: "0".to_string(),
18128                redrive_allow_policy: "-".to_string(),
18129                redrive_policy: "".to_string(),
18130                redrive_task_id: "-".to_string(),
18131                redrive_task_start_time: "-".to_string(),
18132                redrive_task_status: "-".to_string(),
18133                redrive_task_percent: "-".to_string(),
18134                redrive_task_destination: "-".to_string(),
18135            },
18136        ];
18137
18138        assert_eq!(app.sqs_state.queues.selected, 0);
18139        app.next_item();
18140        assert_eq!(app.sqs_state.queues.selected, 1);
18141        app.prev_item();
18142        assert_eq!(app.sqs_state.queues.selected, 0);
18143    }
18144
18145    #[test]
18146    fn test_sqs_subscriptions_navigation() {
18147        let mut app = test_app();
18148        app.service_selected = true;
18149        app.mode = Mode::Normal;
18150        app.current_service = Service::SqsQueues;
18151        app.sqs_state.current_queue = Some("test-queue".to_string());
18152        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
18153        app.sqs_state.subscriptions.items = vec![
18154            SnsSubscription {
18155                subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
18156                topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
18157            },
18158            SnsSubscription {
18159                subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
18160                topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
18161            },
18162        ];
18163
18164        assert_eq!(app.sqs_state.subscriptions.selected, 0);
18165        app.next_item();
18166        assert_eq!(app.sqs_state.subscriptions.selected, 1);
18167        app.prev_item();
18168        assert_eq!(app.sqs_state.subscriptions.selected, 0);
18169    }
18170
18171    #[test]
18172    fn test_sqs_subscription_region_dropdown_navigation() {
18173        let mut app = test_app();
18174        app.service_selected = true;
18175        app.mode = Mode::FilterInput;
18176        app.current_service = Service::SqsQueues;
18177        app.sqs_state.current_queue = Some("test-queue".to_string());
18178        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
18179        app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
18180
18181        assert_eq!(app.sqs_state.subscription_region_selected, 0);
18182        app.next_item();
18183        assert_eq!(app.sqs_state.subscription_region_selected, 1);
18184        app.next_item();
18185        assert_eq!(app.sqs_state.subscription_region_selected, 2);
18186        app.prev_item();
18187        assert_eq!(app.sqs_state.subscription_region_selected, 1);
18188        app.prev_item();
18189        assert_eq!(app.sqs_state.subscription_region_selected, 0);
18190    }
18191
18192    #[test]
18193    fn test_sqs_subscription_region_selection() {
18194        let mut app = test_app();
18195        app.service_selected = true;
18196        app.mode = Mode::FilterInput;
18197        app.current_service = Service::SqsQueues;
18198        app.sqs_state.current_queue = Some("test-queue".to_string());
18199        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
18200        app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
18201        app.sqs_state.subscription_region_selected = 2; // us-west-1
18202
18203        assert_eq!(app.sqs_state.subscription_region_filter, "");
18204        app.handle_action(Action::ApplyFilter);
18205        assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
18206        assert_eq!(app.mode, Mode::Normal);
18207    }
18208
18209    #[test]
18210    fn test_sqs_subscription_region_change_resets_selection() {
18211        let mut app = test_app();
18212        app.service_selected = true;
18213        app.mode = Mode::FilterInput;
18214        app.current_service = Service::SqsQueues;
18215        app.sqs_state.current_queue = Some("test-queue".to_string());
18216        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
18217        app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
18218        app.sqs_state.subscription_region_selected = 0;
18219        app.sqs_state.subscriptions.selected = 5;
18220
18221        app.handle_action(Action::NextItem);
18222
18223        assert_eq!(app.sqs_state.subscription_region_selected, 1);
18224        assert_eq!(app.sqs_state.subscriptions.selected, 0);
18225    }
18226
18227    #[test]
18228    fn test_s3_object_filter_resets_selection() {
18229        let mut app = test_app();
18230        app.service_selected = true;
18231        app.current_service = Service::S3Buckets;
18232        app.s3_state.current_bucket = Some("test-bucket".to_string());
18233        app.s3_state.selected_row = 5;
18234        app.mode = Mode::FilterInput;
18235
18236        app.handle_action(Action::CloseMenu);
18237
18238        assert_eq!(app.s3_state.selected_row, 0);
18239        assert_eq!(app.mode, Mode::Normal);
18240    }
18241
18242    #[test]
18243    fn test_s3_bucket_filter_resets_selection() {
18244        let mut app = test_app();
18245        app.service_selected = true;
18246        app.current_service = Service::S3Buckets;
18247        app.s3_state.selected_row = 10;
18248        app.mode = Mode::FilterInput;
18249
18250        app.handle_action(Action::CloseMenu);
18251
18252        assert_eq!(app.s3_state.selected_row, 0);
18253        assert_eq!(app.mode, Mode::Normal);
18254    }
18255
18256    #[test]
18257    fn test_s3_selection_stays_in_bounds() {
18258        let mut app = test_app();
18259        app.service_selected = true;
18260        app.current_service = Service::S3Buckets;
18261        app.s3_state.selected_row = 0;
18262        app.s3_state.selected_object = 0;
18263
18264        // Simulate going up from row 0
18265        app.prev_item();
18266
18267        // Should stay at 0, not wrap to negative
18268        assert_eq!(app.s3_state.selected_row, 0);
18269        assert_eq!(app.s3_state.selected_object, 0);
18270    }
18271
18272    #[test]
18273    fn test_cfn_filter_resets_selection() {
18274        let mut app = test_app();
18275        app.service_selected = true;
18276        app.current_service = Service::CloudFormationStacks;
18277        app.cfn_state.table.selected = 10;
18278        app.mode = Mode::FilterInput;
18279
18280        app.handle_action(Action::CloseMenu);
18281
18282        assert_eq!(app.cfn_state.table.selected, 0);
18283        assert_eq!(app.mode, Mode::Normal);
18284    }
18285
18286    #[test]
18287    fn test_lambda_filter_resets_selection() {
18288        let mut app = test_app();
18289        app.service_selected = true;
18290        app.current_service = Service::LambdaFunctions;
18291        app.lambda_state.table.selected = 8;
18292        app.mode = Mode::FilterInput;
18293
18294        app.handle_action(Action::CloseMenu);
18295
18296        assert_eq!(app.lambda_state.table.selected, 0);
18297        assert_eq!(app.mode, Mode::Normal);
18298    }
18299
18300    #[test]
18301    fn test_sqs_filter_resets_selection() {
18302        let mut app = test_app();
18303        app.service_selected = true;
18304        app.current_service = Service::SqsQueues;
18305        app.sqs_state.queues.selected = 7;
18306        app.mode = Mode::FilterInput;
18307
18308        app.handle_action(Action::CloseMenu);
18309
18310        assert_eq!(app.sqs_state.queues.selected, 0);
18311        assert_eq!(app.mode, Mode::Normal);
18312    }
18313
18314    #[test]
18315    fn test_sqs_queues_list_shows_preferences() {
18316        let mut app = test_app();
18317        app.service_selected = true;
18318        app.current_service = Service::SqsQueues;
18319        app.mode = Mode::Normal;
18320
18321        app.handle_action(Action::OpenColumnSelector);
18322
18323        assert_eq!(app.mode, Mode::ColumnSelector);
18324    }
18325
18326    #[test]
18327    fn test_sqs_queue_policies_tab_no_preferences() {
18328        let mut app = test_app();
18329        app.service_selected = true;
18330        app.current_service = Service::SqsQueues;
18331        app.sqs_state.current_queue = Some("test-queue".to_string());
18332        app.sqs_state.detail_tab = SqsQueueDetailTab::QueuePolicies;
18333        app.mode = Mode::Normal;
18334
18335        app.handle_action(Action::OpenColumnSelector);
18336
18337        assert_eq!(app.mode, Mode::Normal);
18338    }
18339
18340    #[test]
18341    fn test_sqs_sns_subscriptions_tab_shows_preferences() {
18342        let mut app = test_app();
18343        app.service_selected = true;
18344        app.current_service = Service::SqsQueues;
18345        app.sqs_state.current_queue = Some("test-queue".to_string());
18346        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
18347        app.mode = Mode::Normal;
18348
18349        app.handle_action(Action::OpenColumnSelector);
18350
18351        assert_eq!(app.mode, Mode::ColumnSelector);
18352    }
18353
18354    #[test]
18355    fn test_sqs_monitoring_tab_no_preferences() {
18356        let mut app = test_app();
18357        app.service_selected = true;
18358        app.current_service = Service::SqsQueues;
18359        app.sqs_state.current_queue = Some("test-queue".to_string());
18360        app.sqs_state.detail_tab = SqsQueueDetailTab::Monitoring;
18361        app.mode = Mode::Normal;
18362
18363        app.handle_action(Action::OpenColumnSelector);
18364
18365        assert_eq!(app.mode, Mode::Normal);
18366    }
18367
18368    #[test]
18369    fn test_cfn_status_filter_change_resets_selection() {
18370        use crate::ui::cfn::STATUS_FILTER;
18371        let mut app = test_app();
18372        app.service_selected = true;
18373        app.current_service = Service::CloudFormationStacks;
18374        app.mode = Mode::FilterInput;
18375        app.cfn_state.input_focus = STATUS_FILTER;
18376        app.cfn_state.status_filter = CfnStatusFilter::All;
18377        app.cfn_state.table.items = vec![
18378            CfnStack {
18379                name: "stack1".to_string(),
18380                stack_id: "id1".to_string(),
18381                status: "CREATE_COMPLETE".to_string(),
18382                created_time: "2024-01-01".to_string(),
18383                updated_time: String::new(),
18384                deleted_time: String::new(),
18385                drift_status: String::new(),
18386                last_drift_check_time: String::new(),
18387                status_reason: String::new(),
18388                description: String::new(),
18389                detailed_status: String::new(),
18390                root_stack: String::new(),
18391                parent_stack: String::new(),
18392                termination_protection: false,
18393                iam_role: String::new(),
18394                tags: Vec::new(),
18395                stack_policy: String::new(),
18396                rollback_monitoring_time: String::new(),
18397                rollback_alarms: Vec::new(),
18398                notification_arns: Vec::new(),
18399            },
18400            CfnStack {
18401                name: "stack2".to_string(),
18402                stack_id: "id2".to_string(),
18403                status: "UPDATE_IN_PROGRESS".to_string(),
18404                created_time: "2024-01-02".to_string(),
18405                updated_time: String::new(),
18406                deleted_time: String::new(),
18407                drift_status: String::new(),
18408                last_drift_check_time: String::new(),
18409                status_reason: String::new(),
18410                description: String::new(),
18411                detailed_status: String::new(),
18412                root_stack: String::new(),
18413                parent_stack: String::new(),
18414                termination_protection: false,
18415                iam_role: String::new(),
18416                tags: Vec::new(),
18417                stack_policy: String::new(),
18418                rollback_monitoring_time: String::new(),
18419                rollback_alarms: Vec::new(),
18420                notification_arns: Vec::new(),
18421            },
18422        ];
18423        app.cfn_state.table.selected = 1;
18424
18425        app.handle_action(Action::NextItem);
18426
18427        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
18428        assert_eq!(app.cfn_state.table.selected, 0);
18429    }
18430
18431    #[test]
18432    fn test_cfn_view_nested_toggle_resets_selection() {
18433        use crate::ui::cfn::VIEW_NESTED;
18434        let mut app = test_app();
18435        app.service_selected = true;
18436        app.current_service = Service::CloudFormationStacks;
18437        app.mode = Mode::FilterInput;
18438        app.cfn_state.input_focus = VIEW_NESTED;
18439        app.cfn_state.view_nested = false;
18440        app.cfn_state.table.items = vec![CfnStack {
18441            name: "stack1".to_string(),
18442            stack_id: "id1".to_string(),
18443            status: "CREATE_COMPLETE".to_string(),
18444            created_time: "2024-01-01".to_string(),
18445            updated_time: String::new(),
18446            deleted_time: String::new(),
18447            drift_status: String::new(),
18448            last_drift_check_time: String::new(),
18449            status_reason: String::new(),
18450            description: String::new(),
18451            detailed_status: String::new(),
18452            root_stack: String::new(),
18453            parent_stack: String::new(),
18454            termination_protection: false,
18455            iam_role: String::new(),
18456            tags: Vec::new(),
18457            stack_policy: String::new(),
18458            rollback_monitoring_time: String::new(),
18459            rollback_alarms: Vec::new(),
18460            notification_arns: Vec::new(),
18461        }];
18462        app.cfn_state.table.selected = 5;
18463
18464        app.handle_action(Action::ToggleFilterCheckbox);
18465
18466        assert!(app.cfn_state.view_nested);
18467        assert_eq!(app.cfn_state.table.selected, 0);
18468    }
18469
18470    #[test]
18471    fn test_cfn_template_scroll_up() {
18472        let mut app = test_app();
18473        app.service_selected = true;
18474        app.current_service = Service::CloudFormationStacks;
18475        app.cfn_state.current_stack = Some("test-stack".to_string());
18476        app.cfn_state.detail_tab = CfnDetailTab::Template;
18477        app.cfn_state.template_scroll = 20;
18478
18479        app.page_up();
18480
18481        assert_eq!(app.cfn_state.template_scroll, 10);
18482    }
18483
18484    #[test]
18485    fn test_cfn_template_scroll_down() {
18486        let mut app = test_app();
18487        app.service_selected = true;
18488        app.current_service = Service::CloudFormationStacks;
18489        app.cfn_state.current_stack = Some("test-stack".to_string());
18490        app.cfn_state.detail_tab = CfnDetailTab::Template;
18491        app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
18492        app.cfn_state.template_scroll = 0;
18493
18494        app.page_down();
18495
18496        assert_eq!(app.cfn_state.template_scroll, 10);
18497    }
18498
18499    #[test]
18500    fn test_cfn_template_scroll_down_respects_max() {
18501        let mut app = test_app();
18502        app.service_selected = true;
18503        app.current_service = Service::CloudFormationStacks;
18504        app.cfn_state.current_stack = Some("test-stack".to_string());
18505        app.cfn_state.detail_tab = CfnDetailTab::Template;
18506        app.cfn_state.template_body = "line1\nline2\nline3".to_string();
18507        app.cfn_state.template_scroll = 0;
18508
18509        app.page_down();
18510
18511        // Should not scroll past the last line (3 lines = max scroll of 2)
18512        assert_eq!(app.cfn_state.template_scroll, 2);
18513    }
18514
18515    #[test]
18516    fn test_cfn_template_arrow_up() {
18517        let mut app = test_app();
18518        app.service_selected = true;
18519        app.current_service = Service::CloudFormationStacks;
18520        app.mode = Mode::Normal;
18521        app.cfn_state.current_stack = Some("test-stack".to_string());
18522        app.cfn_state.detail_tab = CfnDetailTab::Template;
18523        app.cfn_state.template_scroll = 5;
18524
18525        app.prev_item();
18526
18527        assert_eq!(app.cfn_state.template_scroll, 4);
18528    }
18529
18530    #[test]
18531    fn test_cfn_template_arrow_down() {
18532        let mut app = test_app();
18533        app.service_selected = true;
18534        app.current_service = Service::CloudFormationStacks;
18535        app.mode = Mode::Normal;
18536        app.cfn_state.current_stack = Some("test-stack".to_string());
18537        app.cfn_state.detail_tab = CfnDetailTab::Template;
18538        app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
18539        app.cfn_state.template_scroll = 2;
18540
18541        app.next_item();
18542
18543        assert_eq!(app.cfn_state.template_scroll, 3);
18544    }
18545
18546    #[test]
18547    fn test_cfn_template_arrow_down_respects_max() {
18548        let mut app = test_app();
18549        app.service_selected = true;
18550        app.current_service = Service::CloudFormationStacks;
18551        app.mode = Mode::Normal;
18552        app.cfn_state.current_stack = Some("test-stack".to_string());
18553        app.cfn_state.detail_tab = CfnDetailTab::Template;
18554        app.cfn_state.template_body = "line1\nline2".to_string();
18555        app.cfn_state.template_scroll = 1;
18556
18557        app.next_item();
18558
18559        // Should stay at max (2 lines = max scroll of 1)
18560        assert_eq!(app.cfn_state.template_scroll, 1);
18561    }
18562}
18563
18564#[cfg(test)]
18565mod lambda_version_tab_tests {
18566    use super::*;
18567    use crate::ui::iam::POLICY_TYPE_DROPDOWN;
18568    use test_helpers::*;
18569
18570    #[test]
18571    fn test_lambda_version_tab_cycling_next() {
18572        let mut app = test_app();
18573        app.current_service = Service::LambdaFunctions;
18574        app.lambda_state.current_function = Some("test-function".to_string());
18575        app.lambda_state.current_version = Some("1".to_string());
18576        app.lambda_state.detail_tab = LambdaDetailTab::Code;
18577
18578        // Code -> Monitor
18579        app.handle_action(Action::NextDetailTab);
18580        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
18581        assert!(app.lambda_state.metrics_loading);
18582
18583        // Monitor -> Configuration
18584        app.lambda_state.metrics_loading = false;
18585        app.handle_action(Action::NextDetailTab);
18586        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
18587
18588        // Configuration -> Code
18589        app.handle_action(Action::NextDetailTab);
18590        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
18591    }
18592
18593    #[test]
18594    fn test_lambda_version_tab_cycling_prev() {
18595        let mut app = test_app();
18596        app.current_service = Service::LambdaFunctions;
18597        app.lambda_state.current_function = Some("test-function".to_string());
18598        app.lambda_state.current_version = Some("1".to_string());
18599        app.lambda_state.detail_tab = LambdaDetailTab::Code;
18600
18601        // Code -> Configuration
18602        app.handle_action(Action::PrevDetailTab);
18603        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
18604
18605        // Configuration -> Monitor
18606        app.handle_action(Action::PrevDetailTab);
18607        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
18608        assert!(app.lambda_state.metrics_loading);
18609
18610        // Monitor -> Code
18611        app.lambda_state.metrics_loading = false;
18612        app.handle_action(Action::PrevDetailTab);
18613        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
18614    }
18615
18616    #[test]
18617    fn test_lambda_version_monitor_clears_metrics() {
18618        let mut app = test_app();
18619        app.current_service = Service::LambdaFunctions;
18620        app.lambda_state.current_function = Some("test-function".to_string());
18621        app.lambda_state.current_version = Some("1".to_string());
18622        app.lambda_state.detail_tab = LambdaDetailTab::Code;
18623
18624        // Add some fake metric data
18625        app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
18626        app.lambda_state.monitoring_scroll = 5;
18627
18628        // Switch to Monitor tab
18629        app.handle_action(Action::NextDetailTab);
18630
18631        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
18632        assert!(app.lambda_state.metrics_loading);
18633        assert_eq!(app.lambda_state.monitoring_scroll, 0);
18634        assert!(app.lambda_state.metric_data_invocations.is_empty());
18635    }
18636
18637    #[test]
18638    fn test_cfn_parameters_expand_collapse() {
18639        let mut app = test_app();
18640        app.current_service = Service::CloudFormationStacks;
18641        app.service_selected = true;
18642        app.cfn_state.current_stack = Some("test-stack".to_string());
18643        app.cfn_state.detail_tab = CfnDetailTab::Parameters;
18644        app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
18645            key: "Param1".to_string(),
18646            value: "Value1".to_string(),
18647            resolved_value: "Resolved1".to_string(),
18648        }];
18649        app.cfn_state.parameters.reset();
18650
18651        assert_eq!(app.cfn_state.parameters.expanded_item, None);
18652
18653        // Right arrow expands
18654        app.handle_action(Action::NextPane);
18655        assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
18656
18657        // Left arrow collapses
18658        app.handle_action(Action::PrevPane);
18659        assert_eq!(app.cfn_state.parameters.expanded_item, None);
18660    }
18661
18662    #[test]
18663    fn test_cfn_parameters_filter_resets_selection() {
18664        let mut app = test_app();
18665        app.current_service = Service::CloudFormationStacks;
18666        app.service_selected = true;
18667        app.cfn_state.current_stack = Some("test-stack".to_string());
18668        app.cfn_state.detail_tab = CfnDetailTab::Parameters;
18669        app.cfn_state.parameters.items = vec![
18670            rusticity_core::cfn::StackParameter {
18671                key: "DatabaseName".to_string(),
18672                value: "mydb".to_string(),
18673                resolved_value: "mydb".to_string(),
18674            },
18675            rusticity_core::cfn::StackParameter {
18676                key: "InstanceType".to_string(),
18677                value: "t2.micro".to_string(),
18678                resolved_value: "t2.micro".to_string(),
18679            },
18680            rusticity_core::cfn::StackParameter {
18681                key: "Environment".to_string(),
18682                value: "production".to_string(),
18683                resolved_value: "production".to_string(),
18684            },
18685        ];
18686        app.cfn_state.parameters.selected = 2; // Select third item
18687        app.mode = Mode::FilterInput;
18688        app.cfn_state.parameters_input_focus = InputFocus::Filter;
18689
18690        // Type a filter character - should reset selection
18691        app.handle_action(Action::FilterInput('D'));
18692        assert_eq!(app.cfn_state.parameters.selected, 0);
18693        assert_eq!(app.cfn_state.parameters.filter, "D");
18694
18695        // Select another item
18696        app.cfn_state.parameters.selected = 1;
18697
18698        // Type another character - should reset again
18699        app.handle_action(Action::FilterInput('a'));
18700        assert_eq!(app.cfn_state.parameters.selected, 0);
18701        assert_eq!(app.cfn_state.parameters.filter, "Da");
18702
18703        // Select another item
18704        app.cfn_state.parameters.selected = 1;
18705
18706        // Backspace - should also reset
18707        app.handle_action(Action::FilterBackspace);
18708        assert_eq!(app.cfn_state.parameters.selected, 0);
18709        assert_eq!(app.cfn_state.parameters.filter, "D");
18710    }
18711
18712    #[test]
18713    fn test_cfn_template_tab_no_preferences() {
18714        let mut app = test_app();
18715        app.current_service = Service::CloudFormationStacks;
18716        app.service_selected = true;
18717        app.cfn_state.current_stack = Some("test-stack".to_string());
18718        app.cfn_state.detail_tab = CfnDetailTab::Template;
18719        app.mode = Mode::Normal;
18720
18721        // Try to open preferences - should be ignored
18722        app.handle_action(Action::OpenColumnSelector);
18723        assert_eq!(app.mode, Mode::Normal); // Should stay in Normal mode
18724
18725        // GitSync tab should also not allow preferences
18726        app.cfn_state.detail_tab = CfnDetailTab::GitSync;
18727        app.handle_action(Action::OpenColumnSelector);
18728        assert_eq!(app.mode, Mode::Normal); // Should stay in Normal mode
18729
18730        // Parameters tab should allow preferences
18731        app.cfn_state.detail_tab = CfnDetailTab::Parameters;
18732        app.handle_action(Action::OpenColumnSelector);
18733        assert_eq!(app.mode, Mode::ColumnSelector); // Should open preferences
18734
18735        // Outputs tab should allow preferences
18736        app.mode = Mode::Normal;
18737        app.cfn_state.detail_tab = CfnDetailTab::Outputs;
18738        app.handle_action(Action::OpenColumnSelector);
18739        assert_eq!(app.mode, Mode::ColumnSelector); // Should open preferences
18740    }
18741
18742    #[test]
18743    fn test_iam_user_groups_tab_shows_preferences() {
18744        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
18745        app.current_service = Service::IamUsers;
18746        app.service_selected = true;
18747        app.mode = Mode::Normal;
18748        app.iam_state.current_user = Some("test-user".to_string());
18749        app.iam_state.user_tab = UserTab::Groups;
18750
18751        // Should allow opening preferences
18752        app.handle_action(Action::OpenColumnSelector);
18753        assert_eq!(app.mode, Mode::ColumnSelector);
18754    }
18755
18756    #[test]
18757    fn test_iam_user_tags_tab_shows_preferences() {
18758        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
18759        app.current_service = Service::IamUsers;
18760        app.service_selected = true;
18761        app.mode = Mode::Normal;
18762        app.iam_state.current_user = Some("test-user".to_string());
18763        app.iam_state.user_tab = UserTab::Tags;
18764
18765        // Should allow opening preferences
18766        app.handle_action(Action::OpenColumnSelector);
18767        assert_eq!(app.mode, Mode::ColumnSelector);
18768    }
18769
18770    #[test]
18771    fn test_iam_user_last_accessed_tab_shows_preferences() {
18772        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
18773        app.current_service = Service::IamUsers;
18774        app.service_selected = true;
18775        app.mode = Mode::Normal;
18776        app.iam_state.current_user = Some("test-user".to_string());
18777        app.iam_state.user_tab = UserTab::LastAccessed;
18778
18779        // Should allow opening preferences
18780        app.handle_action(Action::OpenColumnSelector);
18781        assert_eq!(app.mode, Mode::ColumnSelector);
18782    }
18783
18784    #[test]
18785    fn test_iam_user_security_credentials_tab_no_preferences() {
18786        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
18787        app.current_service = Service::IamUsers;
18788        app.service_selected = true;
18789        app.mode = Mode::Normal;
18790        app.iam_state.current_user = Some("test-user".to_string());
18791        app.iam_state.user_tab = UserTab::SecurityCredentials;
18792
18793        // Should NOT allow opening preferences
18794        app.handle_action(Action::OpenColumnSelector);
18795        assert_eq!(app.mode, Mode::Normal);
18796    }
18797
18798    #[test]
18799    fn test_iam_user_tabs_without_column_preferences() {
18800        let mut app = test_app();
18801        app.current_service = Service::IamUsers;
18802        app.service_selected = true;
18803        app.iam_state.current_user = Some("test-user".to_string());
18804        app.mode = Mode::Normal;
18805
18806        // Groups tab should allow preferences (page size)
18807        app.iam_state.user_tab = UserTab::Groups;
18808        app.handle_action(Action::OpenColumnSelector);
18809        assert_eq!(app.mode, Mode::ColumnSelector);
18810        app.mode = Mode::Normal;
18811
18812        // Tags tab should allow preferences (page size)
18813        app.iam_state.user_tab = UserTab::Tags;
18814        app.handle_action(Action::OpenColumnSelector);
18815        assert_eq!(app.mode, Mode::ColumnSelector);
18816        app.mode = Mode::Normal;
18817
18818        // SecurityCredentials tab should not allow preferences
18819        app.iam_state.user_tab = UserTab::SecurityCredentials;
18820        app.handle_action(Action::OpenColumnSelector);
18821        assert_eq!(app.mode, Mode::Normal);
18822
18823        // LastAccessed tab should allow preferences (page size)
18824        app.iam_state.user_tab = UserTab::LastAccessed;
18825        app.handle_action(Action::OpenColumnSelector);
18826        assert_eq!(app.mode, Mode::ColumnSelector);
18827        app.mode = Mode::Normal;
18828
18829        // Permissions tab should allow preferences
18830        app.iam_state.user_tab = UserTab::Permissions;
18831        app.handle_action(Action::OpenColumnSelector);
18832        assert_eq!(app.mode, Mode::ColumnSelector);
18833
18834        // User list (no current_user) should allow preferences
18835        app.mode = Mode::Normal;
18836        app.iam_state.current_user = None;
18837        app.handle_action(Action::OpenColumnSelector);
18838        assert_eq!(app.mode, Mode::ColumnSelector);
18839    }
18840
18841    #[test]
18842    fn test_iam_role_policies_dropdown_cycling() {
18843        let mut app = test_app();
18844        app.current_service = Service::IamRoles;
18845        app.service_selected = true;
18846        app.iam_state.current_role = Some("test-role".to_string());
18847        app.iam_state.role_tab = RoleTab::Permissions;
18848        app.mode = Mode::FilterInput;
18849        app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
18850        app.iam_state.policy_type_filter = "All types".to_string();
18851
18852        // Test next cycling
18853        app.next_item();
18854        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
18855        app.next_item();
18856        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
18857        app.next_item();
18858        assert_eq!(app.iam_state.policy_type_filter, "All types");
18859
18860        // Test prev cycling
18861        app.prev_item();
18862        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
18863        app.prev_item();
18864        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
18865        app.prev_item();
18866        assert_eq!(app.iam_state.policy_type_filter, "All types");
18867    }
18868
18869    #[test]
18870    fn test_iam_user_policies_dropdown_cycling() {
18871        let mut app = test_app();
18872        app.current_service = Service::IamUsers;
18873        app.service_selected = true;
18874        app.iam_state.current_user = Some("test-user".to_string());
18875        app.iam_state.user_tab = UserTab::Permissions;
18876        app.mode = Mode::FilterInput;
18877        app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
18878        app.iam_state.policy_type_filter = "All types".to_string();
18879
18880        // Test next cycling
18881        app.next_item();
18882        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
18883        app.next_item();
18884        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
18885        app.next_item();
18886        assert_eq!(app.iam_state.policy_type_filter, "All types");
18887
18888        // Test prev cycling
18889        app.prev_item();
18890        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
18891        app.prev_item();
18892        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
18893        app.prev_item();
18894        assert_eq!(app.iam_state.policy_type_filter, "All types");
18895    }
18896
18897    #[test]
18898    fn test_iam_role_tabs_without_column_preferences() {
18899        let mut app = test_app();
18900        app.current_service = Service::IamRoles;
18901        app.service_selected = true;
18902        app.iam_state.current_role = Some("test-role".to_string());
18903        app.mode = Mode::Normal;
18904
18905        // TrustRelationships tab should not allow preferences
18906        app.iam_state.role_tab = RoleTab::TrustRelationships;
18907        app.handle_action(Action::OpenColumnSelector);
18908        assert_eq!(app.mode, Mode::Normal);
18909
18910        // RevokeSessions tab should not allow preferences
18911        app.iam_state.role_tab = RoleTab::RevokeSessions;
18912        app.handle_action(Action::OpenColumnSelector);
18913        assert_eq!(app.mode, Mode::Normal);
18914
18915        // LastAccessed tab should allow preferences
18916        app.iam_state.role_tab = RoleTab::LastAccessed;
18917        app.handle_action(Action::OpenColumnSelector);
18918        assert_eq!(app.mode, Mode::ColumnSelector);
18919
18920        // Permissions tab should allow preferences
18921        app.mode = Mode::Normal;
18922        app.iam_state.role_tab = RoleTab::Permissions;
18923        app.handle_action(Action::OpenColumnSelector);
18924        assert_eq!(app.mode, Mode::ColumnSelector);
18925
18926        // Tags tab should allow preferences
18927        app.mode = Mode::Normal;
18928        app.iam_state.role_tab = RoleTab::Tags;
18929        app.handle_action(Action::OpenColumnSelector);
18930        assert_eq!(app.mode, Mode::ColumnSelector);
18931
18932        // Role list (no current_role) should allow preferences
18933        app.mode = Mode::Normal;
18934        app.iam_state.current_role = None;
18935        app.handle_action(Action::OpenColumnSelector);
18936        assert_eq!(app.mode, Mode::ColumnSelector);
18937    }
18938
18939    #[test]
18940    fn test_iam_role_tags_tab_cycling() {
18941        let mut app = test_app();
18942        app.current_service = Service::IamRoles;
18943        app.service_selected = true;
18944        app.iam_state.current_role = Some("test-role".to_string());
18945        app.iam_state.role_tab = RoleTab::Tags;
18946        app.mode = Mode::ColumnSelector;
18947        app.column_selector_index = 0;
18948
18949        // NextPreferences from column section -> page size
18950        app.handle_action(Action::NextPreferences);
18951        assert_eq!(app.column_selector_index, 4);
18952
18953        // NextPreferences from page size -> column section
18954        app.handle_action(Action::NextPreferences);
18955        assert_eq!(app.column_selector_index, 0);
18956
18957        // PrevPreferences from column section -> page size
18958        app.handle_action(Action::PrevPreferences);
18959        assert_eq!(app.column_selector_index, 4);
18960
18961        // PrevPreferences from page size -> column section
18962        app.handle_action(Action::PrevPreferences);
18963        assert_eq!(app.column_selector_index, 0);
18964    }
18965
18966    #[test]
18967    fn test_cfn_outputs_expand_collapse() {
18968        let mut app = test_app();
18969        app.current_service = Service::CloudFormationStacks;
18970        app.service_selected = true;
18971        app.cfn_state.current_stack = Some("test-stack".to_string());
18972        app.cfn_state.detail_tab = CfnDetailTab::Outputs;
18973        app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
18974            key: "Output1".to_string(),
18975            value: "Value1".to_string(),
18976            description: "Description1".to_string(),
18977            export_name: "Export1".to_string(),
18978        }];
18979        app.cfn_state.outputs.reset();
18980
18981        assert_eq!(app.cfn_state.outputs.expanded_item, None);
18982
18983        // Right arrow expands
18984        app.handle_action(Action::NextPane);
18985        assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
18986
18987        // Left arrow collapses
18988        app.handle_action(Action::PrevPane);
18989        assert_eq!(app.cfn_state.outputs.expanded_item, None);
18990    }
18991
18992    #[test]
18993    fn test_cfn_outputs_filter_resets_selection() {
18994        let mut app = test_app();
18995        app.current_service = Service::CloudFormationStacks;
18996        app.service_selected = true;
18997        app.cfn_state.current_stack = Some("test-stack".to_string());
18998        app.cfn_state.detail_tab = CfnDetailTab::Outputs;
18999        app.cfn_state.outputs.items = vec![
19000            rusticity_core::cfn::StackOutput {
19001                key: "ApiUrl".to_string(),
19002                value: "https://api.example.com".to_string(),
19003                description: "API endpoint".to_string(),
19004                export_name: "MyApiUrl".to_string(),
19005            },
19006            rusticity_core::cfn::StackOutput {
19007                key: "BucketName".to_string(),
19008                value: "my-bucket".to_string(),
19009                description: "S3 bucket".to_string(),
19010                export_name: "MyBucket".to_string(),
19011            },
19012        ];
19013        app.cfn_state.outputs.reset();
19014        app.cfn_state.outputs.selected = 1;
19015
19016        // Start filter mode
19017        app.handle_action(Action::StartFilter);
19018        assert_eq!(app.mode, Mode::FilterInput);
19019
19020        // Type a character - should reset selection
19021        app.handle_action(Action::FilterInput('A'));
19022        assert_eq!(app.cfn_state.outputs.selected, 0);
19023        assert_eq!(app.cfn_state.outputs.filter, "A");
19024
19025        // Type more
19026        app.cfn_state.outputs.selected = 1;
19027        app.handle_action(Action::FilterInput('p'));
19028        assert_eq!(app.cfn_state.outputs.selected, 0);
19029
19030        // Backspace should also reset selection
19031        app.cfn_state.outputs.selected = 1;
19032        app.handle_action(Action::FilterBackspace);
19033        assert_eq!(app.cfn_state.outputs.selected, 0);
19034    }
19035
19036    #[test]
19037    fn test_ec2_service_in_picker() {
19038        let app = test_app();
19039        assert!(app.service_picker.services.contains(&"EC2 › Instances"));
19040    }
19041
19042    #[test]
19043    fn test_ec2_state_filter_cycles() {
19044        let mut app = test_app();
19045        app.current_service = Service::Ec2Instances;
19046        app.service_selected = true;
19047        app.mode = Mode::FilterInput;
19048        app.ec2_state.input_focus = EC2_STATE_FILTER;
19049
19050        let initial = app.ec2_state.state_filter;
19051        assert_eq!(initial, Ec2StateFilter::AllStates);
19052
19053        // Cycle through filters using ToggleFilterCheckbox
19054        app.handle_action(Action::ToggleFilterCheckbox);
19055        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
19056
19057        app.handle_action(Action::ToggleFilterCheckbox);
19058        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
19059
19060        app.handle_action(Action::ToggleFilterCheckbox);
19061        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
19062
19063        app.handle_action(Action::ToggleFilterCheckbox);
19064        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
19065
19066        app.handle_action(Action::ToggleFilterCheckbox);
19067        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
19068
19069        app.handle_action(Action::ToggleFilterCheckbox);
19070        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
19071
19072        app.handle_action(Action::ToggleFilterCheckbox);
19073        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
19074    }
19075
19076    #[test]
19077    fn test_ec2_filter_resets_table() {
19078        let mut app = test_app();
19079        app.current_service = Service::Ec2Instances;
19080        app.service_selected = true;
19081        app.mode = Mode::FilterInput;
19082        app.ec2_state.input_focus = EC2_STATE_FILTER;
19083        app.ec2_state.table.selected = 5;
19084
19085        app.handle_action(Action::ToggleFilterCheckbox);
19086        assert_eq!(app.ec2_state.table.selected, 0);
19087    }
19088
19089    #[test]
19090    fn test_ec2_columns_visible() {
19091        let app = test_app();
19092        assert_eq!(app.ec2_visible_column_ids.len(), 16); // Default visible columns
19093        assert_eq!(app.ec2_column_ids.len(), 52); // Total available columns
19094    }
19095
19096    #[test]
19097    fn test_ec2_breadcrumbs() {
19098        let mut app = test_app();
19099        app.current_service = Service::Ec2Instances;
19100        app.service_selected = true;
19101        let breadcrumb = app.breadcrumbs();
19102        assert_eq!(breadcrumb, "EC2 > Instances");
19103    }
19104
19105    #[test]
19106    fn test_ec2_console_url() {
19107        let mut app = test_app();
19108        app.current_service = Service::Ec2Instances;
19109        app.service_selected = true;
19110        let url = app.get_console_url();
19111        assert!(url.contains("ec2"));
19112        assert!(url.contains("Instances"));
19113    }
19114
19115    #[test]
19116    fn test_ec2_filter_handling() {
19117        let mut app = test_app();
19118        app.current_service = Service::Ec2Instances;
19119        app.service_selected = true;
19120        app.mode = Mode::FilterInput;
19121
19122        app.handle_action(Action::FilterInput('t'));
19123        app.handle_action(Action::FilterInput('e'));
19124        app.handle_action(Action::FilterInput('s'));
19125        app.handle_action(Action::FilterInput('t'));
19126
19127        assert_eq!(app.ec2_state.table.filter, "test");
19128
19129        app.handle_action(Action::FilterBackspace);
19130        assert_eq!(app.ec2_state.table.filter, "tes");
19131    }
19132
19133    #[test]
19134    fn test_column_selector_page_down_ec2() {
19135        let mut app = test_app();
19136        app.current_service = Service::Ec2Instances;
19137        app.service_selected = true;
19138        app.mode = Mode::ColumnSelector;
19139        app.column_selector_index = 0;
19140
19141        app.handle_action(Action::PageDown);
19142        assert_eq!(app.column_selector_index, 10);
19143
19144        app.handle_action(Action::PageDown);
19145        assert_eq!(app.column_selector_index, 20);
19146    }
19147
19148    #[test]
19149    fn test_column_selector_page_up_ec2() {
19150        let mut app = test_app();
19151        app.current_service = Service::Ec2Instances;
19152        app.service_selected = true;
19153        app.mode = Mode::ColumnSelector;
19154        app.column_selector_index = 30;
19155
19156        app.handle_action(Action::PageUp);
19157        assert_eq!(app.column_selector_index, 20);
19158
19159        app.handle_action(Action::PageUp);
19160        assert_eq!(app.column_selector_index, 10);
19161    }
19162
19163    #[test]
19164    fn test_ec2_state_filter_dropdown_focus() {
19165        let mut app = test_app();
19166        app.current_service = Service::Ec2Instances;
19167        app.service_selected = true;
19168        app.mode = Mode::FilterInput;
19169
19170        // Tab to state filter
19171        app.handle_action(Action::NextFilterFocus);
19172        assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
19173
19174        // Dropdown should show when focused (tested in render)
19175        // Verify we can cycle the filter
19176        app.handle_action(Action::ToggleFilterCheckbox);
19177        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
19178    }
19179
19180    #[test]
19181    fn test_column_selector_ctrl_d_scrolling() {
19182        let mut app = test_app();
19183        app.current_service = Service::LambdaFunctions;
19184        app.mode = Mode::ColumnSelector;
19185        app.column_selector_index = 0;
19186
19187        // PageDown from 0 by 10 lands on blank row (10), skips to 11
19188        app.handle_action(Action::PageDown);
19189        assert_eq!(app.column_selector_index, 11);
19190
19191        // Second PageDown should be capped at max
19192        let max = app.get_column_selector_max();
19193        app.handle_action(Action::PageDown);
19194        assert_eq!(app.column_selector_index, max);
19195    }
19196
19197    #[test]
19198    fn test_column_selector_ctrl_u_scrolling() {
19199        let mut app = test_app();
19200        app.current_service = Service::CloudFormationStacks;
19201        app.mode = Mode::ColumnSelector;
19202        app.column_selector_index = 25;
19203
19204        app.handle_action(Action::PageUp);
19205        assert_eq!(app.column_selector_index, 15);
19206
19207        app.handle_action(Action::PageUp);
19208        assert_eq!(app.column_selector_index, 5);
19209    }
19210
19211    #[test]
19212    fn test_prev_preferences_lambda() {
19213        let mut app = test_app();
19214        app.current_service = Service::LambdaFunctions;
19215        app.mode = Mode::ColumnSelector;
19216        let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
19217        app.column_selector_index = page_size_idx;
19218
19219        app.handle_action(Action::PrevPreferences);
19220        assert_eq!(app.column_selector_index, 0);
19221
19222        app.handle_action(Action::PrevPreferences);
19223        assert_eq!(app.column_selector_index, page_size_idx);
19224    }
19225
19226    #[test]
19227    fn test_prev_preferences_cloudformation() {
19228        let mut app = test_app();
19229        app.current_service = Service::CloudFormationStacks;
19230        app.mode = Mode::ColumnSelector;
19231        let page_size_idx = app.cfn_column_ids.len() + 2;
19232        app.column_selector_index = page_size_idx;
19233
19234        app.handle_action(Action::PrevPreferences);
19235        assert_eq!(app.column_selector_index, 0);
19236
19237        app.handle_action(Action::PrevPreferences);
19238        assert_eq!(app.column_selector_index, page_size_idx);
19239    }
19240
19241    #[test]
19242    fn test_prev_preferences_alarms() {
19243        let mut app = test_app();
19244        app.current_service = Service::CloudWatchAlarms;
19245        app.mode = Mode::ColumnSelector;
19246        app.column_selector_index = 28; // WrapLines
19247
19248        app.handle_action(Action::PrevPreferences);
19249        assert_eq!(app.column_selector_index, 22); // PageSize
19250
19251        app.handle_action(Action::PrevPreferences);
19252        assert_eq!(app.column_selector_index, 18); // ViewAs
19253
19254        app.handle_action(Action::PrevPreferences);
19255        assert_eq!(app.column_selector_index, 0); // Columns
19256
19257        app.handle_action(Action::PrevPreferences);
19258        assert_eq!(app.column_selector_index, 28); // Wrap to WrapLines
19259    }
19260
19261    #[test]
19262    fn test_ec2_page_size_in_preferences() {
19263        let mut app = test_app();
19264        app.current_service = Service::Ec2Instances;
19265        app.mode = Mode::ColumnSelector;
19266        app.ec2_state.table.page_size = PageSize::Fifty;
19267
19268        // Navigate to page size section
19269        let page_size_idx = app.ec2_column_ids.len() + 3; // First page size option (10)
19270        app.column_selector_index = page_size_idx;
19271        app.handle_action(Action::ToggleColumn);
19272
19273        assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
19274    }
19275
19276    #[test]
19277    fn test_ec2_next_preferences_with_page_size() {
19278        let mut app = test_app();
19279        app.current_service = Service::Ec2Instances;
19280        app.mode = Mode::ColumnSelector;
19281        app.column_selector_index = 0;
19282
19283        let page_size_idx = app.ec2_column_ids.len() + 2;
19284        app.handle_action(Action::NextPreferences);
19285        assert_eq!(app.column_selector_index, page_size_idx);
19286
19287        app.handle_action(Action::NextPreferences);
19288        assert_eq!(app.column_selector_index, 0);
19289    }
19290
19291    #[test]
19292    fn test_ec2_dropdown_next_item() {
19293        let mut app = test_app();
19294        app.current_service = Service::Ec2Instances;
19295        app.mode = Mode::FilterInput;
19296        app.ec2_state.input_focus = EC2_STATE_FILTER;
19297        app.ec2_state.state_filter = Ec2StateFilter::AllStates;
19298
19299        app.handle_action(Action::NextItem);
19300        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
19301
19302        app.handle_action(Action::NextItem);
19303        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
19304    }
19305
19306    #[test]
19307    fn test_ec2_dropdown_prev_item() {
19308        let mut app = test_app();
19309        app.current_service = Service::Ec2Instances;
19310        app.mode = Mode::FilterInput;
19311        app.ec2_state.input_focus = EC2_STATE_FILTER;
19312        app.ec2_state.state_filter = Ec2StateFilter::Stopped;
19313
19314        app.handle_action(Action::PrevItem);
19315        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
19316
19317        app.handle_action(Action::PrevItem);
19318        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
19319    }
19320
19321    #[test]
19322    fn test_ec2_dropdown_cycles_with_arrows() {
19323        let mut app = test_app();
19324        app.current_service = Service::Ec2Instances;
19325        app.mode = Mode::FilterInput;
19326        app.ec2_state.input_focus = EC2_STATE_FILTER;
19327        app.ec2_state.state_filter = Ec2StateFilter::Stopping;
19328
19329        // Next wraps to AllStates
19330        app.handle_action(Action::NextItem);
19331        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
19332
19333        // Prev wraps to Stopping
19334        app.handle_action(Action::PrevItem);
19335        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
19336    }
19337
19338    #[test]
19339    fn test_collapse_row_ec2_instances() {
19340        let mut app = test_app();
19341        app.current_service = Service::Ec2Instances;
19342        app.ec2_state.table.expanded_item = Some(0);
19343
19344        app.handle_action(Action::CollapseRow);
19345        assert_eq!(app.ec2_state.table.expanded_item, None);
19346    }
19347
19348    #[test]
19349    fn test_collapse_row_ec2_tags() {
19350        let mut app = test_app();
19351        app.current_service = Service::Ec2Instances;
19352        app.ec2_state.current_instance = Some("i-123".to_string());
19353        app.ec2_state.detail_tab = Ec2DetailTab::Tags;
19354        app.ec2_state.tags.expanded_item = Some(1);
19355
19356        app.handle_action(Action::CollapseRow);
19357        assert_eq!(app.ec2_state.tags.expanded_item, None);
19358    }
19359
19360    #[test]
19361    fn test_collapse_row_cloudwatch_log_groups() {
19362        let mut app = test_app();
19363        app.current_service = Service::CloudWatchLogGroups;
19364        app.log_groups_state.log_groups.expanded_item = Some(2);
19365
19366        app.handle_action(Action::CollapseRow);
19367        assert_eq!(app.log_groups_state.log_groups.expanded_item, None);
19368    }
19369
19370    #[test]
19371    fn test_collapse_row_cloudwatch_alarms() {
19372        let mut app = test_app();
19373        app.current_service = Service::CloudWatchAlarms;
19374        app.alarms_state.table.expanded_item = Some(0);
19375
19376        app.handle_action(Action::CollapseRow);
19377        assert_eq!(app.alarms_state.table.expanded_item, None);
19378    }
19379
19380    #[test]
19381    fn test_collapse_row_lambda_functions() {
19382        let mut app = test_app();
19383        app.current_service = Service::LambdaFunctions;
19384        app.lambda_state.table.expanded_item = Some(1);
19385
19386        app.handle_action(Action::CollapseRow);
19387        assert_eq!(app.lambda_state.table.expanded_item, None);
19388    }
19389
19390    #[test]
19391    fn test_collapse_row_cfn_stacks() {
19392        let mut app = test_app();
19393        app.current_service = Service::CloudFormationStacks;
19394        app.cfn_state.table.expanded_item = Some(0);
19395
19396        app.handle_action(Action::CollapseRow);
19397        assert_eq!(app.cfn_state.table.expanded_item, None);
19398    }
19399
19400    #[test]
19401    fn test_collapse_row_cfn_resources() {
19402        let mut app = test_app();
19403        app.current_service = Service::CloudFormationStacks;
19404        app.cfn_state.current_stack = Some("test-stack".to_string());
19405        app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Resources;
19406        app.cfn_state.resources.expanded_item = Some(2);
19407
19408        app.handle_action(Action::CollapseRow);
19409        assert_eq!(app.cfn_state.resources.expanded_item, None);
19410    }
19411
19412    #[test]
19413    fn test_collapse_row_iam_users() {
19414        let mut app = test_app();
19415        app.current_service = Service::IamUsers;
19416        app.iam_state.users.expanded_item = Some(1);
19417
19418        app.handle_action(Action::CollapseRow);
19419        assert_eq!(app.iam_state.users.expanded_item, None);
19420    }
19421
19422    #[test]
19423    fn test_collapse_row_does_nothing_when_not_expanded() {
19424        let mut app = test_app();
19425        app.current_service = Service::Ec2Instances;
19426        app.ec2_state.table.expanded_item = None;
19427
19428        app.handle_action(Action::CollapseRow);
19429        assert_eq!(app.ec2_state.table.expanded_item, None);
19430    }
19431
19432    #[test]
19433    fn test_s3_collapse_expanded_folder_moves_to_parent() {
19434        let mut app = test_app();
19435        app.current_service = Service::S3Buckets;
19436        app.service_selected = true;
19437        app.mode = Mode::Normal;
19438
19439        // Add bucket with folder
19440        app.s3_state.buckets.items = vec![S3Bucket {
19441            name: "bucket1".to_string(),
19442            region: "us-east-1".to_string(),
19443            creation_date: "2024-01-01T00:00:00Z".to_string(),
19444        }];
19445
19446        // Expand bucket
19447        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19448        app.s3_state.bucket_preview.insert(
19449            "bucket1".to_string(),
19450            vec![S3Object {
19451                key: "folder1/".to_string(),
19452                size: 0,
19453                last_modified: "2024-01-01T00:00:00Z".to_string(),
19454                is_prefix: true,
19455                storage_class: String::new(),
19456            }],
19457        );
19458
19459        // Expand folder
19460        app.s3_state
19461            .expanded_prefixes
19462            .insert("folder1/".to_string());
19463        app.s3_state.prefix_preview.insert(
19464            "folder1/".to_string(),
19465            vec![S3Object {
19466                key: "folder1/file.txt".to_string(),
19467                size: 0,
19468                last_modified: "2024-01-01T00:00:00Z".to_string(),
19469                is_prefix: false,
19470                storage_class: String::new(),
19471            }],
19472        );
19473
19474        // Select the expanded folder (row 1)
19475        app.s3_state.selected_row = 1;
19476
19477        // Press Left to collapse
19478        app.handle_action(Action::PrevPane);
19479
19480        // Folder should be collapsed
19481        assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
19482        // Selection should move to parent (bucket at row 0)
19483        assert_eq!(app.s3_state.selected_row, 0);
19484    }
19485
19486    #[test]
19487    fn test_s3_collapse_hierarchy_level_by_level() {
19488        let mut app = test_app();
19489        app.current_service = Service::S3Buckets;
19490        app.service_selected = true;
19491        app.mode = Mode::Normal;
19492
19493        // Add bucket with 3-level hierarchy
19494        app.s3_state.buckets.items = vec![S3Bucket {
19495            name: "bucket1".to_string(),
19496            region: "us-east-1".to_string(),
19497            creation_date: "2024-01-01T00:00:00Z".to_string(),
19498        }];
19499
19500        // Level 1: bucket -> level1/
19501        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19502        app.s3_state.bucket_preview.insert(
19503            "bucket1".to_string(),
19504            vec![S3Object {
19505                key: "level1/".to_string(),
19506                size: 0,
19507                last_modified: "2024-01-01T00:00:00Z".to_string(),
19508                is_prefix: true,
19509                storage_class: String::new(),
19510            }],
19511        );
19512
19513        // Level 2: level1/ -> level2/
19514        app.s3_state.expanded_prefixes.insert("level1/".to_string());
19515        app.s3_state.prefix_preview.insert(
19516            "level1/".to_string(),
19517            vec![S3Object {
19518                key: "level1/level2/".to_string(),
19519                size: 0,
19520                last_modified: "2024-01-01T00:00:00Z".to_string(),
19521                is_prefix: true,
19522                storage_class: String::new(),
19523            }],
19524        );
19525
19526        // Level 3: level2/ -> file
19527        app.s3_state
19528            .expanded_prefixes
19529            .insert("level1/level2/".to_string());
19530        app.s3_state.prefix_preview.insert(
19531            "level1/level2/".to_string(),
19532            vec![S3Object {
19533                key: "level1/level2/file.txt".to_string(),
19534                size: 100,
19535                last_modified: "2024-01-01T00:00:00Z".to_string(),
19536                is_prefix: false,
19537                storage_class: String::new(),
19538            }],
19539        );
19540
19541        // Select deepest level (row 3: file)
19542        app.s3_state.selected_row = 3;
19543
19544        // First Left: move to parent (level2/ at row 2)
19545        app.handle_action(Action::PrevPane);
19546        assert_eq!(app.s3_state.selected_row, 2);
19547
19548        // Second Left: collapse level2/ and move to parent (level1/ at row 1)
19549        app.handle_action(Action::PrevPane);
19550        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
19551        assert_eq!(app.s3_state.selected_row, 1);
19552
19553        // Third Left: collapse level1/ and move to parent (bucket at row 0)
19554        app.handle_action(Action::PrevPane);
19555        assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
19556        assert_eq!(app.s3_state.selected_row, 0);
19557
19558        // Fourth Left: collapse bucket (stays at row 0)
19559        app.handle_action(Action::PrevPane);
19560        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
19561        assert_eq!(app.s3_state.selected_row, 0);
19562    }
19563
19564    #[test]
19565    fn test_ec2_instance_detail_tabs_no_preferences() {
19566        let mut app = test_app();
19567        app.current_service = Service::Ec2Instances;
19568        app.service_selected = true;
19569        app.ec2_state.table.expanded_item = Some(0);
19570        app.mode = Mode::Normal;
19571
19572        // Details tab should NOT allow preferences
19573        app.ec2_state.detail_tab = Ec2DetailTab::Details;
19574        app.handle_action(Action::OpenColumnSelector);
19575        assert_eq!(app.mode, Mode::Normal);
19576
19577        // StatusAndAlarms tab should NOT allow preferences
19578        app.ec2_state.detail_tab = Ec2DetailTab::StatusAndAlarms;
19579        app.handle_action(Action::OpenColumnSelector);
19580        assert_eq!(app.mode, Mode::Normal);
19581
19582        // Monitoring tab should NOT allow preferences
19583        app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
19584        app.handle_action(Action::OpenColumnSelector);
19585        assert_eq!(app.mode, Mode::Normal);
19586
19587        // Security tab should NOT allow preferences
19588        app.ec2_state.detail_tab = Ec2DetailTab::Security;
19589        app.handle_action(Action::OpenColumnSelector);
19590        assert_eq!(app.mode, Mode::Normal);
19591
19592        // Networking tab should NOT allow preferences
19593        app.ec2_state.detail_tab = Ec2DetailTab::Networking;
19594        app.handle_action(Action::OpenColumnSelector);
19595        assert_eq!(app.mode, Mode::Normal);
19596
19597        // Storage tab should NOT allow preferences
19598        app.ec2_state.detail_tab = Ec2DetailTab::Storage;
19599        app.handle_action(Action::OpenColumnSelector);
19600        assert_eq!(app.mode, Mode::Normal);
19601
19602        // Tags tab SHOULD allow preferences
19603        app.ec2_state.detail_tab = Ec2DetailTab::Tags;
19604        app.handle_action(Action::OpenColumnSelector);
19605        assert_eq!(app.mode, Mode::ColumnSelector);
19606    }
19607
19608    #[test]
19609    fn test_log_streams_filter_only_updates_when_focused() {
19610        let mut app = test_app();
19611        app.current_service = Service::CloudWatchLogGroups;
19612        app.service_selected = true;
19613        app.view_mode = ViewMode::Detail;
19614        app.mode = Mode::FilterInput;
19615        app.log_groups_state.stream_filter = "test".to_string();
19616
19617        // When filter is focused, typing should update filter
19618        app.log_groups_state.input_focus = InputFocus::Filter;
19619        app.handle_action(Action::FilterInput('x'));
19620        assert_eq!(app.log_groups_state.stream_filter, "testx");
19621
19622        // When pagination is focused, typing should NOT update filter
19623        app.log_groups_state.input_focus = InputFocus::Pagination;
19624        app.handle_action(Action::FilterInput('y'));
19625        assert_eq!(app.log_groups_state.stream_filter, "testx"); // unchanged
19626    }
19627
19628    #[test]
19629    fn test_log_streams_backspace_only_updates_when_focused() {
19630        let mut app = test_app();
19631        app.current_service = Service::CloudWatchLogGroups;
19632        app.service_selected = true;
19633        app.view_mode = ViewMode::Detail;
19634        app.mode = Mode::FilterInput;
19635        app.log_groups_state.stream_filter = "test".to_string();
19636
19637        // When filter is focused, backspace should update filter
19638        app.log_groups_state.input_focus = InputFocus::Filter;
19639        app.handle_action(Action::FilterBackspace);
19640        assert_eq!(app.log_groups_state.stream_filter, "tes");
19641
19642        // When pagination is focused, backspace should NOT update filter
19643        app.log_groups_state.input_focus = InputFocus::Pagination;
19644        app.handle_action(Action::FilterBackspace);
19645        assert_eq!(app.log_groups_state.stream_filter, "tes"); // unchanged
19646    }
19647
19648    #[test]
19649    fn test_log_groups_filter_only_updates_when_focused() {
19650        let mut app = test_app();
19651        app.current_service = Service::CloudWatchLogGroups;
19652        app.service_selected = true;
19653        app.view_mode = ViewMode::List;
19654        app.mode = Mode::FilterInput;
19655        app.log_groups_state.log_groups.filter = "test".to_string();
19656
19657        // When filter is focused, typing should update filter
19658        app.log_groups_state.input_focus = InputFocus::Filter;
19659        app.handle_action(Action::FilterInput('x'));
19660        assert_eq!(app.log_groups_state.log_groups.filter, "testx");
19661
19662        // When pagination is focused, typing should NOT update filter
19663        app.log_groups_state.input_focus = InputFocus::Pagination;
19664        app.handle_action(Action::FilterInput('y'));
19665        assert_eq!(app.log_groups_state.log_groups.filter, "testx"); // unchanged
19666    }
19667
19668    #[test]
19669    fn test_s3_bucket_collapse_nested_prefix_jumps_to_parent() {
19670        use S3Bucket;
19671        use S3Object;
19672
19673        let mut app = test_app();
19674        app.current_service = Service::S3Buckets;
19675        app.service_selected = true;
19676
19677        // Create bucket with nested prefixes
19678        app.s3_state.buckets.items = vec![S3Bucket {
19679            name: "test-bucket".to_string(),
19680            region: "us-east-1".to_string(),
19681            creation_date: String::new(),
19682        }];
19683
19684        // Expand bucket with folder1/
19685        app.s3_state
19686            .expanded_prefixes
19687            .insert("test-bucket".to_string());
19688        app.s3_state.bucket_preview.insert(
19689            "test-bucket".to_string(),
19690            vec![S3Object {
19691                key: "folder1/".to_string(),
19692                is_prefix: true,
19693                size: 0,
19694                last_modified: String::new(),
19695                storage_class: String::new(),
19696            }],
19697        );
19698
19699        // Expand folder1/ with folder2/
19700        app.s3_state
19701            .expanded_prefixes
19702            .insert("folder1/".to_string());
19703        app.s3_state.prefix_preview.insert(
19704            "folder1/".to_string(),
19705            vec![S3Object {
19706                key: "folder1/folder2/".to_string(),
19707                is_prefix: true,
19708                size: 0,
19709                last_modified: String::new(),
19710                storage_class: String::new(),
19711            }],
19712        );
19713
19714        // Select folder2/ (row 0: bucket, row 1: folder1, row 2: folder2)
19715        app.s3_state.selected_row = 2;
19716
19717        // Press left arrow - should collapse folder2 and jump to folder1
19718        app.handle_action(Action::CollapseRow);
19719
19720        // folder2 should be collapsed
19721        assert!(!app.s3_state.expanded_prefixes.contains("folder1/folder2/"));
19722        // Selection should move to folder1 (row 1)
19723        assert_eq!(app.s3_state.selected_row, 1);
19724    }
19725
19726    #[test]
19727    fn test_s3_bucket_collapse_expanded_folder_moves_to_parent() {
19728        use S3Bucket;
19729        use S3Object;
19730
19731        let mut app = test_app();
19732        app.current_service = Service::S3Buckets;
19733        app.service_selected = true;
19734
19735        // Create bucket with folder
19736        app.s3_state.buckets.items = vec![S3Bucket {
19737            name: "test-bucket".to_string(),
19738            region: "us-east-1".to_string(),
19739            creation_date: String::new(),
19740        }];
19741
19742        // Expand bucket with folder1/
19743        app.s3_state
19744            .expanded_prefixes
19745            .insert("test-bucket".to_string());
19746        app.s3_state.bucket_preview.insert(
19747            "test-bucket".to_string(),
19748            vec![S3Object {
19749                key: "folder1/".to_string(),
19750                is_prefix: true,
19751                size: 0,
19752                last_modified: String::new(),
19753                storage_class: String::new(),
19754            }],
19755        );
19756
19757        // Expand folder1/
19758        app.s3_state
19759            .expanded_prefixes
19760            .insert("folder1/".to_string());
19761        app.s3_state.prefix_preview.insert(
19762            "folder1/".to_string(),
19763            vec![S3Object {
19764                key: "folder1/file.txt".to_string(),
19765                is_prefix: false,
19766                size: 100,
19767                last_modified: String::new(),
19768                storage_class: String::new(),
19769            }],
19770        );
19771
19772        // Select folder1/ (row 0: bucket, row 1: folder1)
19773        app.s3_state.selected_row = 1;
19774
19775        // Press left arrow - should collapse folder1 and jump to bucket
19776        app.handle_action(Action::CollapseRow);
19777
19778        // folder1 should be collapsed
19779        assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
19780        // Selection should move to bucket (row 0)
19781        assert_eq!(app.s3_state.selected_row, 0);
19782    }
19783
19784    #[test]
19785    fn test_log_streams_pagination_limits_table_content() {
19786        let mut app = test_app();
19787        app.current_service = Service::CloudWatchLogGroups;
19788        app.service_selected = true;
19789        app.view_mode = ViewMode::Detail;
19790
19791        // Create 50 log streams
19792        app.log_groups_state.log_streams = (0..50)
19793            .map(|i| rusticity_core::LogStream {
19794                name: format!("stream-{}", i),
19795                creation_time: None,
19796                last_event_time: None,
19797            })
19798            .collect();
19799
19800        // Set page size to 10
19801        app.log_groups_state.stream_page_size = 10;
19802        app.log_groups_state.stream_current_page = 0;
19803
19804        // First page should show streams 0-9
19805        // (This is tested by rendering, but we can verify pagination logic)
19806        assert_eq!(app.log_groups_state.stream_page_size, 10);
19807        assert_eq!(app.log_groups_state.stream_current_page, 0);
19808
19809        // Navigate to page 2
19810        app.log_groups_state.stream_current_page = 1;
19811        assert_eq!(app.log_groups_state.stream_current_page, 1);
19812    }
19813
19814    #[test]
19815    fn test_log_streams_page_size_change_resets_page() {
19816        let mut app = test_app();
19817        app.current_service = Service::CloudWatchLogGroups;
19818        app.service_selected = true;
19819        app.view_mode = ViewMode::Detail;
19820        app.mode = Mode::ColumnSelector;
19821
19822        app.log_groups_state.stream_page_size = 10;
19823        app.log_groups_state.stream_current_page = 3;
19824
19825        // Change page size - should reset to page 0
19826        app.column_selector_index = app.cw_log_stream_column_ids.len() + 4; // 25 items
19827        app.handle_action(Action::ToggleColumn);
19828
19829        assert_eq!(app.log_groups_state.stream_page_size, 25);
19830        assert_eq!(app.log_groups_state.stream_current_page, 0);
19831    }
19832
19833    #[test]
19834    fn test_s3_objects_expanded_rows_stay_visible() {
19835        use S3Object;
19836
19837        let mut app = test_app();
19838        app.current_service = Service::S3Buckets;
19839        app.service_selected = true;
19840        app.mode = Mode::Normal;
19841        app.s3_state.current_bucket = Some("test-bucket".to_string());
19842
19843        // Create a folder with many nested items
19844        app.s3_state.objects = vec![S3Object {
19845            key: "folder1/".to_string(),
19846            is_prefix: true,
19847            size: 0,
19848            last_modified: String::new(),
19849            storage_class: String::new(),
19850        }];
19851
19852        // Expand folder1 with 20 files
19853        app.s3_state
19854            .expanded_prefixes
19855            .insert("folder1/".to_string());
19856        app.s3_state.prefix_preview.insert(
19857            "folder1/".to_string(),
19858            (0..20)
19859                .map(|i| S3Object {
19860                    key: format!("folder1/file{}.txt", i),
19861                    is_prefix: false,
19862                    size: 100,
19863                    last_modified: String::new(),
19864                    storage_class: String::new(),
19865                })
19866                .collect(),
19867        );
19868
19869        // Set viewport to show 10 rows
19870        app.s3_state.object_visible_rows.set(10);
19871        app.s3_state.object_scroll_offset = 0;
19872        app.s3_state.selected_object = 0; // folder1
19873
19874        // Navigate down through all items
19875        for i in 1..=20 {
19876            app.handle_action(Action::NextItem);
19877            assert_eq!(app.s3_state.selected_object, i);
19878
19879            // Check that selection is within visible range
19880            let visible_start = app.s3_state.object_scroll_offset;
19881            let visible_end = visible_start + app.s3_state.object_visible_rows.get();
19882            assert!(
19883                app.s3_state.selected_object >= visible_start
19884                    && app.s3_state.selected_object < visible_end,
19885                "Selection {} should be visible in range [{}, {})",
19886                app.s3_state.selected_object,
19887                visible_start,
19888                visible_end
19889            );
19890        }
19891    }
19892
19893    #[test]
19894    fn test_s3_bucket_error_rows_counted_in_total() {
19895        use S3Bucket;
19896
19897        let mut app = test_app();
19898        app.current_service = Service::S3Buckets;
19899        app.service_selected = true;
19900
19901        // Create buckets
19902        app.s3_state.buckets.items = vec![
19903            S3Bucket {
19904                name: "bucket1".to_string(),
19905                region: "us-east-1".to_string(),
19906                creation_date: String::new(),
19907            },
19908            S3Bucket {
19909                name: "bucket2".to_string(),
19910                region: "us-east-1".to_string(),
19911                creation_date: String::new(),
19912            },
19913        ];
19914
19915        // Expand bucket1 with error (long error message that will wrap)
19916        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19917        let long_error = "service error: unhandled error (PermanentRedirect): Error { code: PermanentRedirect, message: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint., request_id: 6D5VJ9TXYEMXSMXG, s3_extended_request_id: CGSwddO9ummjFYFHKyqNEU= }".to_string();
19918        app.s3_state
19919            .bucket_errors
19920            .insert("bucket1".to_string(), long_error.clone());
19921
19922        // Calculate total rows
19923        let total = app.calculate_total_bucket_rows();
19924
19925        // Should be: 2 buckets + error rows (long_error.len() / 120 rounded up)
19926        let error_rows = long_error.len().div_ceil(120);
19927        assert_eq!(total, 2 + error_rows);
19928    }
19929
19930    #[test]
19931    fn test_s3_bucket_with_error_can_be_collapsed() {
19932        use S3Bucket;
19933
19934        let mut app = test_app();
19935        app.current_service = Service::S3Buckets;
19936        app.service_selected = true;
19937        app.mode = Mode::Normal;
19938
19939        // Create bucket
19940        app.s3_state.buckets.items = vec![S3Bucket {
19941            name: "bucket1".to_string(),
19942            region: "us-east-1".to_string(),
19943            creation_date: String::new(),
19944        }];
19945
19946        // Expand bucket with error
19947        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19948        let error = "service error: PermanentRedirect".to_string();
19949        app.s3_state
19950            .bucket_errors
19951            .insert("bucket1".to_string(), error);
19952
19953        // Select the bucket row (row 0) - error rows are not selectable
19954        app.s3_state.selected_row = 0;
19955
19956        // Press left arrow to collapse
19957        app.handle_action(Action::CollapseRow);
19958
19959        // Bucket should be collapsed
19960        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
19961        // Selection should stay on bucket
19962        assert_eq!(app.s3_state.selected_row, 0);
19963    }
19964
19965    #[test]
19966    fn test_s3_bucket_collapse_on_bucket_row() {
19967        use S3Bucket;
19968
19969        let mut app = test_app();
19970        app.current_service = Service::S3Buckets;
19971        app.service_selected = true;
19972        app.mode = Mode::Normal;
19973
19974        // Create bucket
19975        app.s3_state.buckets.items = vec![S3Bucket {
19976            name: "bucket1".to_string(),
19977            region: "us-east-1".to_string(),
19978            creation_date: String::new(),
19979        }];
19980
19981        // Expand bucket with error
19982        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19983        let error = "service error: PermanentRedirect".to_string();
19984        app.s3_state
19985            .bucket_errors
19986            .insert("bucket1".to_string(), error);
19987
19988        // Select the bucket row itself (row 0)
19989        app.s3_state.selected_row = 0;
19990
19991        // Press left arrow to collapse
19992        app.handle_action(Action::CollapseRow);
19993
19994        // Bucket should be collapsed
19995        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
19996        // Selection should stay on bucket
19997        assert_eq!(app.s3_state.selected_row, 0);
19998    }
19999
20000    #[test]
20001    fn test_s3_bucket_collapse_adjusts_scroll_offset() {
20002        use S3Bucket;
20003
20004        let mut app = test_app();
20005        app.current_service = Service::S3Buckets;
20006        app.service_selected = true;
20007        app.mode = Mode::Normal;
20008
20009        // Create multiple buckets
20010        app.s3_state.buckets.items = (0..20)
20011            .map(|i| S3Bucket {
20012                name: format!("bucket{}", i),
20013                region: "us-east-1".to_string(),
20014                creation_date: String::new(),
20015            })
20016            .collect();
20017
20018        // Expand bucket 10 with a long error
20019        app.s3_state
20020            .expanded_prefixes
20021            .insert("bucket10".to_string());
20022        let long_error = "service error: unhandled error (PermanentRedirect): Error { code: PermanentRedirect, message: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint., request_id: 6D5VJ9TXYEMXSMXG }".to_string();
20023        app.s3_state
20024            .bucket_errors
20025            .insert("bucket10".to_string(), long_error.clone());
20026
20027        // Set viewport to 10 rows and scroll so bucket10 is at top
20028        app.s3_state.bucket_visible_rows.set(10);
20029        app.s3_state.bucket_scroll_offset = 10; // bucket10 is at row 10
20030
20031        // Select bucket10 (row 10) - error rows are not selectable
20032        app.s3_state.selected_row = 10;
20033
20034        // Press left arrow to collapse
20035        app.handle_action(Action::CollapseRow);
20036
20037        // Bucket should be collapsed
20038        assert!(!app.s3_state.expanded_prefixes.contains("bucket10"));
20039        // Selection should stay on bucket10 (row 10)
20040        assert_eq!(app.s3_state.selected_row, 10);
20041        // Scroll offset should be adjusted to show bucket10
20042        assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
20043        assert!(
20044            app.s3_state.selected_row
20045                < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
20046        );
20047    }
20048
20049    #[test]
20050    fn test_s3_collapse_second_to_last_bucket_with_last_having_error() {
20051        use S3Bucket;
20052
20053        let mut app = test_app();
20054        app.current_service = Service::S3Buckets;
20055        app.service_selected = true;
20056        app.mode = Mode::Normal;
20057
20058        // Create 3 buckets
20059        app.s3_state.buckets.items = vec![
20060            S3Bucket {
20061                name: "bucket1".to_string(),
20062                region: "us-east-1".to_string(),
20063                creation_date: String::new(),
20064            },
20065            S3Bucket {
20066                name: "bucket2".to_string(),
20067                region: "us-east-1".to_string(),
20068                creation_date: String::new(),
20069            },
20070            S3Bucket {
20071                name: "bucket3".to_string(),
20072                region: "us-east-1".to_string(),
20073                creation_date: String::new(),
20074            },
20075        ];
20076
20077        // Expand bucket2 (second to last) with preview
20078        app.s3_state.expanded_prefixes.insert("bucket2".to_string());
20079        app.s3_state.bucket_preview.insert(
20080            "bucket2".to_string(),
20081            vec![
20082                S3Object {
20083                    key: "folder1/".to_string(),
20084                    is_prefix: true,
20085                    size: 0,
20086                    last_modified: String::new(),
20087                    storage_class: String::new(),
20088                },
20089                S3Object {
20090                    key: "file1.txt".to_string(),
20091                    is_prefix: false,
20092                    size: 100,
20093                    last_modified: String::new(),
20094                    storage_class: String::new(),
20095                },
20096            ],
20097        );
20098
20099        // Expand bucket3 (last) with error
20100        app.s3_state.expanded_prefixes.insert("bucket3".to_string());
20101        let error = "service error: PermanentRedirect".to_string();
20102        app.s3_state
20103            .bucket_errors
20104            .insert("bucket3".to_string(), error);
20105
20106        // Set viewport
20107        app.s3_state.bucket_visible_rows.set(10);
20108        app.s3_state.bucket_scroll_offset = 0;
20109
20110        // Select last item in bucket2 (row 3: bucket1, bucket2, folder1, file1)
20111        app.s3_state.selected_row = 3;
20112
20113        // Collapse - should move to parent (bucket2)
20114        app.handle_action(Action::CollapseRow);
20115
20116        // bucket2 should still be expanded (we only moved to parent, didn't collapse)
20117        assert!(app.s3_state.expanded_prefixes.contains("bucket2"));
20118        // Selection should move to bucket2
20119        assert_eq!(app.s3_state.selected_row, 1);
20120        // Selection should be visible
20121        assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
20122        assert!(
20123            app.s3_state.selected_row
20124                < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
20125        );
20126    }
20127
20128    #[test]
20129    fn test_s3_collapse_bucket_with_error() {
20130        use S3Bucket;
20131
20132        let mut app = test_app();
20133        app.current_service = Service::S3Buckets;
20134        app.service_selected = true;
20135        app.mode = Mode::Normal;
20136
20137        app.s3_state.buckets.items = vec![
20138            S3Bucket {
20139                name: "bucket1".to_string(),
20140                region: "us-east-1".to_string(),
20141                creation_date: String::new(),
20142            },
20143            S3Bucket {
20144                name: "bucket2".to_string(),
20145                region: "us-east-1".to_string(),
20146                creation_date: String::new(),
20147            },
20148        ];
20149
20150        let error = "service error: unhandled error (PermanentRedirect)".to_string();
20151        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
20152        app.s3_state
20153            .bucket_errors
20154            .insert("bucket1".to_string(), error);
20155
20156        app.s3_state.bucket_visible_rows.set(10);
20157        app.s3_state.bucket_scroll_offset = 0;
20158
20159        // Select bucket1 (row 0) - error rows are not selectable
20160        app.s3_state.selected_row = 0;
20161
20162        // Collapse
20163        app.handle_action(Action::CollapseRow);
20164
20165        // bucket1 should be collapsed
20166        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
20167        assert_eq!(app.s3_state.selected_row, 0);
20168    }
20169
20170    #[test]
20171    fn test_s3_collapse_row_with_multiple_error_buckets() {
20172        use S3Bucket;
20173
20174        let mut app = test_app();
20175        app.current_service = Service::S3Buckets;
20176        app.service_selected = true;
20177        app.mode = Mode::Normal;
20178
20179        app.s3_state.buckets.items = vec![
20180            S3Bucket {
20181                name: "bucket1".to_string(),
20182                region: "us-east-1".to_string(),
20183                creation_date: String::new(),
20184            },
20185            S3Bucket {
20186                name: "bucket2".to_string(),
20187                region: "us-east-1".to_string(),
20188                creation_date: String::new(),
20189            },
20190            S3Bucket {
20191                name: "bucket3".to_string(),
20192                region: "us-east-1".to_string(),
20193                creation_date: String::new(),
20194            },
20195        ];
20196
20197        let error = "service error: unhandled error (PermanentRedirect)".to_string();
20198
20199        // Expand bucket1 with error
20200        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
20201        app.s3_state
20202            .bucket_errors
20203            .insert("bucket1".to_string(), error.clone());
20204
20205        // Expand bucket3 with error
20206        app.s3_state.expanded_prefixes.insert("bucket3".to_string());
20207        app.s3_state
20208            .bucket_errors
20209            .insert("bucket3".to_string(), error.clone());
20210
20211        app.s3_state.bucket_visible_rows.set(30);
20212        app.s3_state.bucket_scroll_offset = 0;
20213
20214        // Row 0: bucket1 (expanded with error - error rows not selectable)
20215        // Row 1: bucket2
20216        // Row 2: bucket3 (expanded with error - error rows not selectable)
20217        // Select bucket3
20218        app.s3_state.selected_row = 2;
20219
20220        app.handle_action(Action::CollapseRow);
20221
20222        // bucket3 should be collapsed, NOT bucket1
20223        assert!(
20224            !app.s3_state.expanded_prefixes.contains("bucket3"),
20225            "bucket3 should be collapsed"
20226        );
20227        assert!(
20228            app.s3_state.expanded_prefixes.contains("bucket1"),
20229            "bucket1 should still be expanded"
20230        );
20231        assert_eq!(app.s3_state.selected_row, 2);
20232    }
20233
20234    #[test]
20235    fn test_s3_collapse_row_nested_only_collapses_one_level() {
20236        use S3Bucket;
20237
20238        let mut app = test_app();
20239        app.current_service = Service::S3Buckets;
20240        app.service_selected = true;
20241        app.mode = Mode::Normal;
20242
20243        app.s3_state.buckets.items = vec![S3Bucket {
20244            name: "bucket1".to_string(),
20245            region: "us-east-1".to_string(),
20246            creation_date: String::new(),
20247        }];
20248
20249        // Level 1: bucket -> level1/
20250        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
20251        app.s3_state.bucket_preview.insert(
20252            "bucket1".to_string(),
20253            vec![S3Object {
20254                key: "level1/".to_string(),
20255                size: 0,
20256                last_modified: "2024-01-01T00:00:00Z".to_string(),
20257                is_prefix: true,
20258                storage_class: String::new(),
20259            }],
20260        );
20261
20262        // Level 2: level1/ -> level2/
20263        app.s3_state.expanded_prefixes.insert("level1/".to_string());
20264        app.s3_state.prefix_preview.insert(
20265            "level1/".to_string(),
20266            vec![S3Object {
20267                key: "level1/level2/".to_string(),
20268                size: 0,
20269                last_modified: "2024-01-01T00:00:00Z".to_string(),
20270                is_prefix: true,
20271                storage_class: String::new(),
20272            }],
20273        );
20274
20275        // Level 3: level2/ -> file
20276        app.s3_state
20277            .expanded_prefixes
20278            .insert("level1/level2/".to_string());
20279        app.s3_state.prefix_preview.insert(
20280            "level1/level2/".to_string(),
20281            vec![S3Object {
20282                key: "level1/level2/file.txt".to_string(),
20283                size: 100,
20284                last_modified: "2024-01-01T00:00:00Z".to_string(),
20285                is_prefix: false,
20286                storage_class: String::new(),
20287            }],
20288        );
20289
20290        app.s3_state.bucket_visible_rows.set(10);
20291
20292        // Select level2/ (row 2: bucket, level1, level2)
20293        app.s3_state.selected_row = 2;
20294
20295        // Collapse - should only collapse level2/, not the entire bucket
20296        app.handle_action(Action::CollapseRow);
20297
20298        // level2/ should be collapsed
20299        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
20300        // level1/ should still be expanded
20301        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
20302        // bucket should still be expanded
20303        assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
20304        // Selection should move to parent (level1/ at row 1)
20305        assert_eq!(app.s3_state.selected_row, 1);
20306    }
20307
20308    #[test]
20309    fn test_s3_collapse_row_deeply_nested_file() {
20310        use S3Bucket;
20311
20312        let mut app = test_app();
20313        app.current_service = Service::S3Buckets;
20314        app.service_selected = true;
20315        app.mode = Mode::Normal;
20316
20317        app.s3_state.buckets.items = vec![S3Bucket {
20318            name: "bucket1".to_string(),
20319            region: "us-east-1".to_string(),
20320            creation_date: String::new(),
20321        }];
20322
20323        // Level 1: bucket -> level1/
20324        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
20325        app.s3_state.bucket_preview.insert(
20326            "bucket1".to_string(),
20327            vec![S3Object {
20328                key: "level1/".to_string(),
20329                size: 0,
20330                last_modified: "2024-01-01T00:00:00Z".to_string(),
20331                is_prefix: true,
20332                storage_class: String::new(),
20333            }],
20334        );
20335
20336        // Level 2: level1/ -> level2/
20337        app.s3_state.expanded_prefixes.insert("level1/".to_string());
20338        app.s3_state.prefix_preview.insert(
20339            "level1/".to_string(),
20340            vec![S3Object {
20341                key: "level1/level2/".to_string(),
20342                size: 0,
20343                last_modified: "2024-01-01T00:00:00Z".to_string(),
20344                is_prefix: true,
20345                storage_class: String::new(),
20346            }],
20347        );
20348
20349        // Level 3: level2/ -> file
20350        app.s3_state
20351            .expanded_prefixes
20352            .insert("level1/level2/".to_string());
20353        app.s3_state.prefix_preview.insert(
20354            "level1/level2/".to_string(),
20355            vec![S3Object {
20356                key: "level1/level2/file.txt".to_string(),
20357                size: 100,
20358                last_modified: "2024-01-01T00:00:00Z".to_string(),
20359                is_prefix: false,
20360                storage_class: String::new(),
20361            }],
20362        );
20363
20364        app.s3_state.bucket_visible_rows.set(10);
20365
20366        // Select file (row 3: bucket, level1, level2, file)
20367        app.s3_state.selected_row = 3;
20368
20369        // Collapse - should move to parent (level2/)
20370        app.handle_action(Action::CollapseRow);
20371
20372        // All levels should still be expanded
20373        assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
20374        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
20375        assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
20376        // Selection should move to parent (level2/ at row 2)
20377        assert_eq!(app.s3_state.selected_row, 2);
20378    }
20379
20380    #[test]
20381    fn test_s3_bucket_pagination_adjusts_scroll() {
20382        use S3Bucket;
20383
20384        let mut app = test_app();
20385        app.current_service = Service::S3Buckets;
20386        app.service_selected = true;
20387        app.mode = Mode::Normal;
20388
20389        // Create 150 buckets (3 pages with page size 50)
20390        app.s3_state.buckets.items = (0..150)
20391            .map(|i| S3Bucket {
20392                name: format!("bucket{:03}", i),
20393                region: "us-east-1".to_string(),
20394                creation_date: String::new(),
20395            })
20396            .collect();
20397
20398        app.s3_state.bucket_visible_rows.set(20);
20399        app.s3_state.selected_row = 0;
20400        app.s3_state.bucket_scroll_offset = 0;
20401
20402        // Go to page 2 (should select row 50 and scroll to show it)
20403        app.go_to_page(2);
20404
20405        assert_eq!(app.s3_state.selected_row, 50);
20406        // Scroll offset should be adjusted to show page 2
20407        assert_eq!(app.s3_state.bucket_scroll_offset, 50);
20408
20409        // Go to page 3 (should select row 100 and scroll to show it)
20410        app.go_to_page(3);
20411
20412        assert_eq!(app.s3_state.selected_row, 100);
20413        assert_eq!(app.s3_state.bucket_scroll_offset, 100);
20414
20415        // Go to page 1 (should select row 0 and scroll to top)
20416        app.go_to_page(1);
20417
20418        assert_eq!(app.s3_state.selected_row, 0);
20419        assert_eq!(app.s3_state.bucket_scroll_offset, 0);
20420    }
20421
20422    #[test]
20423    fn test_s3_bucket_pagination_uses_page_size() {
20424        use S3Bucket;
20425
20426        let mut app = test_app();
20427        app.current_service = Service::S3Buckets;
20428        app.service_selected = true;
20429        app.mode = Mode::Normal;
20430
20431        // Create 100 buckets
20432        app.s3_state.buckets.items = (0..100)
20433            .map(|i| S3Bucket {
20434                name: format!("bucket{:03}", i),
20435                region: "us-east-1".to_string(),
20436                creation_date: String::new(),
20437            })
20438            .collect();
20439
20440        app.s3_state.bucket_visible_rows.set(20);
20441        app.s3_state.selected_row = 0;
20442
20443        // Default page size is 50
20444        assert_eq!(app.s3_state.buckets.page_size.value(), 50);
20445
20446        // Go to page 2 with default page size (50)
20447        app.go_to_page(2);
20448        assert_eq!(app.s3_state.selected_row, 50);
20449        assert_eq!(app.s3_state.bucket_scroll_offset, 50);
20450
20451        // Change page size to 25
20452        app.s3_state.buckets.page_size = crate::common::PageSize::TwentyFive;
20453        assert_eq!(app.s3_state.buckets.page_size.value(), 25);
20454
20455        // Go to page 2 with new page size (25)
20456        app.go_to_page(2);
20457        assert_eq!(app.s3_state.selected_row, 25);
20458        assert_eq!(app.s3_state.bucket_scroll_offset, 25);
20459    }
20460
20461    #[test]
20462    fn test_s3_bucket_page_size_limits_visible_rows() {
20463        use S3Bucket;
20464
20465        let mut app = test_app();
20466        app.current_service = Service::S3Buckets;
20467        app.service_selected = true;
20468        app.mode = Mode::Normal;
20469
20470        // Create 100 buckets
20471        app.s3_state.buckets.items = (0..100)
20472            .map(|i| S3Bucket {
20473                name: format!("bucket{:03}", i),
20474                region: "us-east-1".to_string(),
20475                creation_date: String::new(),
20476            })
20477            .collect();
20478
20479        // Set page size to 10
20480        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
20481        assert_eq!(app.s3_state.buckets.page_size.value(), 10);
20482
20483        // Calculate total rows - should only count buckets on current page
20484        let total_rows = app.calculate_total_bucket_rows();
20485        // With 100 buckets and page size 10, we should see 10 buckets per page
20486        // But calculate_total_bucket_rows returns ALL rows, not just current page
20487        // This is the issue - we need to paginate the display
20488        assert!(total_rows >= 10, "Should have at least 10 rows");
20489    }
20490
20491    #[test]
20492    fn test_s3_bucket_tab_cycling_in_filter() {
20493        use crate::common::InputFocus;
20494
20495        let mut app = test_app();
20496        app.current_service = Service::S3Buckets;
20497        app.mode = Mode::FilterInput;
20498
20499        // Start at Filter
20500        assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
20501
20502        // Tab to Pagination
20503        app.handle_action(Action::NextFilterFocus);
20504        assert_eq!(app.s3_state.input_focus, InputFocus::Pagination);
20505
20506        // Tab back to Filter
20507        app.handle_action(Action::NextFilterFocus);
20508        assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
20509    }
20510
20511    #[test]
20512    fn test_s3_bucket_pagination_navigation_with_arrows() {
20513        use S3Bucket;
20514
20515        let mut app = test_app();
20516        app.current_service = Service::S3Buckets;
20517        app.mode = Mode::FilterInput;
20518
20519        // Create 100 buckets
20520        app.s3_state.buckets.items = (0..100)
20521            .map(|i| S3Bucket {
20522                name: format!("bucket{:03}", i),
20523                region: "us-east-1".to_string(),
20524                creation_date: String::new(),
20525            })
20526            .collect();
20527
20528        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
20529        app.s3_state.selected_row = 0;
20530
20531        // Focus pagination
20532        app.s3_state.input_focus = crate::common::InputFocus::Pagination;
20533
20534        // Right arrow should go to next page (row 10)
20535        app.handle_action(Action::NextItem);
20536        assert_eq!(app.s3_state.selected_row, 10);
20537
20538        // Right arrow again (row 20)
20539        app.handle_action(Action::NextItem);
20540        assert_eq!(app.s3_state.selected_row, 20);
20541
20542        // Left arrow should go back (row 10)
20543        app.handle_action(Action::PrevItem);
20544        assert_eq!(app.s3_state.selected_row, 10);
20545    }
20546
20547    #[test]
20548    fn test_s3_bucket_go_to_page_shows_correct_buckets() {
20549        use S3Bucket;
20550
20551        let mut app = test_app();
20552        app.current_service = Service::S3Buckets;
20553        app.service_selected = true;
20554        app.mode = Mode::Normal;
20555
20556        // Create 100 buckets
20557        app.s3_state.buckets.items = (0..100)
20558            .map(|i| S3Bucket {
20559                name: format!("bucket{:03}", i),
20560                region: "us-east-1".to_string(),
20561                creation_date: String::new(),
20562            })
20563            .collect();
20564
20565        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
20566
20567        // Go to page 2 (should show buckets 10-19)
20568        app.go_to_page(2);
20569        assert_eq!(app.s3_state.selected_row, 10);
20570
20571        // Go to page 5 (should show buckets 40-49)
20572        app.go_to_page(5);
20573        assert_eq!(app.s3_state.selected_row, 40);
20574    }
20575
20576    #[test]
20577    fn test_s3_bucket_left_right_arrows_change_pages() {
20578        use S3Bucket;
20579
20580        let mut app = test_app();
20581        app.current_service = Service::S3Buckets;
20582        app.mode = Mode::FilterInput;
20583
20584        // Create 100 buckets
20585        app.s3_state.buckets.items = (0..100)
20586            .map(|i| S3Bucket {
20587                name: format!("bucket{:03}", i),
20588                region: "us-east-1".to_string(),
20589                creation_date: String::new(),
20590            })
20591            .collect();
20592
20593        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
20594        app.s3_state.selected_row = 0;
20595        app.s3_state.input_focus = crate::common::InputFocus::Pagination;
20596
20597        // Right arrow (PageDown) should go to next page
20598        app.handle_action(Action::PageDown);
20599        assert_eq!(app.s3_state.selected_row, 10);
20600
20601        // Right arrow again
20602        app.handle_action(Action::PageDown);
20603        assert_eq!(app.s3_state.selected_row, 20);
20604
20605        // Left arrow (PageUp) should go back
20606        app.handle_action(Action::PageUp);
20607        assert_eq!(app.s3_state.selected_row, 10);
20608    }
20609
20610    #[test]
20611    fn test_apig_detail_tab_navigation() {
20612        use crate::apig::api::RestApi;
20613        use crate::ui::apig::ApiDetailTab;
20614
20615        let mut app = test_app();
20616        app.current_service = Service::ApiGatewayApis;
20617        app.apig_state.current_api = Some(RestApi {
20618            id: "test123".to_string(),
20619            name: "Test API".to_string(),
20620            description: "Test".to_string(),
20621            created_date: "2024-01-01".to_string(),
20622            api_key_source: "HEADER".to_string(),
20623            endpoint_configuration: "REGIONAL".to_string(),
20624            protocol_type: "REST".to_string(),
20625            disable_execute_api_endpoint: false,
20626            status: "AVAILABLE".to_string(),
20627        });
20628
20629        // Should start on Routes tab
20630        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20631
20632        // Next tab - should stay on Routes (only tab)
20633        app.handle_action(Action::NextDetailTab);
20634        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20635
20636        // Prev tab - should stay on Routes (only tab)
20637        app.handle_action(Action::PrevDetailTab);
20638        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20639
20640        // Another next
20641        app.handle_action(Action::NextDetailTab);
20642        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20643
20644        // Prev tab should stay on Routes (only tab)
20645        app.handle_action(Action::PrevDetailTab);
20646        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20647
20648        // Next tab should stay on Routes (only tab)
20649        app.handle_action(Action::NextDetailTab);
20650        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20651    }
20652
20653    #[test]
20654    fn test_apig_routes_expand_collapse() {
20655        use crate::apig::api::RestApi;
20656        use crate::apig::route::Route;
20657        use crate::ui::apig::ApiDetailTab;
20658
20659        let mut app = test_app();
20660        app.current_service = Service::ApiGatewayApis;
20661        app.apig_state.current_api = Some(RestApi {
20662            id: "test123".to_string(),
20663            name: "Test API".to_string(),
20664            description: "Test".to_string(),
20665            created_date: "2024-01-01".to_string(),
20666            api_key_source: "HEADER".to_string(),
20667            endpoint_configuration: "REGIONAL".to_string(),
20668            protocol_type: "HTTP".to_string(),
20669            disable_execute_api_endpoint: false,
20670            status: "AVAILABLE".to_string(),
20671        });
20672        app.apig_state.detail_tab = ApiDetailTab::Routes;
20673
20674        // Create hierarchy with virtual parent
20675        let virtual_parent = Route {
20676            route_id: "virtual_/api".to_string(),
20677            route_key: "/api".to_string(),
20678            target: String::new(), // Empty target = virtual parent
20679            authorization_type: String::new(),
20680            api_key_required: false,
20681            display_name: String::new(),
20682            arn: String::new(),
20683        };
20684        let child_route = Route {
20685            route_id: "1".to_string(),
20686            route_key: "/api/users".to_string(),
20687            target: "integration1".to_string(),
20688            authorization_type: "NONE".to_string(),
20689            api_key_required: false,
20690            display_name: String::new(),
20691            arn: String::new(),
20692        };
20693
20694        app.apig_state.routes.items = vec![virtual_parent];
20695        app.apig_state
20696            .route_children
20697            .insert("/api".to_string(), vec![child_route]);
20698
20699        // Initially no routes expanded
20700        assert!(app.apig_state.expanded_routes.is_empty());
20701
20702        // Expand virtual parent
20703        app.apig_state.routes.selected = 0;
20704        app.handle_action(Action::ExpandRow);
20705        assert!(app.apig_state.expanded_routes.contains("/api"));
20706
20707        // Collapse virtual parent
20708        app.handle_action(Action::CollapseRow);
20709        assert!(!app.apig_state.expanded_routes.contains("/api"));
20710
20711        // Toggle expand again
20712        app.handle_action(Action::ExpandRow);
20713        assert!(app.apig_state.expanded_routes.contains("/api"));
20714    }
20715
20716    #[test]
20717    fn test_apig_routes_navigation() {
20718        use crate::apig::api::RestApi;
20719        use crate::apig::route::Route;
20720        use crate::ui::apig::ApiDetailTab;
20721
20722        let mut app = test_app();
20723        app.mode = Mode::Normal;
20724        app.service_selected = true;
20725        app.current_service = Service::ApiGatewayApis;
20726        app.apig_state.current_api = Some(RestApi {
20727            id: "test123".to_string(),
20728            name: "Test API".to_string(),
20729            description: "Test".to_string(),
20730            created_date: "2024-01-01".to_string(),
20731            api_key_source: "HEADER".to_string(),
20732            endpoint_configuration: "REGIONAL".to_string(),
20733            protocol_type: "HTTP".to_string(),
20734            disable_execute_api_endpoint: false,
20735            status: "AVAILABLE".to_string(),
20736        });
20737        app.apig_state.detail_tab = ApiDetailTab::Routes;
20738        app.apig_state.routes.items = vec![
20739            Route {
20740                route_id: "1".to_string(),
20741                route_key: "/api/users".to_string(),
20742                target: "integration1".to_string(),
20743                authorization_type: "NONE".to_string(),
20744                api_key_required: false,
20745                display_name: String::new(),
20746                arn: String::new(),
20747            },
20748            Route {
20749                route_id: "2".to_string(),
20750                route_key: "/health".to_string(),
20751                target: "integration2".to_string(),
20752                authorization_type: "NONE".to_string(),
20753                api_key_required: false,
20754                display_name: String::new(),
20755                arn: String::new(),
20756            },
20757        ];
20758
20759        // Start at first route
20760        assert_eq!(app.apig_state.routes.selected, 0);
20761
20762        // Navigate down
20763        app.handle_action(Action::NextItem);
20764        assert_eq!(app.apig_state.routes.selected, 1);
20765
20766        // Navigate down (should stay at last)
20767        app.handle_action(Action::NextItem);
20768        assert_eq!(app.apig_state.routes.selected, 1);
20769
20770        // Navigate up
20771        app.handle_action(Action::PrevItem);
20772        assert_eq!(app.apig_state.routes.selected, 0);
20773
20774        // Navigate up (should stay at first)
20775        app.handle_action(Action::PrevItem);
20776        assert_eq!(app.apig_state.routes.selected, 0);
20777    }
20778
20779    #[test]
20780    fn test_apig_routes_expand_jumps_to_child() {
20781        use crate::apig::api::RestApi;
20782        use crate::apig::route::Route;
20783        use crate::ui::apig::ApiDetailTab;
20784        use std::collections::HashMap;
20785
20786        let mut app = test_app();
20787        app.mode = Mode::Normal;
20788        app.service_selected = true;
20789        app.current_service = Service::ApiGatewayApis;
20790        app.apig_state.current_api = Some(RestApi {
20791            id: "test123".to_string(),
20792            name: "Test API".to_string(),
20793            description: "Test".to_string(),
20794            created_date: "2024-01-01".to_string(),
20795            api_key_source: "HEADER".to_string(),
20796            endpoint_configuration: "REGIONAL".to_string(),
20797            protocol_type: "HTTP".to_string(),
20798            disable_execute_api_endpoint: false,
20799            status: "AVAILABLE".to_string(),
20800        });
20801        app.apig_state.detail_tab = ApiDetailTab::Routes;
20802
20803        // Set up hierarchy: /api with child /api/users
20804        app.apig_state.routes.items = vec![Route {
20805            route_id: "virtual_/api".to_string(),
20806            route_key: "/api".to_string(),
20807            target: String::new(),
20808            authorization_type: String::new(),
20809            api_key_required: false,
20810            display_name: String::new(),
20811            arn: String::new(),
20812        }];
20813
20814        let mut children = HashMap::new();
20815        children.insert(
20816            "/api".to_string(),
20817            vec![Route {
20818                route_id: "1".to_string(),
20819                route_key: "/api/users".to_string(),
20820                target: "integration1".to_string(),
20821                authorization_type: "NONE".to_string(),
20822                api_key_required: false,
20823                display_name: String::new(),
20824                arn: String::new(),
20825            }],
20826        );
20827        app.apig_state.route_children = children;
20828
20829        // Start at /api (row 0)
20830        assert_eq!(app.apig_state.routes.selected, 0);
20831        assert!(!app.apig_state.expanded_routes.contains("/api"));
20832
20833        // First expand - should expand /api
20834        app.handle_action(Action::ExpandRow);
20835        assert!(app.apig_state.expanded_routes.contains("/api"));
20836        assert_eq!(app.apig_state.routes.selected, 0);
20837
20838        // Second expand - should jump to first child (row 1)
20839        app.handle_action(Action::ExpandRow);
20840        assert_eq!(app.apig_state.routes.selected, 1);
20841    }
20842
20843    #[test]
20844    fn test_apig_filter_only_when_focused() {
20845        use crate::apig::api::RestApi;
20846        use crate::ui::apig::ApiDetailTab;
20847
20848        let mut app = test_app();
20849        app.current_service = Service::ApiGatewayApis;
20850        app.apig_state.current_api = Some(RestApi {
20851            id: "test".to_string(),
20852            name: "Test API".to_string(),
20853            description: "Test".to_string(),
20854            created_date: "2024-01-01".to_string(),
20855            api_key_source: "HEADER".to_string(),
20856            endpoint_configuration: "REGIONAL".to_string(),
20857            protocol_type: "HTTP".to_string(),
20858            disable_execute_api_endpoint: false,
20859            status: "AVAILABLE".to_string(),
20860        });
20861        app.apig_state.detail_tab = ApiDetailTab::Routes;
20862        app.mode = Mode::FilterInput;
20863
20864        // When input_focus is NOT on Filter, typing should not update filter
20865        app.apig_state.input_focus = InputFocus::Pagination;
20866        app.handle_action(Action::FilterInput('x'));
20867        assert_eq!(app.apig_state.route_filter, "");
20868
20869        // When input_focus IS on Filter, typing should update filter
20870        app.apig_state.input_focus = InputFocus::Filter;
20871        app.handle_action(Action::FilterInput('x'));
20872        assert_eq!(app.apig_state.route_filter, "x");
20873    }
20874
20875    #[test]
20876    fn test_apig_routes_and_resources_use_same_render_function() {
20877        // Both routes and resources must call crate::ui::table::render_tree_table
20878        let source = include_str!("ui/apig.rs");
20879        let render_calls: Vec<_> = source
20880            .match_indices("crate::ui::table::render_tree_table")
20881            .collect();
20882
20883        // Should have exactly 2 calls: one for resources, one for routes
20884        assert_eq!(
20885            render_calls.len(),
20886            2,
20887            "Both routes and resources must use render_tree_table"
20888        );
20889    }
20890
20891    #[test]
20892    fn test_s3_uses_same_render_function() {
20893        // S3 objects must also call crate::ui::table::render_tree_table
20894        let source = include_str!("ui/s3.rs");
20895        let render_calls: Vec<_> = source
20896            .match_indices("crate::ui::table::render_tree_table")
20897            .collect();
20898
20899        // Should have at least 1 call for objects table
20900        assert!(!render_calls.is_empty(), "S3 must use render_tree_table");
20901    }
20902
20903    #[test]
20904    fn test_search_icon_has_proper_border_spacing() {
20905        // SEARCH_ICON should have dashes on both sides for proper border rendering
20906        // Should be "─ 🔍 ─" not "─ 🔍 " to avoid "╭N" appearance
20907        use crate::ui::SEARCH_ICON;
20908
20909        assert!(SEARCH_ICON.starts_with("─"), "Should start with dash");
20910        assert!(
20911            SEARCH_ICON.ends_with("─"),
20912            "Should end with dash for proper border spacing"
20913        );
20914        assert!(SEARCH_ICON.contains("🔍"), "Should contain search icon");
20915    }
20916
20917    #[test]
20918    fn test_apig_expand_with_filter() {
20919        use crate::apig::api::RestApi;
20920        use crate::apig::route::Route;
20921        use crate::ui::apig::ApiDetailTab;
20922        use std::collections::HashMap;
20923
20924        let mut app = test_app();
20925        app.current_service = Service::ApiGatewayApis;
20926        app.apig_state.current_api = Some(RestApi {
20927            id: "test123".to_string(),
20928            name: "Test API".to_string(),
20929            description: "Test".to_string(),
20930            created_date: "2024-01-01".to_string(),
20931            api_key_source: "HEADER".to_string(),
20932            endpoint_configuration: "REGIONAL".to_string(),
20933            protocol_type: "HTTP".to_string(),
20934            disable_execute_api_endpoint: false,
20935            status: "AVAILABLE".to_string(),
20936        });
20937        app.apig_state.detail_tab = ApiDetailTab::Routes;
20938
20939        // Setup routes with parent and child
20940        app.apig_state.routes.items = vec![Route {
20941            route_id: "0".to_string(),
20942            route_key: "/api".to_string(),
20943            target: "".to_string(),
20944            authorization_type: "NONE".to_string(),
20945            api_key_required: false,
20946            display_name: "/api".to_string(),
20947            arn: String::new(),
20948        }];
20949
20950        let mut children = HashMap::new();
20951        children.insert(
20952            "/api".to_string(),
20953            vec![Route {
20954                route_id: "1".to_string(),
20955                route_key: "GET".to_string(),
20956                target: "integration1".to_string(),
20957                authorization_type: "NONE".to_string(),
20958                api_key_required: false,
20959                display_name: "GET".to_string(),
20960                arn: String::new(),
20961            }],
20962        );
20963        app.apig_state.route_children = children;
20964
20965        // Apply filter that matches child
20966        app.apig_state.route_filter = "GET".to_string();
20967
20968        // Parent should be visible (because child matches)
20969        // Expand should work on filtered parent
20970        assert_eq!(app.apig_state.routes.selected, 0);
20971        assert!(!app.apig_state.expanded_routes.contains("/api"));
20972
20973        app.handle_action(Action::ExpandRow);
20974
20975        // Should expand the parent
20976        assert!(app.apig_state.expanded_routes.contains("/api"));
20977    }
20978
20979    #[test]
20980    fn test_apig_console_url_routes() {
20981        use crate::apig::api::RestApi;
20982        use crate::apig::route::Route;
20983        use crate::ui::apig::ApiDetailTab;
20984
20985        let mut app = test_app();
20986        app.current_service = Service::ApiGatewayApis;
20987        app.config.region = "us-east-1".to_string();
20988
20989        // Test API list URL
20990        let url = app.get_console_url();
20991        assert!(url.contains("apigateway/main/apis"));
20992        assert!(url.contains("region=us-east-1"));
20993
20994        // Test routes URL with selection
20995        app.apig_state.current_api = Some(RestApi {
20996            id: "2todvod3n0".to_string(),
20997            name: "Test API".to_string(),
20998            description: "Test".to_string(),
20999            created_date: "2024-01-01".to_string(),
21000            api_key_source: "HEADER".to_string(),
21001            endpoint_configuration: "REGIONAL".to_string(),
21002            protocol_type: "HTTP".to_string(),
21003            disable_execute_api_endpoint: false,
21004            status: "AVAILABLE".to_string(),
21005        });
21006        app.apig_state.detail_tab = ApiDetailTab::Routes;
21007        app.apig_state.routes.items = vec![Route {
21008            route_id: "eizmisr".to_string(),
21009            route_key: "GET /test".to_string(),
21010            target: "integration1".to_string(),
21011            authorization_type: "NONE".to_string(),
21012            api_key_required: false,
21013            display_name: "GET /test".to_string(),
21014            arn: String::new(),
21015        }];
21016        app.apig_state.routes.selected = 0;
21017
21018        let url = app.get_console_url();
21019        assert!(url.contains("apigateway/main/develop/routes"));
21020        assert!(url.contains("api=2todvod3n0"));
21021        assert!(url.contains("routes=eizmisr"));
21022        assert!(url.contains("region=us-east-1"));
21023    }
21024
21025    #[test]
21026    fn test_apig_console_url_resources() {
21027        use crate::apig::api::RestApi;
21028        use crate::apig::resource::Resource;
21029        use crate::ui::apig::ApiDetailTab;
21030
21031        let mut app = test_app();
21032        app.current_service = Service::ApiGatewayApis;
21033        app.config.region = "us-east-1".to_string();
21034
21035        // Test resources URL with selection
21036        app.apig_state.current_api = Some(RestApi {
21037            id: "2j9j50ze47".to_string(),
21038            name: "Test API".to_string(),
21039            description: "Test".to_string(),
21040            created_date: "2024-01-01".to_string(),
21041            api_key_source: "HEADER".to_string(),
21042            endpoint_configuration: "REGIONAL".to_string(),
21043            protocol_type: "REST".to_string(),
21044            disable_execute_api_endpoint: false,
21045            status: "AVAILABLE".to_string(),
21046        });
21047        app.apig_state.detail_tab = ApiDetailTab::Routes;
21048        app.apig_state.resources.items = vec![Resource {
21049            id: "abc123".to_string(),
21050            path: "/test".to_string(),
21051            parent_id: None,
21052            methods: vec![],
21053            display_name: "/test".to_string(),
21054            arn: String::new(),
21055        }];
21056        app.apig_state.resources.selected = 0;
21057
21058        let url = app.get_console_url();
21059        assert!(url.contains("apigateway/main/apis/2j9j50ze47/resources"));
21060        assert!(url.contains("api=2j9j50ze47"));
21061        assert!(url.contains("#abc123"));
21062        assert!(url.contains("region=us-east-1"));
21063    }
21064
21065    #[test]
21066    fn test_apig_console_url_routes_parent_vs_leaf() {
21067        use crate::apig::api::RestApi;
21068        use crate::apig::route::Route;
21069        use crate::ui::apig::ApiDetailTab;
21070        use std::collections::HashMap;
21071
21072        let mut app = test_app();
21073        app.current_service = Service::ApiGatewayApis;
21074        app.config.region = "us-east-1".to_string();
21075
21076        app.apig_state.current_api = Some(RestApi {
21077            id: "2todvod3n0".to_string(),
21078            name: "Test API".to_string(),
21079            description: "Test".to_string(),
21080            created_date: "2024-01-01".to_string(),
21081            api_key_source: "HEADER".to_string(),
21082            endpoint_configuration: "REGIONAL".to_string(),
21083            protocol_type: "HTTP".to_string(),
21084            disable_execute_api_endpoint: false,
21085            status: "AVAILABLE".to_string(),
21086        });
21087        app.apig_state.detail_tab = ApiDetailTab::Routes;
21088
21089        // Parent node (virtual path, no target)
21090        app.apig_state.routes.items = vec![Route {
21091            route_id: "parent".to_string(),
21092            route_key: "/v1/get/jobs".to_string(),
21093            target: "".to_string(), // Empty target = parent node
21094            authorization_type: "NONE".to_string(),
21095            api_key_required: false,
21096            display_name: "/v1/get/jobs".to_string(),
21097            arn: String::new(),
21098        }];
21099
21100        // Child leaf node (actual route with target)
21101        let mut children = HashMap::new();
21102        children.insert(
21103            "/v1/get/jobs".to_string(),
21104            vec![Route {
21105                route_id: "1iz9vtl".to_string(),
21106                route_key: "GET".to_string(),
21107                target: "integration1".to_string(), // Has target = leaf node
21108                authorization_type: "NONE".to_string(),
21109                api_key_required: false,
21110                display_name: "GET".to_string(),
21111                arn: String::new(),
21112            }],
21113        );
21114        app.apig_state.route_children = children;
21115
21116        // Select parent - should NOT include routes parameter
21117        app.apig_state.routes.selected = 0;
21118        let url = app.get_console_url();
21119        assert!(url.contains("apigateway/main/develop/routes"));
21120        assert!(url.contains("api=2todvod3n0"));
21121        assert!(
21122            !url.contains("routes="),
21123            "Parent node should not include routes parameter"
21124        );
21125
21126        // Expand parent and select child leaf
21127        app.apig_state
21128            .expanded_routes
21129            .insert("/v1/get/jobs".to_string());
21130        app.apig_state.routes.selected = 1;
21131        let url = app.get_console_url();
21132        assert!(
21133            url.contains("routes=1iz9vtl"),
21134            "Leaf node should include routes parameter: {}",
21135            url
21136        );
21137    }
21138
21139    #[test]
21140    fn test_apig_preferences_context() {
21141        use crate::apig::api::RestApi;
21142        use crate::ui::apig::ApiDetailTab;
21143
21144        let mut app = test_app();
21145        app.current_service = Service::ApiGatewayApis;
21146
21147        // In API list view - current_api is None
21148        assert!(
21149            app.apig_state.current_api.is_none(),
21150            "Should be in list view"
21151        );
21152
21153        // In detail view - current_api is Some
21154        app.apig_state.current_api = Some(RestApi {
21155            id: "test123".to_string(),
21156            name: "Test API".to_string(),
21157            description: "Test".to_string(),
21158            created_date: "2024-01-01".to_string(),
21159            api_key_source: "HEADER".to_string(),
21160            endpoint_configuration: "REGIONAL".to_string(),
21161            protocol_type: "HTTP".to_string(),
21162            disable_execute_api_endpoint: false,
21163            status: "AVAILABLE".to_string(),
21164        });
21165        app.apig_state.detail_tab = ApiDetailTab::Routes;
21166
21167        assert!(
21168            app.apig_state.current_api.is_some(),
21169            "Should be in detail view"
21170        );
21171        // Preferences rendering will check current_api.is_none() to decide what to show
21172    }
21173
21174    #[test]
21175    fn test_apig_route_columns() {
21176        use crate::apig::route::Column as RouteColumn;
21177
21178        // Verify all columns exist
21179        let cols = RouteColumn::all();
21180        assert_eq!(cols.len(), 5);
21181
21182        // Verify column IDs
21183        assert_eq!(RouteColumn::RouteKey.id(), "route_key");
21184        assert_eq!(RouteColumn::RouteId.id(), "route_id");
21185        assert_eq!(RouteColumn::Arn.id(), "arn");
21186        assert_eq!(RouteColumn::AuthorizationType.id(), "authorization_type");
21187        assert_eq!(RouteColumn::Target.id(), "target");
21188
21189        // Verify from_id
21190        assert_eq!(
21191            RouteColumn::from_id("route_key"),
21192            Some(RouteColumn::RouteKey)
21193        );
21194        assert_eq!(RouteColumn::from_id("arn"), Some(RouteColumn::Arn));
21195        assert_eq!(RouteColumn::from_id("invalid"), None);
21196    }
21197
21198    #[test]
21199    fn test_apig_yank_copies_route_arn() {
21200        use crate::apig::api::RestApi;
21201        use crate::ui::apig::ApiDetailTab;
21202
21203        let mut app = test_app();
21204        app.current_service = Service::ApiGatewayApis;
21205        app.apig_state.current_api = Some(RestApi {
21206            id: "test123".to_string(),
21207            name: "Test API".to_string(),
21208            description: "Test".to_string(),
21209            created_date: "2024-01-01".to_string(),
21210            api_key_source: "HEADER".to_string(),
21211            endpoint_configuration: "REGIONAL".to_string(),
21212            protocol_type: "HTTP".to_string(),
21213            disable_execute_api_endpoint: false,
21214            status: "AVAILABLE".to_string(),
21215        });
21216        app.apig_state.detail_tab = ApiDetailTab::Routes;
21217
21218        // Add routes with ARNs
21219        app.apig_state.routes.items = vec![
21220            Route {
21221                route_id: "route1".to_string(),
21222                route_key: "GET /users".to_string(),
21223                target: "integrations/abc".to_string(),
21224                authorization_type: "NONE".to_string(),
21225                api_key_required: false,
21226                display_name: "GET /users".to_string(),
21227                arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1".to_string(),
21228            },
21229            Route {
21230                route_id: "route2".to_string(),
21231                route_key: "POST /users".to_string(),
21232                target: "integrations/def".to_string(),
21233                authorization_type: "AWS_IAM".to_string(),
21234                api_key_required: true,
21235                display_name: "POST /users".to_string(),
21236                arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route2".to_string(),
21237            },
21238        ];
21239
21240        // Select first route
21241        app.apig_state.routes.selected = 0;
21242
21243        // Yank should copy ARN (we can't test clipboard directly, but we can verify the logic path)
21244        assert_eq!(
21245            app.apig_state.routes.items[0].arn,
21246            "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1"
21247        );
21248    }
21249
21250    #[test]
21251    fn test_apig_yank_ignores_empty_arn() {
21252        use crate::apig::api::RestApi;
21253        use crate::ui::apig::ApiDetailTab;
21254
21255        let mut app = test_app();
21256        app.current_service = Service::ApiGatewayApis;
21257        app.apig_state.current_api = Some(RestApi {
21258            id: "test123".to_string(),
21259            name: "Test API".to_string(),
21260            description: "Test".to_string(),
21261            created_date: "2024-01-01".to_string(),
21262            api_key_source: "HEADER".to_string(),
21263            endpoint_configuration: "REGIONAL".to_string(),
21264            protocol_type: "HTTP".to_string(),
21265            disable_execute_api_endpoint: false,
21266            status: "AVAILABLE".to_string(),
21267        });
21268        app.apig_state.detail_tab = ApiDetailTab::Routes;
21269
21270        // Add virtual parent node with empty ARN
21271        app.apig_state.routes.items = vec![Route {
21272            route_id: String::new(),
21273            route_key: "/users".to_string(),
21274            target: String::new(), // Virtual parent
21275            authorization_type: String::new(),
21276            api_key_required: false,
21277            display_name: "/users".to_string(),
21278            arn: String::new(), // Empty ARN
21279        }];
21280
21281        app.apig_state.routes.selected = 0;
21282
21283        // Verify empty ARN won't be copied
21284        assert!(
21285            app.apig_state.routes.items[0].arn.is_empty(),
21286            "Virtual parent should have empty ARN"
21287        );
21288    }
21289
21290    #[test]
21291    fn test_apig_route_column_toggle() {
21292        use crate::apig::api::RestApi;
21293        use crate::apig::route::Column as RouteColumn;
21294        use crate::keymap::Action;
21295        use crate::ui::apig::ApiDetailTab;
21296
21297        let mut app = test_app();
21298        app.current_service = Service::ApiGatewayApis;
21299        app.mode = Mode::ColumnSelector;
21300        app.apig_state.current_api = Some(RestApi {
21301            id: "test123".to_string(),
21302            name: "Test API".to_string(),
21303            description: "Test".to_string(),
21304            created_date: "2024-01-01".to_string(),
21305            api_key_source: "HEADER".to_string(),
21306            endpoint_configuration: "REGIONAL".to_string(),
21307            protocol_type: "HTTP".to_string(),
21308            disable_execute_api_endpoint: false,
21309            status: "AVAILABLE".to_string(),
21310        });
21311        app.apig_state.detail_tab = ApiDetailTab::Routes;
21312
21313        // Initially all columns visible
21314        assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21315
21316        // Try to toggle first column (Route) - should be locked, no effect
21317        app.column_selector_index = 1;
21318        app.handle_action(Action::ToggleColumn);
21319        assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21320        assert!(app
21321            .apig_route_visible_column_ids
21322            .contains(&RouteColumn::RouteKey.id()));
21323
21324        // Select ARN column (index 3 in UI)
21325        app.column_selector_index = 3;
21326        app.handle_action(Action::ToggleColumn);
21327
21328        // ARN should be removed
21329        assert_eq!(app.apig_route_visible_column_ids.len(), 4);
21330        assert!(!app
21331            .apig_route_visible_column_ids
21332            .contains(&RouteColumn::Arn.id()));
21333
21334        // Toggle again to add it back
21335        app.handle_action(Action::ToggleColumn);
21336        assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21337        assert!(app
21338            .apig_route_visible_column_ids
21339            .contains(&RouteColumn::Arn.id()));
21340    }
21341
21342    #[test]
21343    fn test_apig_resource_column_toggle() {
21344        use crate::apig::api::RestApi;
21345        use crate::apig::resource::Column as ResourceColumn;
21346        use crate::keymap::Action;
21347        use crate::ui::apig::ApiDetailTab;
21348
21349        let mut app = test_app();
21350        app.current_service = Service::ApiGatewayApis;
21351        app.mode = Mode::ColumnSelector;
21352        app.apig_state.current_api = Some(RestApi {
21353            id: "test123".to_string(),
21354            name: "Test API".to_string(),
21355            description: "Test".to_string(),
21356            created_date: "2024-01-01".to_string(),
21357            api_key_source: "HEADER".to_string(),
21358            endpoint_configuration: "REGIONAL".to_string(),
21359            protocol_type: "REST".to_string(), // REST API shows resources
21360            disable_execute_api_endpoint: false,
21361            status: "AVAILABLE".to_string(),
21362        });
21363        app.apig_state.detail_tab = ApiDetailTab::Routes; // Resources shown in Routes tab for REST APIs
21364
21365        // Initially all columns visible
21366        assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21367
21368        // Try to toggle first column (Resource) - should be locked, no effect
21369        app.column_selector_index = 1;
21370        app.handle_action(Action::ToggleColumn);
21371        assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21372        assert!(app
21373            .apig_resource_visible_column_ids
21374            .contains(&ResourceColumn::Path.id()));
21375
21376        // Select ARN column (index 3 in UI)
21377        app.column_selector_index = 3;
21378        app.handle_action(Action::ToggleColumn);
21379
21380        // ARN should be removed
21381        assert_eq!(app.apig_resource_visible_column_ids.len(), 2);
21382        assert!(!app
21383            .apig_resource_visible_column_ids
21384            .contains(&ResourceColumn::Arn.id()));
21385
21386        // Toggle again to add it back
21387        app.handle_action(Action::ToggleColumn);
21388        assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21389        assert!(app
21390            .apig_resource_visible_column_ids
21391            .contains(&ResourceColumn::Arn.id()));
21392    }
21393
21394    #[test]
21395    fn test_cloudtrail_filter_input() {
21396        let mut app = App::new_without_client("default".to_string(), None);
21397        app.service_selected = true;
21398        app.current_service = Service::CloudTrailEvents;
21399
21400        app.handle_action(Action::StartFilter);
21401        assert_eq!(app.mode, Mode::FilterInput);
21402
21403        app.cloudtrail_state.table.filter = "test".to_string();
21404
21405        assert_eq!(app.cloudtrail_state.table.filter, "test");
21406    }
21407
21408    #[test]
21409    fn test_cloudtrail_row_expansion() {
21410        let mut app = App::new_without_client("default".to_string(), None);
21411        app.service_selected = true;
21412        app.current_service = Service::CloudTrailEvents;
21413        app.cloudtrail_state.table.items = vec![CloudTrailEvent {
21414            event_name: "Event1".to_string(),
21415            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21416            username: "user1".to_string(),
21417            event_source: "s3.amazonaws.com".to_string(),
21418            resource_type: "Bucket".to_string(),
21419            resource_name: "my-bucket".to_string(),
21420            read_only: "false".to_string(),
21421            aws_region: "us-east-1".to_string(),
21422            event_id: "abc123".to_string(),
21423            access_key_id: "AKIA...".to_string(),
21424            source_ip_address: "1.2.3.4".to_string(),
21425            error_code: "".to_string(),
21426            request_id: "req-123".to_string(),
21427            event_type: "AwsApiCall".to_string(),
21428            cloud_trail_event_json: "{}".to_string(),
21429        }];
21430
21431        assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21432
21433        app.expand_row();
21434        assert_eq!(app.cloudtrail_state.table.expanded_item, Some(0));
21435
21436        app.collapse_row();
21437        assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21438    }
21439
21440    #[test]
21441    fn test_cloudtrail_service_initialization() {
21442        let app = App::new_without_client("default".to_string(), None);
21443        assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
21444        assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
21445    }
21446
21447    #[test]
21448    fn test_cloudtrail_service_name() {
21449        assert_eq!(
21450            Service::CloudTrailEvents.name(),
21451            "CloudTrail › Event History"
21452        );
21453    }
21454
21455    #[test]
21456    fn test_cloudtrail_in_service_picker() {
21457        let app = App::new_without_client("default".to_string(), None);
21458        assert!(app
21459            .service_picker
21460            .services
21461            .contains(&"CloudTrail › Event History"));
21462    }
21463
21464    #[test]
21465    fn test_cloudtrail_service_selection() {
21466        let mut app = App::new_without_client("default".to_string(), None);
21467        app.current_service = Service::CloudTrailEvents;
21468        app.service_selected = true;
21469
21470        assert_eq!(app.current_service, Service::CloudTrailEvents);
21471        assert!(app.service_selected);
21472    }
21473
21474    #[test]
21475    fn test_cloudtrail_filter_resets_selection() {
21476        let mut app = App::new_without_client("default".to_string(), None);
21477        app.service_selected = true;
21478        app.current_service = Service::CloudTrailEvents;
21479        app.cloudtrail_state.table.selected = 5;
21480        app.cloudtrail_state.table.expanded_item = Some(3);
21481
21482        app.handle_action(Action::StartFilter);
21483        app.apply_filter_operation(|_| {});
21484
21485        assert_eq!(app.cloudtrail_state.table.selected, 0);
21486        assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21487    }
21488
21489    #[test]
21490    fn test_cloudtrail_column_toggle() {
21491        let mut app = App::new_without_client("default".to_string(), None);
21492        app.service_selected = true;
21493        app.current_service = Service::CloudTrailEvents;
21494        app.mode = Mode::ColumnSelector;
21495
21496        // Initially 6 columns visible
21497        assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
21498
21499        // Toggle column at index 7 (ReadOnly - 7th column, index 1-14)
21500        app.column_selector_index = 7;
21501        app.handle_action(Action::ToggleColumn);
21502
21503        // Should now have 7 visible columns
21504        assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 7);
21505    }
21506
21507    #[test]
21508    fn test_cloudtrail_tab_cycles_filter_focus() {
21509        let mut app = App::new_without_client("default".to_string(), None);
21510        app.service_selected = true;
21511        app.current_service = Service::CloudTrailEvents;
21512        app.mode = Mode::FilterInput;
21513        app.cloudtrail_state.input_focus = InputFocus::Filter;
21514
21515        // Tab should cycle to Pagination
21516        app.handle_action(Action::NextFilterFocus);
21517        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
21518
21519        // Tab again should cycle back to Filter
21520        app.handle_action(Action::NextFilterFocus);
21521        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
21522    }
21523
21524    #[test]
21525    fn test_cloudtrail_shift_tab_cycles_filter_focus() {
21526        let mut app = App::new_without_client("default".to_string(), None);
21527        app.service_selected = true;
21528        app.current_service = Service::CloudTrailEvents;
21529        app.mode = Mode::FilterInput;
21530        app.cloudtrail_state.input_focus = InputFocus::Filter;
21531
21532        // Shift+Tab should cycle to Pagination
21533        app.handle_action(Action::PrevFilterFocus);
21534        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
21535
21536        // Shift+Tab again should cycle back to Filter
21537        app.handle_action(Action::PrevFilterFocus);
21538        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
21539    }
21540
21541    #[test]
21542    fn test_cloudtrail_detail_view_tab_cycles_focus() {
21543        let mut app = App::new_without_client("default".to_string(), None);
21544        app.service_selected = true;
21545        app.current_service = Service::CloudTrailEvents;
21546        app.mode = Mode::Normal;
21547        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21548            event_name: "PutObject".to_string(),
21549            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21550            username: "user".to_string(),
21551            event_source: "s3.amazonaws.com".to_string(),
21552            resource_type: "AWS::S3::Bucket".to_string(),
21553            resource_name: "my-bucket".to_string(),
21554            read_only: "false".to_string(),
21555            aws_region: "us-east-1".to_string(),
21556            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21557            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21558            source_ip_address: "192.0.2.1".to_string(),
21559            error_code: "".to_string(),
21560            request_id: "req-123".to_string(),
21561            event_type: "AwsApiCall".to_string(),
21562            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21563        });
21564
21565        // Default focus is Resources
21566        assert_eq!(
21567            app.cloudtrail_state.detail_focus,
21568            CloudTrailDetailFocus::Resources
21569        );
21570
21571        // Tab cycles to EventRecord
21572        app.handle_action(Action::NextDetailTab);
21573        assert_eq!(
21574            app.cloudtrail_state.detail_focus,
21575            CloudTrailDetailFocus::EventRecord
21576        );
21577
21578        // Tab cycles back to Resources
21579        app.handle_action(Action::NextDetailTab);
21580        assert_eq!(
21581            app.cloudtrail_state.detail_focus,
21582            CloudTrailDetailFocus::Resources
21583        );
21584    }
21585
21586    #[test]
21587    fn test_cloudtrail_detail_view_shift_tab_cycles_focus() {
21588        let mut app = App::new_without_client("default".to_string(), None);
21589        app.service_selected = true;
21590        app.current_service = Service::CloudTrailEvents;
21591        app.mode = Mode::Normal;
21592        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21593            event_name: "PutObject".to_string(),
21594            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21595            username: "user".to_string(),
21596            event_source: "s3.amazonaws.com".to_string(),
21597            resource_type: "AWS::S3::Bucket".to_string(),
21598            resource_name: "my-bucket".to_string(),
21599            read_only: "false".to_string(),
21600            aws_region: "us-east-1".to_string(),
21601            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21602            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21603            source_ip_address: "192.0.2.1".to_string(),
21604            error_code: "".to_string(),
21605            request_id: "req-123".to_string(),
21606            event_type: "AwsApiCall".to_string(),
21607            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21608        });
21609
21610        // Default focus is Resources
21611        assert_eq!(
21612            app.cloudtrail_state.detail_focus,
21613            CloudTrailDetailFocus::Resources
21614        );
21615
21616        // Shift+Tab cycles to EventRecord
21617        app.handle_action(Action::PrevDetailTab);
21618        assert_eq!(
21619            app.cloudtrail_state.detail_focus,
21620            CloudTrailDetailFocus::EventRecord
21621        );
21622
21623        // Shift+Tab cycles back to Resources
21624        app.handle_action(Action::PrevDetailTab);
21625        assert_eq!(
21626            app.cloudtrail_state.detail_focus,
21627            CloudTrailDetailFocus::Resources
21628        );
21629    }
21630
21631    #[test]
21632    fn test_cloudtrail_json_scroll_with_arrow_keys() {
21633        let mut app = App::new_without_client("default".to_string(), None);
21634        app.service_selected = true;
21635        app.current_service = Service::CloudTrailEvents;
21636        app.mode = Mode::Normal;
21637        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21638            event_name: "PutObject".to_string(),
21639            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21640            username: "user".to_string(),
21641            event_source: "s3.amazonaws.com".to_string(),
21642            resource_type: "AWS::S3::Bucket".to_string(),
21643            resource_name: "my-bucket".to_string(),
21644            read_only: "false".to_string(),
21645            aws_region: "us-east-1".to_string(),
21646            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21647            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21648            source_ip_address: "192.0.2.1".to_string(),
21649            error_code: "".to_string(),
21650            request_id: "req-123".to_string(),
21651            event_type: "AwsApiCall".to_string(),
21652            cloud_trail_event_json: (0..50)
21653                .map(|i| format!("line {}", i))
21654                .collect::<Vec<_>>()
21655                .join("\n"),
21656        });
21657        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21658        app.cloudtrail_state.event_json_scroll = 0;
21659
21660        // Down arrow scrolls down
21661        app.handle_action(Action::NextItem);
21662        assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
21663
21664        app.handle_action(Action::NextItem);
21665        assert_eq!(app.cloudtrail_state.event_json_scroll, 2);
21666
21667        // Up arrow scrolls up
21668        app.handle_action(Action::PrevItem);
21669        assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
21670
21671        app.handle_action(Action::PrevItem);
21672        assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
21673
21674        // Up at 0 stays at 0
21675        app.handle_action(Action::PrevItem);
21676        assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
21677    }
21678
21679    #[test]
21680    fn test_cloudtrail_tab_works_with_no_resources() {
21681        let mut app = App::new_without_client("default".to_string(), None);
21682        app.service_selected = true;
21683        app.current_service = Service::CloudTrailEvents;
21684        app.mode = Mode::Normal;
21685        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21686            event_name: "PutObject".to_string(),
21687            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21688            username: "user".to_string(),
21689            event_source: "s3.amazonaws.com".to_string(),
21690            resource_type: "".to_string(), // No resource
21691            resource_name: "".to_string(), // No resource
21692            read_only: "false".to_string(),
21693            aws_region: "us-east-1".to_string(),
21694            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21695            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21696            source_ip_address: "192.0.2.1".to_string(),
21697            error_code: "".to_string(),
21698            request_id: "req-123".to_string(),
21699            event_type: "AwsApiCall".to_string(),
21700            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21701        });
21702        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21703
21704        // Tab cycles to Resources even with no resources
21705        app.handle_action(Action::NextDetailTab);
21706        assert_eq!(
21707            app.cloudtrail_state.detail_focus,
21708            CloudTrailDetailFocus::Resources
21709        );
21710
21711        // Tab cycles back to EventRecord
21712        app.handle_action(Action::NextDetailTab);
21713        assert_eq!(
21714            app.cloudtrail_state.detail_focus,
21715            CloudTrailDetailFocus::EventRecord
21716        );
21717    }
21718
21719    #[test]
21720    fn test_cloudtrail_resources_expand_collapse() {
21721        let mut app = App::new_without_client("default".to_string(), None);
21722        app.service_selected = true;
21723        app.current_service = Service::CloudTrailEvents;
21724        app.mode = Mode::Normal;
21725        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21726            event_name: "PutObject".to_string(),
21727            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21728            username: "user".to_string(),
21729            event_source: "s3.amazonaws.com".to_string(),
21730            resource_type: "AWS::S3::Bucket".to_string(),
21731            resource_name: "my-bucket".to_string(),
21732            read_only: "false".to_string(),
21733            aws_region: "us-east-1".to_string(),
21734            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21735            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21736            source_ip_address: "192.0.2.1".to_string(),
21737            error_code: "".to_string(),
21738            request_id: "req-123".to_string(),
21739            event_type: "AwsApiCall".to_string(),
21740            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21741        });
21742        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21743        app.cloudtrail_state.resources_expanded_index = None;
21744
21745        // Right arrow expands
21746        app.handle_action(Action::ExpandRow);
21747        assert_eq!(app.cloudtrail_state.resources_expanded_index, Some(0));
21748
21749        // Left arrow collapses
21750        app.handle_action(Action::CollapseRow);
21751        assert_eq!(app.cloudtrail_state.resources_expanded_index, None);
21752    }
21753
21754    #[test]
21755    fn test_cloudtrail_event_json_no_column_selector() {
21756        let mut app = App::new_without_client("default".to_string(), None);
21757        app.service_selected = true;
21758        app.current_service = Service::CloudTrailEvents;
21759        app.mode = Mode::Normal;
21760        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21761            event_name: "PutObject".to_string(),
21762            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21763            username: "user".to_string(),
21764            event_source: "s3.amazonaws.com".to_string(),
21765            resource_type: "AWS::S3::Bucket".to_string(),
21766            resource_name: "my-bucket".to_string(),
21767            read_only: "false".to_string(),
21768            aws_region: "us-east-1".to_string(),
21769            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21770            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21771            source_ip_address: "192.0.2.1".to_string(),
21772            error_code: "".to_string(),
21773            request_id: "req-123".to_string(),
21774            event_type: "AwsApiCall".to_string(),
21775            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21776        });
21777        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21778
21779        // 'p' should not open column selector on Event JSON
21780        app.handle_action(Action::OpenColumnSelector);
21781        assert_eq!(app.mode, Mode::Normal);
21782
21783        // But should work on Resources
21784        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21785        app.handle_action(Action::OpenColumnSelector);
21786        assert_eq!(app.mode, Mode::ColumnSelector);
21787    }
21788
21789    #[test]
21790    fn test_cloudtrail_resources_preferences_show_only_3_columns() {
21791        let mut app = App::new_without_client("default".to_string(), None);
21792        app.service_selected = true;
21793        app.current_service = Service::CloudTrailEvents;
21794        app.mode = Mode::ColumnSelector;
21795        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21796            event_name: "PutObject".to_string(),
21797            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21798            username: "user".to_string(),
21799            event_source: "s3.amazonaws.com".to_string(),
21800            resource_type: "AWS::S3::Bucket".to_string(),
21801            resource_name: "my-bucket".to_string(),
21802            read_only: "false".to_string(),
21803            aws_region: "us-east-1".to_string(),
21804            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21805            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21806            source_ip_address: "192.0.2.1".to_string(),
21807            error_code: "".to_string(),
21808            request_id: "req-123".to_string(),
21809            event_type: "AwsApiCall".to_string(),
21810            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21811        });
21812        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21813
21814        // Should show 3 columns
21815        assert_eq!(app.get_column_count(), 3);
21816        assert_eq!(app.get_column_selector_max(), 3);
21817
21818        // All 3 columns should be visible by default
21819        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 3);
21820
21821        // Toggle first column off
21822        app.column_selector_index = 1;
21823        app.handle_action(Action::ToggleColumn);
21824        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
21825
21826        // Toggle second column off
21827        app.column_selector_index = 2;
21828        app.handle_action(Action::ToggleColumn);
21829        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
21830
21831        // Try to toggle last column off - should NOT work (at least 1 must remain)
21832        app.column_selector_index = 3;
21833        app.handle_action(Action::ToggleColumn);
21834        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
21835
21836        // Toggle first column back on
21837        app.column_selector_index = 1;
21838        app.handle_action(Action::ToggleColumn);
21839        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
21840    }
21841
21842    #[test]
21843    fn test_cloudtrail_resources_preferences_tab_cycles() {
21844        let mut app = App::new_without_client("default".to_string(), None);
21845        app.service_selected = true;
21846        app.current_service = Service::CloudTrailEvents;
21847        app.mode = Mode::ColumnSelector;
21848        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21849            event_name: "PutObject".to_string(),
21850            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21851            username: "user".to_string(),
21852            event_source: "s3.amazonaws.com".to_string(),
21853            resource_type: "AWS::S3::Bucket".to_string(),
21854            resource_name: "my-bucket".to_string(),
21855            read_only: "false".to_string(),
21856            aws_region: "us-east-1".to_string(),
21857            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21858            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21859            source_ip_address: "192.0.2.1".to_string(),
21860            error_code: "".to_string(),
21861            request_id: "req-123".to_string(),
21862            event_type: "AwsApiCall".to_string(),
21863            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21864        });
21865        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21866        app.column_selector_index = 1;
21867
21868        // Tab wraps to start (no page size section)
21869        app.handle_action(Action::NextPreferences);
21870        assert_eq!(app.column_selector_index, 0);
21871
21872        // Shift+Tab wraps to start (no page size section)
21873        app.column_selector_index = 1;
21874        app.handle_action(Action::PrevPreferences);
21875        assert_eq!(app.column_selector_index, 0);
21876    }
21877
21878    #[test]
21879    fn test_cloudtrail_resources_height_stays_constant_when_expanding() {
21880        // This test verifies that table height is pre-allocated for expansion
21881        // so content doesn't jump around when expanding/collapsing
21882
21883        // Height formula: (n_rows + n_visible_cols - 1) + 1 table header + 2 borders + 1 title
21884        // For 1 row with 3 visible columns: (1 + 3 - 1) + 1 + 2 + 1 = 7
21885
21886        // With 3 visible columns
21887        let visible_cols_3 = 3;
21888        let height_3 = (1 + visible_cols_3 - 1 + 1 + 2 + 1) as u16;
21889        assert_eq!(height_3, 7);
21890
21891        // With 2 visible columns (one toggled off)
21892        let visible_cols_2 = 2;
21893        let height_2 = (1 + visible_cols_2 - 1 + 1 + 2 + 1) as u16;
21894        assert_eq!(height_2, 6);
21895
21896        // With 1 visible column (two toggled off)
21897        let visible_cols_1 = 1;
21898        let height_1 = (1 + visible_cols_1 - 1 + 1 + 2 + 1) as u16;
21899        assert_eq!(height_1, 5);
21900    }
21901
21902    #[test]
21903    fn test_cloudtrail_default_focus_is_resources() {
21904        let app = App::new_without_client("default".to_string(), None);
21905        assert_eq!(
21906            app.cloudtrail_state.detail_focus,
21907            CloudTrailDetailFocus::Resources
21908        );
21909    }
21910
21911    fn setup_cloudtrail_pagination_test() -> App {
21912        let mut app = App::new_without_client("default".to_string(), None);
21913        app.service_selected = true;
21914        app.current_service = Service::CloudTrailEvents;
21915        app.mode = Mode::FilterInput;
21916        app.cloudtrail_state.input_focus = InputFocus::Pagination;
21917        app.cloudtrail_state.table.page_size = PageSize::Ten;
21918        app.cloudtrail_state.table.items = (0..25)
21919            .map(|i| CloudTrailEvent {
21920                event_name: format!("Event{}", i),
21921                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21922                username: "user".to_string(),
21923                event_source: "s3.amazonaws.com".to_string(),
21924                resource_type: "Bucket".to_string(),
21925                resource_name: "bucket".to_string(),
21926                read_only: "false".to_string(),
21927                aws_region: "us-east-1".to_string(),
21928                event_id: "id".to_string(),
21929                access_key_id: "key".to_string(),
21930                source_ip_address: "1.2.3.4".to_string(),
21931                error_code: "".to_string(),
21932                request_id: "req".to_string(),
21933                event_type: "AwsApiCall".to_string(),
21934                cloud_trail_event_json: "{}".to_string(),
21935            })
21936            .collect();
21937        app
21938    }
21939
21940    #[test]
21941    fn test_cloudtrail_pagination_navigation() {
21942        // Test right arrow navigation
21943        let mut app = setup_cloudtrail_pagination_test();
21944        assert_eq!(app.cloudtrail_state.table.selected, 0);
21945        app.handle_action(Action::NextItem);
21946        assert_eq!(app.cloudtrail_state.table.selected, 10);
21947        app.handle_action(Action::NextItem);
21948        assert_eq!(app.cloudtrail_state.table.selected, 20);
21949        app.handle_action(Action::NextItem);
21950        assert_eq!(app.cloudtrail_state.table.selected, 20);
21951
21952        // Test left arrow navigation
21953        app.handle_action(Action::PrevItem);
21954        assert_eq!(app.cloudtrail_state.table.selected, 10);
21955        app.handle_action(Action::PrevItem);
21956        assert_eq!(app.cloudtrail_state.table.selected, 0);
21957        app.handle_action(Action::PrevItem);
21958        assert_eq!(app.cloudtrail_state.table.selected, 0);
21959    }
21960
21961    #[test]
21962    fn test_cloudtrail_pagination_navigation_right_arrow() {
21963        let mut app = setup_cloudtrail_pagination_test();
21964        app.current_service = Service::CloudTrailEvents;
21965        app.mode = Mode::FilterInput;
21966        app.cloudtrail_state.input_focus = InputFocus::Pagination;
21967        app.cloudtrail_state.table.page_size = PageSize::Ten;
21968
21969        // Create 25 items (3 pages)
21970        app.cloudtrail_state.table.items = (0..25)
21971            .map(|i| CloudTrailEvent {
21972                event_name: format!("Event{}", i),
21973                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21974                username: "user".to_string(),
21975                event_source: "s3.amazonaws.com".to_string(),
21976                resource_type: "Bucket".to_string(),
21977                resource_name: "bucket".to_string(),
21978                read_only: "false".to_string(),
21979                aws_region: "us-east-1".to_string(),
21980                event_id: "id".to_string(),
21981                access_key_id: "key".to_string(),
21982                source_ip_address: "1.2.3.4".to_string(),
21983                error_code: "".to_string(),
21984                request_id: "req".to_string(),
21985                event_type: "AwsApiCall".to_string(),
21986                cloud_trail_event_json: "{}".to_string(),
21987            })
21988            .collect();
21989
21990        // Start at page 0
21991        assert_eq!(app.cloudtrail_state.table.selected, 0);
21992
21993        // Right arrow should go to page 1
21994        app.handle_action(Action::NextItem);
21995        assert_eq!(app.cloudtrail_state.table.selected, 10);
21996
21997        // Right arrow should go to page 2
21998        app.handle_action(Action::NextItem);
21999        assert_eq!(app.cloudtrail_state.table.selected, 20);
22000
22001        // Right arrow at last page should stay
22002        app.handle_action(Action::NextItem);
22003        assert_eq!(app.cloudtrail_state.table.selected, 20);
22004    }
22005
22006    #[test]
22007    fn test_cloudtrail_pagination_navigation_left_arrow() {
22008        let mut app = App::new_without_client("default".to_string(), None);
22009        app.service_selected = true;
22010        app.current_service = Service::CloudTrailEvents;
22011        app.mode = Mode::FilterInput;
22012        app.cloudtrail_state.input_focus = InputFocus::Pagination;
22013        app.cloudtrail_state.table.page_size = PageSize::Ten;
22014
22015        // Create 25 items (3 pages)
22016        app.cloudtrail_state.table.items = (0..25)
22017            .map(|i| CloudTrailEvent {
22018                event_name: format!("Event{}", i),
22019                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22020                username: "user".to_string(),
22021                event_source: "s3.amazonaws.com".to_string(),
22022                resource_type: "Bucket".to_string(),
22023                resource_name: "bucket".to_string(),
22024                read_only: "false".to_string(),
22025                aws_region: "us-east-1".to_string(),
22026                event_id: "id".to_string(),
22027                access_key_id: "key".to_string(),
22028                source_ip_address: "1.2.3.4".to_string(),
22029                error_code: "".to_string(),
22030                request_id: "req".to_string(),
22031                event_type: "AwsApiCall".to_string(),
22032                cloud_trail_event_json: "{}".to_string(),
22033            })
22034            .collect();
22035
22036        // Start at page 2
22037        app.cloudtrail_state.table.selected = 20;
22038
22039        // Left arrow should go to page 1
22040        app.handle_action(Action::PrevItem);
22041        assert_eq!(app.cloudtrail_state.table.selected, 10);
22042
22043        // Left arrow should go to page 0
22044        app.handle_action(Action::PrevItem);
22045        assert_eq!(app.cloudtrail_state.table.selected, 0);
22046
22047        // Left arrow at first page should stay
22048        app.handle_action(Action::PrevItem);
22049        assert_eq!(app.cloudtrail_state.table.selected, 0);
22050    }
22051
22052    #[test]
22053    fn test_cloudtrail_arrow_navigation() {
22054        let mut app = App::new_without_client("default".to_string(), None);
22055        app.service_selected = true;
22056        app.current_service = Service::CloudTrailEvents;
22057        app.mode = Mode::Normal;
22058
22059        // Create 5 items
22060        app.cloudtrail_state.table.items = (0..5)
22061            .map(|i| CloudTrailEvent {
22062                event_name: format!("Event{}", i),
22063                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22064                username: "user".to_string(),
22065                event_source: "s3.amazonaws.com".to_string(),
22066                resource_type: "Bucket".to_string(),
22067                resource_name: "bucket".to_string(),
22068                read_only: "false".to_string(),
22069                aws_region: "us-east-1".to_string(),
22070                event_id: "id".to_string(),
22071                access_key_id: "key".to_string(),
22072                source_ip_address: "1.2.3.4".to_string(),
22073                error_code: "".to_string(),
22074                request_id: "req".to_string(),
22075                event_type: "AwsApiCall".to_string(),
22076                cloud_trail_event_json: "{}".to_string(),
22077            })
22078            .collect();
22079
22080        // Down arrow should move to next item
22081        app.handle_action(Action::NextItem);
22082        assert_eq!(app.cloudtrail_state.table.selected, 1);
22083
22084        app.handle_action(Action::NextItem);
22085        assert_eq!(app.cloudtrail_state.table.selected, 2);
22086
22087        // Up arrow should move to previous item
22088        app.handle_action(Action::PrevItem);
22089        assert_eq!(app.cloudtrail_state.table.selected, 1);
22090    }
22091
22092    #[test]
22093    fn test_cloudtrail_page_down_navigation() {
22094        let mut app = App::new_without_client("default".to_string(), None);
22095        app.service_selected = true;
22096        app.current_service = Service::CloudTrailEvents;
22097        app.mode = Mode::Normal;
22098        app.cloudtrail_state.table.page_size = PageSize::Ten;
22099
22100        // Create 25 items
22101        app.cloudtrail_state.table.items = (0..25)
22102            .map(|i| CloudTrailEvent {
22103                event_name: format!("Event{}", i),
22104                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22105                username: "user".to_string(),
22106                event_source: "s3.amazonaws.com".to_string(),
22107                resource_type: "Bucket".to_string(),
22108                resource_name: "bucket".to_string(),
22109                read_only: "false".to_string(),
22110                aws_region: "us-east-1".to_string(),
22111                event_id: "id".to_string(),
22112                access_key_id: "key".to_string(),
22113                source_ip_address: "1.2.3.4".to_string(),
22114                error_code: "".to_string(),
22115                request_id: "req".to_string(),
22116                event_type: "AwsApiCall".to_string(),
22117                cloud_trail_event_json: "{}".to_string(),
22118            })
22119            .collect();
22120
22121        // Ctrl+D should jump down a page
22122        app.handle_action(Action::PageDown);
22123        assert_eq!(app.cloudtrail_state.table.selected, 10);
22124
22125        app.handle_action(Action::PageDown);
22126        assert_eq!(app.cloudtrail_state.table.selected, 20);
22127    }
22128
22129    #[test]
22130    fn test_cloudtrail_page_up_navigation() {
22131        let mut app = App::new_without_client("default".to_string(), None);
22132        app.service_selected = true;
22133        app.current_service = Service::CloudTrailEvents;
22134        app.mode = Mode::Normal;
22135        app.cloudtrail_state.table.page_size = PageSize::Ten;
22136
22137        // Create 25 items
22138        app.cloudtrail_state.table.items = (0..25)
22139            .map(|i| CloudTrailEvent {
22140                event_name: format!("Event{}", i),
22141                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22142                username: "user".to_string(),
22143                event_source: "s3.amazonaws.com".to_string(),
22144                resource_type: "Bucket".to_string(),
22145                resource_name: "bucket".to_string(),
22146                read_only: "false".to_string(),
22147                aws_region: "us-east-1".to_string(),
22148                event_id: "id".to_string(),
22149                access_key_id: "key".to_string(),
22150                source_ip_address: "1.2.3.4".to_string(),
22151                error_code: "".to_string(),
22152                request_id: "req".to_string(),
22153                event_type: "AwsApiCall".to_string(),
22154                cloud_trail_event_json: "{}".to_string(),
22155            })
22156            .collect();
22157
22158        // Start at page 2
22159        app.cloudtrail_state.table.selected = 20;
22160
22161        // Ctrl+U should jump up a page
22162        app.handle_action(Action::PageUp);
22163        assert_eq!(app.cloudtrail_state.table.selected, 10);
22164
22165        app.handle_action(Action::PageUp);
22166        assert_eq!(app.cloudtrail_state.table.selected, 0);
22167    }
22168
22169    #[test]
22170    fn test_cloudtrail_page_size_change_updates_display() {
22171        let mut app = App::new_without_client("default".to_string(), None);
22172        app.service_selected = true;
22173        app.current_service = Service::CloudTrailEvents;
22174        app.mode = Mode::ColumnSelector;
22175
22176        // Create 50 items
22177        app.cloudtrail_state.table.items = (0..50)
22178            .map(|i| CloudTrailEvent {
22179                event_name: format!("Event{}", i),
22180                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22181                username: "user".to_string(),
22182                event_source: "s3.amazonaws.com".to_string(),
22183                resource_type: "Bucket".to_string(),
22184                resource_name: "bucket".to_string(),
22185                read_only: "false".to_string(),
22186                aws_region: "us-east-1".to_string(),
22187                event_id: "id".to_string(),
22188                access_key_id: "key".to_string(),
22189                source_ip_address: "1.2.3.4".to_string(),
22190                error_code: "".to_string(),
22191                request_id: "req".to_string(),
22192                event_type: "AwsApiCall".to_string(),
22193                cloud_trail_event_json: "{}".to_string(),
22194            })
22195            .collect();
22196
22197        // Default page size is 50
22198        assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Fifty);
22199
22200        // Change to page size 10 (index 17)
22201        app.column_selector_index = 17;
22202        app.handle_action(Action::ToggleColumn);
22203
22204        assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
22205        // snap_to_page should have been called to adjust display
22206    }
22207
22208    #[test]
22209    fn test_cloudtrail_all_columns_toggleable() {
22210        let mut app = App::new_without_client("default".to_string(), None);
22211        app.service_selected = true;
22212        app.current_service = Service::CloudTrailEvents;
22213        app.mode = Mode::ColumnSelector;
22214
22215        // Verify we have 14 columns
22216        assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
22217
22218        // Test toggling each column (indices 1-14)
22219        for idx in 1..=14 {
22220            app.column_selector_index = idx;
22221            let initial_visible = app.cloudtrail_event_visible_column_ids.clone();
22222
22223            app.handle_action(Action::ToggleColumn);
22224
22225            // Verify the visible columns changed
22226            assert_ne!(
22227                app.cloudtrail_event_visible_column_ids, initial_visible,
22228                "Column at index {} should be toggleable",
22229                idx
22230            );
22231        }
22232    }
22233
22234    #[test]
22235    fn test_cloudtrail_readonly_column_toggleable() {
22236        use crate::cloudtrail::events::CloudTrailEventColumn;
22237
22238        let mut app = App::new_without_client("default".to_string(), None);
22239        app.service_selected = true;
22240        app.current_service = Service::CloudTrailEvents;
22241        app.mode = Mode::ColumnSelector;
22242
22243        // ReadOnly is at position 6 (0-indexed) in the all() array
22244        // So it's at index 7 in the column selector (1-indexed)
22245        let readonly_id = CloudTrailEventColumn::ReadOnly.id();
22246
22247        // Verify ReadOnly is in the column list
22248        assert!(app.cloudtrail_event_column_ids.contains(&readonly_id));
22249
22250        // Initially not visible
22251        assert!(!app
22252            .cloudtrail_event_visible_column_ids
22253            .contains(&readonly_id));
22254
22255        // Toggle it on (index 7)
22256        app.column_selector_index = 7;
22257        app.handle_action(Action::ToggleColumn);
22258
22259        // Should now be visible
22260        assert!(
22261            app.cloudtrail_event_visible_column_ids
22262                .contains(&readonly_id),
22263            "ReadOnly column should be toggleable at index 7"
22264        );
22265
22266        // Toggle it off
22267        app.handle_action(Action::ToggleColumn);
22268
22269        // Should be hidden again
22270        assert!(!app
22271            .cloudtrail_event_visible_column_ids
22272            .contains(&readonly_id));
22273    }
22274
22275    #[test]
22276    fn test_cloudtrail_pagination_limits_displayed_items() {
22277        let mut app = App::new_without_client("default".to_string(), None);
22278        app.service_selected = true;
22279        app.current_service = Service::CloudTrailEvents;
22280
22281        // Create 50 items
22282        app.cloudtrail_state.table.items = (0..50)
22283            .map(|i| CloudTrailEvent {
22284                event_name: format!("Event{}", i),
22285                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22286                username: "user".to_string(),
22287                event_source: "s3.amazonaws.com".to_string(),
22288                resource_type: "Bucket".to_string(),
22289                resource_name: "bucket".to_string(),
22290                read_only: "false".to_string(),
22291                aws_region: "us-east-1".to_string(),
22292                event_id: "id".to_string(),
22293                access_key_id: "key".to_string(),
22294                source_ip_address: "1.2.3.4".to_string(),
22295                error_code: "".to_string(),
22296                request_id: "req".to_string(),
22297                event_type: "AwsApiCall".to_string(),
22298                cloud_trail_event_json: "{}".to_string(),
22299            })
22300            .collect();
22301
22302        // Set page size to 10
22303        app.cloudtrail_state.table.page_size = PageSize::Ten;
22304
22305        // Render to a test backend to verify pagination
22306        use ratatui::backend::TestBackend;
22307        use ratatui::Terminal;
22308
22309        let backend = TestBackend::new(100, 30);
22310        let mut terminal = Terminal::new(backend).unwrap();
22311
22312        terminal
22313            .draw(|frame| {
22314                let area = frame.area();
22315                crate::ui::cloudtrail::render_events(frame, &app, area);
22316            })
22317            .unwrap();
22318
22319        // The rendering should only show 10 items per page
22320        // This is verified by the pagination logic in render_events
22321        assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
22322    }
22323
22324    #[test]
22325    fn test_cloudtrail_readonly_column_selectable_in_preferences() {
22326        let mut app = App::new_without_client("default".to_string(), None);
22327        app.service_selected = true;
22328        app.current_service = Service::CloudTrailEvents;
22329        app.mode = Mode::ColumnSelector;
22330
22331        // Verify we have 14 columns
22332        assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
22333
22334        // Navigate to Read-only column (index 7)
22335        app.column_selector_index = 7;
22336
22337        // Verify we can select it by checking the index is valid
22338        let max_index = app.get_column_selector_max();
22339        assert!(
22340            app.column_selector_index <= max_index,
22341            "Read-only column index {} should be <= max {}",
22342            app.column_selector_index,
22343            max_index
22344        );
22345
22346        // Verify the column exists at this index
22347        let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
22348        let col = CloudTrailEventColumn::from_id(col_id);
22349        assert!(col.is_some(), "Column should exist at index 7");
22350        assert_eq!(
22351            col.unwrap().default_name(),
22352            "Read-only",
22353            "Column at index 7 should be Read-only"
22354        );
22355    }
22356
22357    #[test]
22358    fn test_cloudtrail_navigate_to_readonly_column() {
22359        let mut app = App::new_without_client("default".to_string(), None);
22360        app.service_selected = true;
22361        app.current_service = Service::CloudTrailEvents;
22362        app.mode = Mode::ColumnSelector;
22363        app.column_selector_index = 1; // Start at first column
22364
22365        // Check column IDs are initialized
22366        println!(
22367            "cloudtrail_event_column_ids.len() = {}",
22368            app.cloudtrail_event_column_ids.len()
22369        );
22370        assert_eq!(
22371            app.cloudtrail_event_column_ids.len(),
22372            14,
22373            "Should have 14 columns"
22374        );
22375
22376        // Check blank row index
22377        let column_count = app.get_column_count();
22378        println!("Column count from get_column_count(): {}", column_count);
22379        assert_eq!(column_count, 14, "Column count should be 14");
22380
22381        // The blank row should be at index 15 (14 columns + 1)
22382        assert!(!app.is_blank_row_index(7), "Index 7 should NOT be blank");
22383        assert!(app.is_blank_row_index(15), "Index 15 should be blank");
22384
22385        // Navigate down to Read-only column (index 7)
22386        for _ in 0..6 {
22387            app.handle_action(Action::NextItem);
22388        }
22389
22390        assert_eq!(
22391            app.column_selector_index, 7,
22392            "Should navigate to Read-only column at index 7"
22393        );
22394
22395        // Verify it's the Read-only column
22396        let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
22397        let col = CloudTrailEventColumn::from_id(col_id).unwrap();
22398        assert_eq!(col.default_name(), "Read-only");
22399    }
22400
22401    #[test]
22402    fn test_cloudtrail_navigate_beyond_loaded_items() {
22403        let mut app = App::new_without_client("default".to_string(), None);
22404        app.service_selected = true;
22405        app.current_service = Service::CloudTrailEvents;
22406
22407        // Load only 50 items
22408        app.cloudtrail_state.table.items = (0..50)
22409            .map(|i| CloudTrailEvent {
22410                event_name: format!("Event{}", i),
22411                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22412                username: "user".to_string(),
22413                event_source: "s3.amazonaws.com".to_string(),
22414                resource_type: "Bucket".to_string(),
22415                resource_name: "bucket".to_string(),
22416                read_only: "false".to_string(),
22417                aws_region: "us-east-1".to_string(),
22418                event_id: "id".to_string(),
22419                access_key_id: "key".to_string(),
22420                source_ip_address: "1.2.3.4".to_string(),
22421                error_code: "".to_string(),
22422                request_id: "req".to_string(),
22423                event_type: "AwsApiCall".to_string(),
22424                cloud_trail_event_json: "{}".to_string(),
22425            })
22426            .collect();
22427
22428        // Page size is 50 by default, so we have 1 page of data
22429        // Try to navigate to page 10 (beyond loaded data)
22430        let initial_selected = app.cloudtrail_state.table.selected;
22431        app.page_input = "10".to_string();
22432        app.go_to_page(10);
22433
22434        // Should do nothing (stay at initial position)
22435        assert_eq!(
22436            app.cloudtrail_state.table.selected, initial_selected,
22437            "Should ignore navigation to page 10 when only 1 page is loaded"
22438        );
22439
22440        // But page 2 should work (loaded + 1)
22441        app.go_to_page(2);
22442        let page_size = app.cloudtrail_state.table.page_size.value();
22443        assert_eq!(
22444            app.cloudtrail_state.table.selected, page_size,
22445            "Should allow navigation to page 2 (loaded + 1)"
22446        );
22447    }
22448
22449    #[test]
22450    fn test_cloudtrail_page_change_resets_expansion() {
22451        let mut app = App::new_without_client("default".to_string(), None);
22452        app.service_selected = true;
22453        app.current_service = Service::CloudTrailEvents;
22454
22455        // Create 100 items (2 pages with page size 50)
22456        app.cloudtrail_state.table.items = (0..100)
22457            .map(|i| CloudTrailEvent {
22458                event_name: format!("Event{}", i),
22459                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22460                username: "user".to_string(),
22461                event_source: "s3.amazonaws.com".to_string(),
22462                resource_type: "Bucket".to_string(),
22463                resource_name: "bucket".to_string(),
22464                read_only: "false".to_string(),
22465                aws_region: "us-east-1".to_string(),
22466                event_id: "id".to_string(),
22467                access_key_id: "key".to_string(),
22468                source_ip_address: "1.2.3.4".to_string(),
22469                error_code: "".to_string(),
22470                request_id: "req".to_string(),
22471                event_type: "AwsApiCall".to_string(),
22472                cloud_trail_event_json: "{}".to_string(),
22473            })
22474            .collect();
22475
22476        // Expand an item on page 1
22477        app.cloudtrail_state.table.expanded_item = Some(5);
22478        assert_eq!(app.cloudtrail_state.table.expanded_item, Some(5));
22479
22480        // Navigate to page 2
22481        app.go_to_page(2);
22482
22483        // Expansion should be reset
22484        assert_eq!(
22485            app.cloudtrail_state.table.expanded_item, None,
22486            "Page change should reset expanded_item"
22487        );
22488    }
22489}