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 current_alarm: Option<String>,
236    pub alarm_tab: AlarmTab,
237    pub view_as: AlarmViewMode,
238    pub wrap_lines: bool,
239    pub sort_column: String,
240    pub sort_direction: SortDirection,
241    pub input_focus: InputFocus,
242    pub metric_data: Vec<(i64, f64)>,
243    pub metrics_loading: bool,
244}
245
246#[derive(Debug, Clone)]
247pub struct CloudTrailState {
248    pub table: TableState<CloudTrailEvent>,
249    pub input_focus: InputFocus,
250    pub current_event: Option<CloudTrailEvent>,
251    pub event_json_scroll: usize,
252    pub detail_focus: CloudTrailDetailFocus,
253    pub resources_expanded_index: Option<usize>,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq)]
257pub enum CloudTrailDetailFocus {
258    Resources,
259    EventRecord,
260}
261
262impl CyclicEnum for CloudTrailDetailFocus {
263    const ALL: &'static [Self] = &[Self::Resources, Self::EventRecord];
264}
265
266impl PageSize {
267    pub fn value(&self) -> usize {
268        match self {
269            PageSize::Ten => 10,
270            PageSize::TwentyFive => 25,
271            PageSize::Fifty => 50,
272            PageSize::OneHundred => 100,
273        }
274    }
275
276    pub fn next(&self) -> Self {
277        match self {
278            PageSize::Ten => PageSize::TwentyFive,
279            PageSize::TwentyFive => PageSize::Fifty,
280            PageSize::Fifty => PageSize::OneHundred,
281            PageSize::OneHundred => PageSize::Ten,
282        }
283    }
284}
285
286pub struct ServicePickerState {
287    pub filter: String,
288    pub filter_active: bool,
289    pub selected: usize,
290    pub services: Vec<&'static str>,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq)]
294pub enum ViewMode {
295    List,
296    Detail,
297    Events,
298    InsightsResults,
299    PolicyView,
300}
301
302#[derive(Debug, Clone, Copy, PartialEq)]
303pub enum Service {
304    ApiGatewayApis,
305    CloudWatchLogGroups,
306    CloudWatchInsights,
307    CloudWatchAlarms,
308    CloudTrailEvents,
309    S3Buckets,
310    SqsQueues,
311    Ec2Instances,
312    EcrRepositories,
313    LambdaFunctions,
314    LambdaApplications,
315    CloudFormationStacks,
316    IamUsers,
317    IamRoles,
318    IamUserGroups,
319}
320
321impl Service {
322    pub fn name(&self) -> &str {
323        match self {
324            Service::ApiGatewayApis => "API Gateway › APIs",
325            Service::CloudWatchLogGroups => "CloudWatch › Log Groups",
326            Service::CloudWatchInsights => "CloudWatch › Logs Insights",
327            Service::CloudWatchAlarms => "CloudWatch › Alarms",
328            Service::CloudTrailEvents => "CloudTrail › Event History",
329            Service::S3Buckets => "S3 › Buckets",
330            Service::SqsQueues => "SQS › Queues",
331            Service::Ec2Instances => "EC2 › Instances",
332            Service::EcrRepositories => "ECR › Repositories",
333            Service::LambdaFunctions => "Lambda › Functions",
334            Service::LambdaApplications => "Lambda › Applications",
335            Service::CloudFormationStacks => "CloudFormation › Stacks",
336            Service::IamUsers => "IAM › Users",
337            Service::IamRoles => "IAM › Roles",
338            Service::IamUserGroups => "IAM › User Groups",
339        }
340    }
341}
342
343pub(crate) fn copy_to_clipboard(text: &str) {
344    use std::io::Write;
345    use std::process::{Command, Stdio};
346    if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
347        if let Some(mut stdin) = child.stdin.take() {
348            let _ = stdin.write_all(text.as_bytes());
349        }
350        let _ = child.wait();
351    }
352}
353
354fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
355    if max > 0 {
356        *selected = (*selected + page_size).min(max - 1);
357    }
358}
359
360fn toggle_iam_preference(
361    idx: usize,
362    column_ids: &[String],
363    visible_column_ids: &mut Vec<String>,
364    page_size: &mut PageSize,
365) {
366    if idx > 0 && idx <= column_ids.len() {
367        if let Some(col) = column_ids.get(idx - 1) {
368            if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
369                // Only remove if more than one column is visible
370                if visible_column_ids.len() > 1 {
371                    visible_column_ids.remove(pos);
372                }
373            } else {
374                visible_column_ids.push(col.clone());
375            }
376        }
377    } else if idx == column_ids.len() + 3 {
378        *page_size = PageSize::Ten;
379    } else if idx == column_ids.len() + 4 {
380        *page_size = PageSize::TwentyFive;
381    } else if idx == column_ids.len() + 5 {
382        *page_size = PageSize::Fifty;
383    }
384}
385
386fn toggle_iam_preference_static(
387    idx: usize,
388    column_ids: &[ColumnId],
389    visible_column_ids: &mut Vec<ColumnId>,
390    page_size: &mut PageSize,
391) {
392    if idx > 0 && idx <= column_ids.len() {
393        if let Some(col) = column_ids.get(idx - 1) {
394            if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
395                // Only remove if more than one column is visible
396                if visible_column_ids.len() > 1 {
397                    visible_column_ids.remove(pos);
398                }
399            } else {
400                visible_column_ids.push(*col);
401            }
402        }
403    } else if idx == column_ids.len() + 3 {
404        *page_size = PageSize::Ten;
405    } else if idx == column_ids.len() + 4 {
406        *page_size = PageSize::TwentyFive;
407    } else if idx == column_ids.len() + 5 {
408        *page_size = PageSize::Fifty;
409    }
410}
411
412fn toggle_iam_page_size_only(idx: usize, base_idx: usize, page_size: &mut PageSize) {
413    if idx == base_idx {
414        *page_size = PageSize::Ten;
415    } else if idx == base_idx + 1 {
416        *page_size = PageSize::TwentyFive;
417    } else if idx == base_idx + 2 {
418        *page_size = PageSize::Fifty;
419    }
420}
421
422/// Helper to cycle to next preference section (Columns -> PageSize -> Columns)
423fn cycle_preference_next(current_idx: &mut usize, num_columns: usize) {
424    let page_size_idx = num_columns + 2;
425    if *current_idx < page_size_idx {
426        *current_idx = page_size_idx;
427    } else {
428        *current_idx = 0;
429    }
430}
431
432/// Helper to cycle to previous preference section (Columns <- PageSize <- Columns)
433fn cycle_preference_prev(current_idx: &mut usize, num_columns: usize) {
434    let page_size_idx = num_columns + 2;
435    if *current_idx >= page_size_idx {
436        *current_idx = 0;
437    } else {
438        *current_idx = page_size_idx;
439    }
440}
441
442impl App {
443    pub fn get_input_focus(&self) -> InputFocus {
444        InputFocus::Filter
445    }
446
447    fn get_active_filter_mut(&mut self) -> Option<&mut String> {
448        if self.current_service == Service::ApiGatewayApis {
449            if self.apig_state.current_api.is_some()
450                && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
451            {
452                Some(&mut self.apig_state.route_filter)
453            } else {
454                Some(&mut self.apig_state.apis.filter)
455            }
456        } else if self.current_service == Service::CloudWatchAlarms {
457            Some(&mut self.alarms_state.table.filter)
458        } else if self.current_service == Service::CloudTrailEvents {
459            Some(&mut self.cloudtrail_state.table.filter)
460        } else if self.current_service == Service::Ec2Instances {
461            if self.ec2_state.current_instance.is_some()
462                && self.ec2_state.detail_tab == Ec2DetailTab::Tags
463            {
464                Some(&mut self.ec2_state.tags.filter)
465            } else {
466                Some(&mut self.ec2_state.table.filter)
467            }
468        } else if self.current_service == Service::S3Buckets {
469            if self.s3_state.current_bucket.is_some() {
470                Some(&mut self.s3_state.object_filter)
471            } else {
472                Some(&mut self.s3_state.buckets.filter)
473            }
474        } else if self.current_service == Service::EcrRepositories {
475            if self.ecr_state.current_repository.is_some() {
476                Some(&mut self.ecr_state.images.filter)
477            } else {
478                Some(&mut self.ecr_state.repositories.filter)
479            }
480        } else if self.current_service == Service::SqsQueues {
481            if self.sqs_state.current_queue.is_some()
482                && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
483            {
484                Some(&mut self.sqs_state.triggers.filter)
485            } else if self.sqs_state.current_queue.is_some()
486                && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
487            {
488                Some(&mut self.sqs_state.pipes.filter)
489            } else if self.sqs_state.current_queue.is_some()
490                && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
491            {
492                Some(&mut self.sqs_state.tags.filter)
493            } else if self.sqs_state.current_queue.is_some()
494                && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
495            {
496                Some(&mut self.sqs_state.subscriptions.filter)
497            } else {
498                Some(&mut self.sqs_state.queues.filter)
499            }
500        } else if self.current_service == Service::LambdaFunctions {
501            if self.lambda_state.current_version.is_some()
502                && self.lambda_state.detail_tab == LambdaDetailTab::Configuration
503            {
504                Some(&mut self.lambda_state.alias_table.filter)
505            } else if self.lambda_state.current_function.is_some()
506                && self.lambda_state.detail_tab == LambdaDetailTab::Versions
507            {
508                Some(&mut self.lambda_state.version_table.filter)
509            } else if self.lambda_state.current_function.is_some()
510                && self.lambda_state.detail_tab == LambdaDetailTab::Aliases
511            {
512                Some(&mut self.lambda_state.alias_table.filter)
513            } else {
514                Some(&mut self.lambda_state.table.filter)
515            }
516        } else if self.current_service == Service::LambdaApplications {
517            if self.lambda_application_state.current_application.is_some() {
518                if self.lambda_application_state.detail_tab
519                    == LambdaApplicationDetailTab::Deployments
520                {
521                    Some(&mut self.lambda_application_state.deployments.filter)
522                } else {
523                    Some(&mut self.lambda_application_state.resources.filter)
524                }
525            } else {
526                Some(&mut self.lambda_application_state.table.filter)
527            }
528        } else if self.current_service == Service::CloudFormationStacks {
529            if self.cfn_state.current_stack.is_some()
530                && self.cfn_state.detail_tab == CfnDetailTab::Resources
531            {
532                Some(&mut self.cfn_state.resources.filter)
533            } else {
534                Some(&mut self.cfn_state.table.filter)
535            }
536        } else if self.current_service == Service::IamUsers {
537            if self.iam_state.current_user.is_some() {
538                if self.iam_state.user_tab == UserTab::Tags {
539                    Some(&mut self.iam_state.user_tags.filter)
540                } else {
541                    Some(&mut self.iam_state.policies.filter)
542                }
543            } else {
544                Some(&mut self.iam_state.users.filter)
545            }
546        } else if self.current_service == Service::IamRoles {
547            if self.iam_state.current_role.is_some() {
548                if self.iam_state.role_tab == RoleTab::Tags {
549                    Some(&mut self.iam_state.tags.filter)
550                } else if self.iam_state.role_tab == RoleTab::LastAccessed {
551                    Some(&mut self.iam_state.last_accessed_filter)
552                } else {
553                    Some(&mut self.iam_state.policies.filter)
554                }
555            } else {
556                Some(&mut self.iam_state.roles.filter)
557            }
558        } else if self.current_service == Service::IamUserGroups {
559            if self.iam_state.current_group.is_some() {
560                if self.iam_state.group_tab == GroupTab::Permissions {
561                    Some(&mut self.iam_state.policies.filter)
562                } else if self.iam_state.group_tab == GroupTab::Users {
563                    Some(&mut self.iam_state.group_users.filter)
564                } else {
565                    None
566                }
567            } else {
568                Some(&mut self.iam_state.groups.filter)
569            }
570        } else if self.view_mode == ViewMode::List {
571            Some(&mut self.log_groups_state.log_groups.filter)
572        } else if self.view_mode == ViewMode::Detail
573            && self.log_groups_state.detail_tab == DetailTab::LogStreams
574        {
575            Some(&mut self.log_groups_state.stream_filter)
576        } else {
577            None
578        }
579    }
580
581    fn apply_filter_operation<F>(&mut self, op: F)
582    where
583        F: FnOnce(&mut String),
584    {
585        if let Some(filter) = self.get_active_filter_mut() {
586            op(filter);
587            // Automatically reset selection for all services
588            if self.current_service == Service::CloudWatchAlarms {
589                self.alarms_state.table.reset();
590            } else if self.current_service == Service::CloudTrailEvents {
591                self.cloudtrail_state.table.reset();
592            } else if self.current_service == Service::Ec2Instances {
593                self.ec2_state.table.reset();
594            } else if self.current_service == Service::S3Buckets {
595                crate::s3::actions::apply_filter_reset(self);
596            } else if self.current_service == Service::EcrRepositories {
597                crate::ecr::actions::apply_filter_reset(self);
598            } else if self.current_service == Service::SqsQueues {
599                self.sqs_state.queues.reset();
600            } else if self.current_service == Service::LambdaFunctions {
601                crate::lambda::functions::apply_filter_reset(self);
602            } else if self.current_service == Service::LambdaApplications {
603                crate::lambda::applications::apply_filter_reset(self);
604            } else if self.current_service == Service::CloudFormationStacks {
605                self.cfn_state.table.reset();
606            } else if self.current_service == Service::IamUsers {
607                if self.iam_state.current_user.is_some() {
608                    self.iam_state.user_tags.reset();
609                    self.iam_state.policies.reset();
610                } else {
611                    self.iam_state.users.reset();
612                }
613            } else if self.current_service == Service::IamRoles {
614                if self.iam_state.current_role.is_some() {
615                    self.iam_state.tags.reset();
616                    self.iam_state.policies.reset();
617                } else {
618                    self.iam_state.roles.reset();
619                }
620            } else if self.current_service == Service::IamUserGroups {
621                if self.iam_state.current_group.is_some() {
622                    self.iam_state.policies.reset();
623                    self.iam_state.group_users.reset();
624                } else {
625                    self.iam_state.groups.reset();
626                }
627            } else if self.current_service == Service::CloudWatchLogGroups {
628                if self.view_mode == ViewMode::List {
629                    self.log_groups_state.log_groups.reset();
630                } else if self.log_groups_state.detail_tab == DetailTab::LogStreams {
631                    self.log_groups_state.selected_stream = 0;
632                }
633            } else if self.current_service == Service::ApiGatewayApis {
634                self.apig_state.apis.reset();
635            }
636        }
637    }
638
639    pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
640        let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
641            .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
642
643        std::env::set_var("AWS_PROFILE", &profile_name);
644
645        let config = AwsConfig::new(region).await?;
646        let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
647        let cloudtrail_client = CloudTrailClient::new(config.clone());
648        let s3_client = S3Client::new(config.clone());
649        let sqs_client = SqsClient::new(config.clone());
650        let alarms_client = AlarmsClient::new(config.clone());
651        let ec2_client = Ec2Client::new(config.clone());
652        let ecr_client = EcrClient::new(config.clone());
653        let apig_client = ApiGatewayClient::new(config.clone());
654        let iam_client = IamClient::new(config.clone());
655        let lambda_client = LambdaClient::new(config.clone());
656        let cloudformation_client = CloudFormationClient::new(config.clone());
657        let region_name = config.region.clone();
658
659        Ok(Self {
660            running: true,
661            mode: Mode::ServicePicker,
662            config,
663            cloudwatch_client,
664            cloudtrail_client,
665            s3_client,
666            sqs_client,
667            alarms_client,
668            ec2_client,
669            ecr_client,
670            apig_client,
671            iam_client,
672            lambda_client,
673            cloudformation_client,
674            current_service: Service::CloudWatchLogGroups,
675            tabs: Vec::new(),
676            current_tab: 0,
677            tab_picker_selected: 0,
678            tab_filter: String::new(),
679            pending_key: None,
680            log_groups_state: CloudWatchLogGroupsState::new(),
681            insights_state: CloudWatchInsightsState::new(),
682            alarms_state: CloudWatchAlarmsState::new(),
683            cloudtrail_state: CloudTrailState {
684                table: TableState::new(),
685                input_focus: InputFocus::Filter,
686                current_event: None,
687                event_json_scroll: 0,
688                detail_focus: CloudTrailDetailFocus::Resources,
689                resources_expanded_index: None,
690            },
691            s3_state: S3State::new(),
692            sqs_state: SqsState::new(),
693            ec2_state: Ec2State::default(),
694            ecr_state: EcrState::new(),
695            apig_state: ApigState::new(),
696            lambda_state: LambdaState::new(),
697            lambda_application_state: LambdaApplicationState::new(),
698            cfn_state: CfnState::new(),
699            iam_state: IamState::new(),
700            service_picker: ServicePickerState::new(),
701            service_selected: false,
702            profile: profile_name,
703            region: region_name,
704            region_selector_index: 0,
705            cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
706            cw_log_group_column_ids: LogGroupColumn::ids(),
707            column_selector_index: 0,
708            cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
709            cw_log_stream_column_ids: StreamColumn::ids(),
710            cw_log_event_visible_column_ids: EventColumn::default_visible(),
711            cw_log_event_column_ids: EventColumn::ids(),
712            cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
713            cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
714            cw_alarm_visible_column_ids: [
715                AlarmColumn::Name,
716                AlarmColumn::State,
717                AlarmColumn::LastStateUpdate,
718                AlarmColumn::Conditions,
719                AlarmColumn::Actions,
720            ]
721            .iter()
722            .map(|c| c.id())
723            .collect(),
724            cw_alarm_column_ids: AlarmColumn::ids(),
725            cloudtrail_event_visible_column_ids: [
726                CloudTrailEventColumn::EventName,
727                CloudTrailEventColumn::EventTime,
728                CloudTrailEventColumn::Username,
729                CloudTrailEventColumn::EventSource,
730                CloudTrailEventColumn::ResourceType,
731                CloudTrailEventColumn::ResourceName,
732            ]
733            .iter()
734            .map(|c| c.id())
735            .collect(),
736            cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
737            cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
738            cloudtrail_resource_column_ids: EventResourceColumn::ids(),
739            s3_bucket_visible_column_ids: S3BucketColumn::ids(),
740            s3_bucket_column_ids: S3BucketColumn::ids(),
741            sqs_visible_column_ids: [
742                SqsColumn::Name,
743                SqsColumn::Type,
744                SqsColumn::Created,
745                SqsColumn::MessagesAvailable,
746                SqsColumn::MessagesInFlight,
747                SqsColumn::Encryption,
748                SqsColumn::ContentBasedDeduplication,
749            ]
750            .iter()
751            .map(|c| c.id())
752            .collect(),
753            sqs_column_ids: SqsColumn::ids(),
754            ec2_visible_column_ids: [
755                Ec2Column::Name,
756                Ec2Column::InstanceId,
757                Ec2Column::InstanceState,
758                Ec2Column::InstanceType,
759                Ec2Column::StatusCheck,
760                Ec2Column::AlarmStatus,
761                Ec2Column::AvailabilityZone,
762                Ec2Column::PublicIpv4Dns,
763                Ec2Column::PublicIpv4Address,
764                Ec2Column::ElasticIp,
765                Ec2Column::Ipv6Ips,
766                Ec2Column::Monitoring,
767                Ec2Column::SecurityGroupName,
768                Ec2Column::KeyName,
769                Ec2Column::LaunchTime,
770                Ec2Column::PlatformDetails,
771            ]
772            .iter()
773            .map(|c| c.id())
774            .collect(),
775            ec2_column_ids: Ec2Column::ids(),
776            ecr_repo_visible_column_ids: EcrColumn::ids(),
777            ecr_repo_column_ids: EcrColumn::ids(),
778            ecr_image_visible_column_ids: EcrImageColumn::ids(),
779            ecr_image_column_ids: EcrImageColumn::ids(),
780            apig_api_visible_column_ids: ApigColumn::ids(),
781            apig_api_column_ids: ApigColumn::ids(),
782            apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
783            apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
784            apig_resource_visible_column_ids: ResourceColumn::all()
785                .iter()
786                .map(|c| c.id())
787                .collect(),
788            apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
789            lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
790            lambda_application_column_ids: LambdaApplicationColumn::ids(),
791            lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
792            lambda_deployment_column_ids: DeploymentColumn::ids(),
793            lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
794            lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
795            cfn_visible_column_ids: [
796                CfnColumn::Name,
797                CfnColumn::Status,
798                CfnColumn::CreatedTime,
799                CfnColumn::Description,
800            ]
801            .iter()
802            .map(|c| c.id())
803            .collect(),
804            cfn_column_ids: CfnColumn::ids(),
805            cfn_parameter_visible_column_ids: parameter_column_ids(),
806            cfn_parameter_column_ids: parameter_column_ids(),
807            cfn_output_visible_column_ids: output_column_ids(),
808            cfn_output_column_ids: output_column_ids(),
809            cfn_resource_visible_column_ids: resource_column_ids(),
810            cfn_resource_column_ids: resource_column_ids(),
811            iam_user_visible_column_ids: UserColumn::visible(),
812            iam_user_column_ids: UserColumn::ids(),
813            iam_role_visible_column_ids: RoleColumn::visible(),
814            iam_role_column_ids: RoleColumn::ids(),
815            iam_group_visible_column_ids: vec![
816                "Group name".to_string(),
817                "Users".to_string(),
818                "Permissions".to_string(),
819                "Creation time".to_string(),
820            ],
821            iam_group_column_ids: vec![
822                "Group name".to_string(),
823                "Path".to_string(),
824                "Users".to_string(),
825                "Permissions".to_string(),
826                "Creation time".to_string(),
827            ],
828            iam_policy_visible_column_ids: vec![
829                "Policy name".to_string(),
830                "Type".to_string(),
831                "Attached via".to_string(),
832            ],
833            iam_policy_column_ids: vec![
834                "Policy name".to_string(),
835                "Type".to_string(),
836                "Attached via".to_string(),
837                "Attached entities".to_string(),
838                "Description".to_string(),
839                "Creation time".to_string(),
840                "Edited time".to_string(),
841            ],
842            preference_section: Preferences::Columns,
843            view_mode: ViewMode::List,
844            error_message: None,
845            error_scroll: 0,
846            page_input: String::new(),
847            calendar_date: None,
848            calendar_selecting: CalendarField::StartDate,
849            cursor_pos: 0,
850            current_session: None,
851            sessions: Vec::new(),
852            session_picker_selected: 0,
853            session_filter: String::new(),
854            session_filter_active: false,
855            region_filter: String::new(),
856            region_filter_active: false,
857            region_picker_selected: 0,
858            region_latencies: std::collections::HashMap::new(),
859            profile_filter: String::new(),
860            profile_filter_active: false,
861            profile_picker_selected: 0,
862            available_profiles: Vec::new(),
863            snapshot_requested: false,
864        })
865    }
866
867    pub fn new_without_client(profile: String, region: Option<String>) -> Self {
868        let config = AwsConfig::dummy(region.clone());
869        Self {
870            running: true,
871            mode: Mode::ServicePicker,
872            config: config.clone(),
873            cloudwatch_client: CloudWatchClient::dummy(config.clone()),
874            cloudtrail_client: CloudTrailClient::new(config.clone()),
875            s3_client: S3Client::new(config.clone()),
876            sqs_client: SqsClient::new(config.clone()),
877            alarms_client: AlarmsClient::new(config.clone()),
878            ec2_client: Ec2Client::new(config.clone()),
879            ecr_client: EcrClient::new(config.clone()),
880            apig_client: ApiGatewayClient::new(config.clone()),
881            iam_client: IamClient::new(config.clone()),
882            lambda_client: LambdaClient::new(config.clone()),
883            cloudformation_client: CloudFormationClient::new(config.clone()),
884            current_service: Service::CloudWatchLogGroups,
885            tabs: Vec::new(),
886            current_tab: 0,
887            tab_picker_selected: 0,
888            tab_filter: String::new(),
889            pending_key: None,
890            log_groups_state: CloudWatchLogGroupsState::new(),
891            insights_state: CloudWatchInsightsState::new(),
892            alarms_state: CloudWatchAlarmsState::new(),
893            s3_state: S3State::new(),
894            cloudtrail_state: CloudTrailState {
895                table: TableState::new(),
896                input_focus: InputFocus::Filter,
897                current_event: None,
898                event_json_scroll: 0,
899                detail_focus: CloudTrailDetailFocus::Resources,
900                resources_expanded_index: None,
901            },
902            sqs_state: SqsState::new(),
903            ec2_state: Ec2State::default(),
904            ecr_state: EcrState::new(),
905            apig_state: ApigState::new(),
906            lambda_state: LambdaState::new(),
907            lambda_application_state: LambdaApplicationState::new(),
908            cfn_state: CfnState::new(),
909            iam_state: IamState::new(),
910            service_picker: ServicePickerState::new(),
911            service_selected: false,
912            profile,
913            region: region.unwrap_or_default(),
914            region_selector_index: 0,
915            cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
916            cw_log_group_column_ids: LogGroupColumn::ids(),
917            column_selector_index: 0,
918            preference_section: Preferences::Columns,
919            cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
920            cw_log_stream_column_ids: StreamColumn::ids(),
921            cw_log_event_visible_column_ids: EventColumn::default_visible(),
922            cw_log_event_column_ids: EventColumn::ids(),
923            cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
924            cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
925            cw_alarm_visible_column_ids: [
926                AlarmColumn::Name,
927                AlarmColumn::State,
928                AlarmColumn::LastStateUpdate,
929                AlarmColumn::Conditions,
930                AlarmColumn::Actions,
931            ]
932            .iter()
933            .map(|c| c.id())
934            .collect(),
935            cw_alarm_column_ids: AlarmColumn::ids(),
936            s3_bucket_visible_column_ids: S3BucketColumn::ids(),
937            cloudtrail_event_visible_column_ids: [
938                CloudTrailEventColumn::EventName,
939                CloudTrailEventColumn::EventTime,
940                CloudTrailEventColumn::Username,
941                CloudTrailEventColumn::EventSource,
942                CloudTrailEventColumn::ResourceType,
943                CloudTrailEventColumn::ResourceName,
944            ]
945            .iter()
946            .map(|c| c.id())
947            .collect(),
948            cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
949            cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
950            cloudtrail_resource_column_ids: EventResourceColumn::ids(),
951            s3_bucket_column_ids: S3BucketColumn::ids(),
952            sqs_visible_column_ids: [
953                SqsColumn::Name,
954                SqsColumn::Type,
955                SqsColumn::Created,
956                SqsColumn::MessagesAvailable,
957                SqsColumn::MessagesInFlight,
958                SqsColumn::Encryption,
959                SqsColumn::ContentBasedDeduplication,
960            ]
961            .iter()
962            .map(|c| c.id())
963            .collect(),
964            sqs_column_ids: SqsColumn::ids(),
965            ec2_visible_column_ids: [
966                Ec2Column::Name,
967                Ec2Column::InstanceId,
968                Ec2Column::InstanceState,
969                Ec2Column::InstanceType,
970                Ec2Column::StatusCheck,
971                Ec2Column::AlarmStatus,
972                Ec2Column::AvailabilityZone,
973                Ec2Column::PublicIpv4Dns,
974                Ec2Column::PublicIpv4Address,
975                Ec2Column::ElasticIp,
976                Ec2Column::Ipv6Ips,
977                Ec2Column::Monitoring,
978                Ec2Column::SecurityGroupName,
979                Ec2Column::KeyName,
980                Ec2Column::LaunchTime,
981                Ec2Column::PlatformDetails,
982            ]
983            .iter()
984            .map(|c| c.id())
985            .collect(),
986            ec2_column_ids: Ec2Column::ids(),
987            ecr_repo_visible_column_ids: EcrColumn::ids(),
988            ecr_repo_column_ids: EcrColumn::ids(),
989            ecr_image_visible_column_ids: EcrImageColumn::ids(),
990            ecr_image_column_ids: EcrImageColumn::ids(),
991            lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
992            lambda_application_column_ids: LambdaApplicationColumn::ids(),
993            apig_api_visible_column_ids: ApigColumn::ids(),
994            apig_api_column_ids: ApigColumn::ids(),
995            apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
996            apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
997            apig_resource_visible_column_ids: ResourceColumn::all()
998                .iter()
999                .map(|c| c.id())
1000                .collect(),
1001            apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
1002            lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
1003            lambda_deployment_column_ids: DeploymentColumn::ids(),
1004            lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
1005            lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
1006            cfn_visible_column_ids: [
1007                CfnColumn::Name,
1008                CfnColumn::Status,
1009                CfnColumn::CreatedTime,
1010                CfnColumn::Description,
1011            ]
1012            .iter()
1013            .map(|c| c.id())
1014            .collect(),
1015            cfn_column_ids: CfnColumn::ids(),
1016            iam_user_visible_column_ids: UserColumn::visible(),
1017            cfn_parameter_visible_column_ids: parameter_column_ids(),
1018            cfn_parameter_column_ids: parameter_column_ids(),
1019            cfn_output_visible_column_ids: output_column_ids(),
1020            cfn_output_column_ids: output_column_ids(),
1021            cfn_resource_visible_column_ids: resource_column_ids(),
1022            cfn_resource_column_ids: resource_column_ids(),
1023            iam_user_column_ids: UserColumn::ids(),
1024            iam_role_visible_column_ids: RoleColumn::visible(),
1025            iam_role_column_ids: RoleColumn::ids(),
1026            iam_group_visible_column_ids: vec![
1027                "Group name".to_string(),
1028                "Users".to_string(),
1029                "Permissions".to_string(),
1030                "Creation time".to_string(),
1031            ],
1032            iam_group_column_ids: vec![
1033                "Group name".to_string(),
1034                "Path".to_string(),
1035                "Users".to_string(),
1036                "Permissions".to_string(),
1037                "Creation time".to_string(),
1038            ],
1039            iam_policy_visible_column_ids: vec![
1040                "Policy name".to_string(),
1041                "Type".to_string(),
1042                "Attached via".to_string(),
1043            ],
1044            iam_policy_column_ids: vec![
1045                "Policy name".to_string(),
1046                "Type".to_string(),
1047                "Attached via".to_string(),
1048                "Attached entities".to_string(),
1049                "Description".to_string(),
1050                "Creation time".to_string(),
1051                "Edited time".to_string(),
1052            ],
1053            view_mode: ViewMode::List,
1054            error_message: None,
1055            error_scroll: 0,
1056            page_input: String::new(),
1057            calendar_date: None,
1058            calendar_selecting: CalendarField::StartDate,
1059            cursor_pos: 0,
1060            current_session: None,
1061            sessions: Vec::new(),
1062            session_picker_selected: 0,
1063            session_filter: String::new(),
1064            session_filter_active: false,
1065            region_filter: String::new(),
1066            region_filter_active: false,
1067            region_picker_selected: 0,
1068            region_latencies: std::collections::HashMap::new(),
1069            profile_filter: String::new(),
1070            profile_filter_active: false,
1071            profile_picker_selected: 0,
1072            available_profiles: Vec::new(),
1073            snapshot_requested: false,
1074        }
1075    }
1076
1077    pub fn handle_action(&mut self, action: Action) {
1078        match action {
1079            Action::Noop => {}
1080            Action::EnterFilterMode => match self.mode {
1081                Mode::ServicePicker => self.service_picker.filter_active = true,
1082                Mode::RegionPicker => self.region_filter_active = true,
1083                Mode::SessionPicker => self.session_filter_active = true,
1084                Mode::ProfilePicker => self.profile_filter_active = true,
1085                _ => {}
1086            },
1087            Action::ExitFilterMode => match self.mode {
1088                Mode::ServicePicker => {
1089                    if self.service_picker.filter_active {
1090                        self.service_picker.filter_active = false;
1091                    } else {
1092                        self.mode = Mode::Normal;
1093                    }
1094                }
1095                Mode::RegionPicker => {
1096                    if self.region_filter_active {
1097                        self.region_filter_active = false;
1098                    } else {
1099                        self.mode = Mode::Normal;
1100                    }
1101                }
1102                Mode::SessionPicker => {
1103                    if self.session_filter_active {
1104                        self.session_filter_active = false;
1105                    } else {
1106                        self.mode = Mode::Normal;
1107                    }
1108                }
1109                Mode::ProfilePicker => {
1110                    if self.profile_filter_active {
1111                        self.profile_filter_active = false;
1112                    } else {
1113                        self.mode = Mode::Normal;
1114                    }
1115                }
1116                _ => {}
1117            },
1118            Action::Quit => {
1119                self.save_current_session();
1120                self.running = false;
1121            }
1122            Action::CloseService => {
1123                if !self.tabs.is_empty() {
1124                    // Close the current tab
1125                    self.tabs.remove(self.current_tab);
1126
1127                    if self.tabs.is_empty() {
1128                        // Last tab closed - show service picker
1129                        self.service_selected = false;
1130                        self.current_tab = 0;
1131                        self.mode = Mode::ServicePicker;
1132                    } else {
1133                        // Tabs remain - switch to adjacent tab
1134                        if self.current_tab >= self.tabs.len() {
1135                            self.current_tab = self.tabs.len() - 1;
1136                        }
1137                        self.current_service = self.tabs[self.current_tab].service;
1138                        self.service_selected = true;
1139                        self.mode = Mode::Normal;
1140                    }
1141                } else {
1142                    // No tabs - just close service picker if open
1143                    self.service_selected = false;
1144                    self.mode = Mode::Normal;
1145                }
1146                self.service_picker.filter.clear();
1147                self.service_picker.selected = 0;
1148            }
1149            Action::NextItem => {
1150                // Only navigate when filter is not active
1151                let should_navigate = match self.mode {
1152                    Mode::ServicePicker => !self.service_picker.filter_active,
1153                    Mode::RegionPicker => !self.region_filter_active,
1154                    Mode::SessionPicker => !self.session_filter_active,
1155                    Mode::ProfilePicker => !self.profile_filter_active,
1156                    _ => true,
1157                };
1158                if should_navigate {
1159                    self.next_item();
1160                }
1161            }
1162            Action::PrevItem => {
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.prev_item();
1173                }
1174            }
1175            Action::PageUp => self.page_up(),
1176            Action::PageDown => self.page_down(),
1177            Action::NextPane => self.next_pane(),
1178            Action::PrevPane => self.prev_pane(),
1179            Action::CollapseRow => self.collapse_row(),
1180            Action::ExpandRow => self.expand_row(),
1181            Action::Select => self.select_item(),
1182            Action::OpenSpaceMenu => {
1183                self.mode = Mode::SpaceMenu;
1184                self.service_picker.filter.clear();
1185                self.service_picker.filter_active = false;
1186                self.service_picker.selected = 0;
1187            }
1188            Action::CloseMenu => {
1189                self.mode = Mode::Normal;
1190                self.service_picker.filter.clear();
1191                // Reset selection when closing filter to avoid out-of-bounds
1192                match self.current_service {
1193                    Service::S3Buckets => {
1194                        crate::s3::actions::reset_on_service_select(self);
1195                    }
1196                    Service::CloudFormationStacks => {
1197                        if self.cfn_state.current_stack.is_some()
1198                            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1199                        {
1200                            self.cfn_state.parameters.reset();
1201                        } else if self.cfn_state.current_stack.is_some()
1202                            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1203                        {
1204                            self.cfn_state.outputs.reset();
1205                        } else {
1206                            self.cfn_state.table.reset();
1207                        }
1208                    }
1209                    Service::LambdaFunctions => {
1210                        crate::lambda::functions::apply_filter_reset(self);
1211                    }
1212                    Service::SqsQueues => {
1213                        self.sqs_state.queues.reset();
1214                    }
1215                    Service::IamRoles => {
1216                        self.iam_state.roles.reset();
1217                    }
1218                    Service::IamUsers => {
1219                        self.iam_state.users.reset();
1220                    }
1221                    Service::IamUserGroups => {
1222                        self.iam_state.groups.reset();
1223                    }
1224                    Service::CloudWatchAlarms => {
1225                        self.alarms_state.table.reset();
1226                    }
1227                    Service::Ec2Instances => {
1228                        self.ec2_state.table.reset();
1229                    }
1230                    Service::EcrRepositories => {
1231                        crate::ecr::actions::apply_filter_reset(self);
1232                    }
1233                    Service::ApiGatewayApis => {
1234                        self.apig_state.apis.reset();
1235                    }
1236                    Service::LambdaApplications => {
1237                        crate::lambda::applications::apply_filter_reset(self);
1238                    }
1239                    _ => {}
1240                }
1241            }
1242            Action::NextTab => {
1243                if !self.tabs.is_empty() {
1244                    self.current_tab = (self.current_tab + 1) % self.tabs.len();
1245                    self.current_service = self.tabs[self.current_tab].service;
1246                }
1247            }
1248            Action::PrevTab => {
1249                if !self.tabs.is_empty() {
1250                    self.current_tab = if self.current_tab == 0 {
1251                        self.tabs.len() - 1
1252                    } else {
1253                        self.current_tab - 1
1254                    };
1255                    self.current_service = self.tabs[self.current_tab].service;
1256                }
1257            }
1258            Action::CloseTab => {
1259                if !self.tabs.is_empty() {
1260                    self.tabs.remove(self.current_tab);
1261                    if self.tabs.is_empty() {
1262                        // Last tab closed - show service picker
1263                        self.service_selected = false;
1264                        self.current_tab = 0;
1265                        self.service_picker.filter.clear();
1266                        self.service_picker.selected = 0;
1267                        self.mode = Mode::ServicePicker;
1268                    } else {
1269                        // If we closed the last tab, move to the left
1270                        // Otherwise stay at same index (which is now the next tab to the right)
1271                        if self.current_tab >= self.tabs.len() {
1272                            self.current_tab = self.tabs.len() - 1;
1273                        }
1274                        self.current_service = self.tabs[self.current_tab].service;
1275                        self.service_selected = true;
1276                        self.mode = Mode::Normal;
1277                    }
1278                }
1279            }
1280            Action::OpenTabPicker => {
1281                if !self.tabs.is_empty() {
1282                    self.tab_picker_selected = self.current_tab;
1283                    self.mode = Mode::TabPicker;
1284                } else {
1285                    self.mode = Mode::Normal;
1286                }
1287            }
1288            Action::OpenSessionPicker => {
1289                self.save_current_session();
1290                self.sessions = Session::list_all().unwrap_or_default();
1291                self.session_picker_selected = 0;
1292                self.mode = Mode::SessionPicker;
1293            }
1294            Action::LoadSession => {
1295                let filtered_sessions = self.get_filtered_sessions();
1296                if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
1297                    let session = session.clone();
1298                    // Load the session
1299                    self.profile = session.profile.clone();
1300                    self.region = session.region.clone();
1301                    self.config.account_id = session.account_id.clone();
1302                    self.config.role_arn = session.role_arn.clone();
1303
1304                    // Clear existing tabs and load session tabs
1305                    self.tabs.clear();
1306                    for session_tab in &session.tabs {
1307                        // Parse service from string
1308                        let service = match session_tab.service.as_str() {
1309                            "ApiGatewayApis" => Service::ApiGatewayApis,
1310                            "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
1311                            "CloudWatchInsights" => Service::CloudWatchInsights,
1312                            "CloudWatchAlarms" => Service::CloudWatchAlarms,
1313                            "CloudTrailEvents" => Service::CloudTrailEvents,
1314                            "S3Buckets" => Service::S3Buckets,
1315                            "SqsQueues" => Service::SqsQueues,
1316                            "Ec2Instances" => Service::Ec2Instances,
1317                            "EcrRepositories" => Service::EcrRepositories,
1318                            "LambdaFunctions" => Service::LambdaFunctions,
1319                            "LambdaApplications" => Service::LambdaApplications,
1320                            "CloudFormationStacks" => Service::CloudFormationStacks,
1321                            "IamUsers" => Service::IamUsers,
1322                            "IamRoles" => Service::IamRoles,
1323                            "IamUserGroups" => Service::IamUserGroups,
1324                            _ => continue,
1325                        };
1326
1327                        self.tabs.push(Tab {
1328                            service,
1329                            title: session_tab.title.clone(),
1330                            breadcrumb: session_tab.breadcrumb.clone(),
1331                        });
1332
1333                        // Restore filter if present
1334                        if let Some(filter) = &session_tab.filter {
1335                            if service == Service::CloudWatchLogGroups {
1336                                self.log_groups_state.log_groups.filter = filter.clone();
1337                            }
1338                        }
1339                    }
1340
1341                    if !self.tabs.is_empty() {
1342                        self.current_tab = 0;
1343                        self.current_service = self.tabs[0].service;
1344                        self.service_selected = true;
1345                        self.current_session = Some(session.clone());
1346                    }
1347                }
1348                self.mode = Mode::Normal;
1349            }
1350            Action::SaveSession => {
1351                // TODO: Implement session saving
1352            }
1353            Action::OpenServicePicker => {
1354                if self.mode == Mode::ServicePicker {
1355                    self.tabs.push(Tab {
1356                        service: Service::S3Buckets,
1357                        title: "S3 › Buckets".to_string(),
1358                        breadcrumb: "S3 › Buckets".to_string(),
1359                    });
1360                    self.current_tab = self.tabs.len() - 1;
1361                    self.current_service = Service::S3Buckets;
1362                    self.view_mode = ViewMode::List;
1363                    self.service_selected = true;
1364                    self.mode = Mode::Normal;
1365                } else {
1366                    self.mode = Mode::ServicePicker;
1367                    self.service_picker.filter.clear();
1368                    self.service_picker.selected = 0;
1369                }
1370            }
1371            Action::OpenCloudWatch => {
1372                self.current_service = Service::CloudWatchLogGroups;
1373                self.view_mode = ViewMode::List;
1374                self.service_selected = true;
1375                self.mode = Mode::Normal;
1376            }
1377            Action::OpenCloudWatchSplit => {
1378                self.current_service = Service::CloudWatchInsights;
1379                self.view_mode = ViewMode::InsightsResults;
1380                self.service_selected = true;
1381                self.mode = Mode::Normal;
1382            }
1383            Action::OpenCloudWatchAlarms => {
1384                self.current_service = Service::CloudWatchAlarms;
1385                self.view_mode = ViewMode::List;
1386                self.service_selected = true;
1387                self.mode = Mode::Normal;
1388            }
1389            Action::FilterInput(c) => {
1390                if self.mode == Mode::TabPicker {
1391                    self.tab_filter.push(c);
1392                    self.tab_picker_selected = 0;
1393                } else if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1394                    self.service_picker.filter.push(c);
1395                    self.service_picker.selected = 0;
1396                } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1397                    self.region_filter.push(c);
1398                    self.region_picker_selected = 0;
1399                } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1400                    self.profile_filter.push(c);
1401                    self.profile_picker_selected = 0;
1402                } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1403                    self.session_filter.push(c);
1404                    self.session_picker_selected = 0;
1405                } else if self.mode == Mode::InsightsInput {
1406                    match self.insights_state.insights.insights_focus {
1407                        InsightsFocus::Query => {
1408                            self.insights_state.insights.query_text.push(c);
1409                        }
1410                        InsightsFocus::LogGroupSearch => {
1411                            self.insights_state.insights.log_group_search.push(c);
1412                            // Update matches
1413                            if !self.insights_state.insights.log_group_search.is_empty() {
1414                                self.insights_state.insights.log_group_matches = self
1415                                    .log_groups_state
1416                                    .log_groups
1417                                    .items
1418                                    .iter()
1419                                    .filter(|g| {
1420                                        g.name.to_lowercase().contains(
1421                                            &self
1422                                                .insights_state
1423                                                .insights
1424                                                .log_group_search
1425                                                .to_lowercase(),
1426                                        )
1427                                    })
1428                                    .take(50)
1429                                    .map(|g| g.name.clone())
1430                                    .collect();
1431                                self.insights_state.insights.show_dropdown = true;
1432                            } else {
1433                                self.insights_state.insights.log_group_matches.clear();
1434                                self.insights_state.insights.show_dropdown = false;
1435                            }
1436                        }
1437                        _ => {}
1438                    }
1439                } else if self.mode == Mode::FilterInput {
1440                    // Check if we should capture digits for page navigation
1441                    let is_pagination_focused = if self.current_service
1442                        == Service::LambdaApplications
1443                    {
1444                        crate::lambda::applications::is_pagination_focused(self)
1445                    } else if self.current_service == Service::CloudFormationStacks {
1446                        self.cfn_state.input_focus == InputFocus::Pagination
1447                    } else if self.current_service == Service::IamRoles
1448                        && self.iam_state.current_role.is_none()
1449                    {
1450                        self.iam_state.role_input_focus == InputFocus::Pagination
1451                    } else if self.view_mode == ViewMode::PolicyView {
1452                        self.iam_state.policy_input_focus == InputFocus::Pagination
1453                    } else if self.current_service == Service::CloudWatchAlarms {
1454                        self.alarms_state.input_focus == InputFocus::Pagination
1455                    } else if self.current_service == Service::CloudTrailEvents {
1456                        self.cloudtrail_state.input_focus == InputFocus::Pagination
1457                    } else if self.current_service == Service::Ec2Instances {
1458                        self.ec2_state.input_focus == InputFocus::Pagination
1459                    } else if self.current_service == Service::CloudWatchLogGroups {
1460                        self.log_groups_state.input_focus == InputFocus::Pagination
1461                    } else if self.current_service == Service::ApiGatewayApis {
1462                        self.apig_state.input_focus == InputFocus::Pagination
1463                    } else if self.current_service == Service::EcrRepositories
1464                        && self.ecr_state.current_repository.is_none()
1465                    {
1466                        crate::ecr::actions::is_pagination_focused(self)
1467                    } else if self.current_service == Service::LambdaFunctions {
1468                        crate::lambda::functions::is_pagination_focused(self)
1469                    } else if self.current_service == Service::SqsQueues {
1470                        if self.sqs_state.current_queue.is_some()
1471                            && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1472                                || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1473                                || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1474                                || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1475                                || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1476                        {
1477                            self.sqs_state.input_focus == InputFocus::Pagination
1478                        } else {
1479                            false
1480                        }
1481                    } else {
1482                        false
1483                    };
1484
1485                    if is_pagination_focused && c.is_ascii_digit() {
1486                        self.page_input.push(c);
1487                    } else if self.current_service == Service::LambdaApplications {
1488                        if crate::lambda::applications::is_filter_focused(self) {
1489                            self.apply_filter_operation(|f| f.push(c));
1490                        }
1491                    } else if self.current_service == Service::CloudFormationStacks {
1492                        if self.cfn_state.current_stack.is_some()
1493                            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1494                        {
1495                            if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1496                                self.cfn_state.parameters.filter.push(c);
1497                                self.cfn_state.parameters.selected = 0;
1498                            }
1499                        } else if self.cfn_state.current_stack.is_some()
1500                            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1501                        {
1502                            if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1503                                self.cfn_state.outputs.filter.push(c);
1504                                self.cfn_state.outputs.selected = 0;
1505                            }
1506                        } else if self.cfn_state.current_stack.is_some()
1507                            && self.cfn_state.detail_tab == CfnDetailTab::Resources
1508                        {
1509                            if self.cfn_state.resources_input_focus == InputFocus::Filter {
1510                                self.cfn_state.resources.filter.push(c);
1511                                self.cfn_state.resources.selected = 0;
1512                            }
1513                        } else if self.cfn_state.input_focus == InputFocus::Filter {
1514                            self.apply_filter_operation(|f| f.push(c));
1515                        }
1516                    } else if self.current_service == Service::EcrRepositories
1517                        && self.ecr_state.current_repository.is_none()
1518                    {
1519                        if self.ecr_state.input_focus == InputFocus::Filter {
1520                            self.apply_filter_operation(|f| f.push(c));
1521                        }
1522                    } else if self.current_service == Service::IamRoles
1523                        && self.iam_state.current_role.is_none()
1524                    {
1525                        if self.iam_state.role_input_focus == InputFocus::Filter {
1526                            self.apply_filter_operation(|f| f.push(c));
1527                        }
1528                    } else if self.view_mode == ViewMode::PolicyView {
1529                        if self.iam_state.policy_input_focus == InputFocus::Filter {
1530                            self.apply_filter_operation(|f| f.push(c));
1531                        }
1532                    } else if self.current_service == Service::ApiGatewayApis
1533                        && self.apig_state.current_api.is_some()
1534                        && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
1535                    {
1536                        if self.apig_state.input_focus == InputFocus::Filter {
1537                            self.apply_filter_operation(|f| f.push(c));
1538                        }
1539                    } else if self.current_service == Service::LambdaFunctions {
1540                        if crate::lambda::functions::filter_char_allowed(self) {
1541                            self.apply_filter_operation(|f| f.push(c));
1542                        }
1543                    } else if self.current_service == Service::SqsQueues
1544                        && self.sqs_state.current_queue.is_some()
1545                        && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1546                            || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1547                            || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1548                            || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1549                            || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1550                    {
1551                        if self.sqs_state.input_focus == InputFocus::Filter {
1552                            self.apply_filter_operation(|f| f.push(c));
1553                        }
1554                    } else if self.current_service == Service::Ec2Instances
1555                        && self.ec2_state.current_instance.is_some()
1556                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1557                    {
1558                        if self.ec2_state.input_focus == InputFocus::Filter {
1559                            self.ec2_state.tags.filter.push(c);
1560                            self.ec2_state.tags.selected = 0;
1561                        }
1562                    } else if self.current_service == Service::CloudWatchLogGroups {
1563                        if self.log_groups_state.input_focus == InputFocus::Filter {
1564                            self.apply_filter_operation(|f| f.push(c));
1565                        }
1566                    } else {
1567                        self.apply_filter_operation(|f| f.push(c));
1568                    }
1569                } else if self.mode == Mode::EventFilterInput {
1570                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1571                        self.log_groups_state.event_filter.push(c);
1572                    } else if c.is_ascii_digit() {
1573                        self.log_groups_state.relative_amount.push(c);
1574                    }
1575                } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1576                    self.page_input.push(c);
1577                }
1578            }
1579            Action::FilterBackspace => {
1580                if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1581                    self.service_picker.filter.pop();
1582                    self.service_picker.selected = 0;
1583                } else if self.mode == Mode::TabPicker {
1584                    self.tab_filter.pop();
1585                    self.tab_picker_selected = 0;
1586                } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1587                    self.region_filter.pop();
1588                    self.region_picker_selected = 0;
1589                } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1590                    self.profile_filter.pop();
1591                    self.profile_picker_selected = 0;
1592                } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1593                    self.session_filter.pop();
1594                    self.session_picker_selected = 0;
1595                } else if self.mode == Mode::InsightsInput {
1596                    match self.insights_state.insights.insights_focus {
1597                        InsightsFocus::Query => {
1598                            self.insights_state.insights.query_text.pop();
1599                        }
1600                        InsightsFocus::LogGroupSearch => {
1601                            self.insights_state.insights.log_group_search.pop();
1602                            // Update matches
1603                            if !self.insights_state.insights.log_group_search.is_empty() {
1604                                self.insights_state.insights.log_group_matches = self
1605                                    .log_groups_state
1606                                    .log_groups
1607                                    .items
1608                                    .iter()
1609                                    .filter(|g| {
1610                                        g.name.to_lowercase().contains(
1611                                            &self
1612                                                .insights_state
1613                                                .insights
1614                                                .log_group_search
1615                                                .to_lowercase(),
1616                                        )
1617                                    })
1618                                    .take(50)
1619                                    .map(|g| g.name.clone())
1620                                    .collect();
1621                                self.insights_state.insights.show_dropdown = true;
1622                            } else {
1623                                self.insights_state.insights.log_group_matches.clear();
1624                                self.insights_state.insights.show_dropdown = false;
1625                            }
1626                        }
1627                        _ => {}
1628                    }
1629                } else if self.mode == Mode::FilterInput {
1630                    // Only allow backspace when focus is on the input field
1631                    if self.current_service == Service::CloudFormationStacks {
1632                        if self.cfn_state.current_stack.is_some()
1633                            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1634                        {
1635                            if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1636                                self.cfn_state.parameters.filter.pop();
1637                                self.cfn_state.parameters.selected = 0;
1638                            }
1639                        } else if self.cfn_state.current_stack.is_some()
1640                            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1641                        {
1642                            if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1643                                self.cfn_state.outputs.filter.pop();
1644                                self.cfn_state.outputs.selected = 0;
1645                            }
1646                        } else if self.cfn_state.current_stack.is_some()
1647                            && self.cfn_state.detail_tab == CfnDetailTab::Resources
1648                        {
1649                            if self.cfn_state.resources_input_focus == InputFocus::Filter {
1650                                self.cfn_state.resources.filter.pop();
1651                                self.cfn_state.resources.selected = 0;
1652                            }
1653                        } else if self.cfn_state.input_focus == InputFocus::Filter {
1654                            self.apply_filter_operation(|f| {
1655                                f.pop();
1656                            });
1657                        }
1658                    } else if self.current_service == Service::Ec2Instances
1659                        && self.ec2_state.current_instance.is_some()
1660                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1661                    {
1662                        if self.ec2_state.input_focus == InputFocus::Filter {
1663                            self.ec2_state.tags.filter.pop();
1664                            self.ec2_state.tags.selected = 0;
1665                        }
1666                    } else if self.current_service == Service::CloudWatchLogGroups {
1667                        if self.log_groups_state.input_focus == InputFocus::Filter {
1668                            self.apply_filter_operation(|f| {
1669                                f.pop();
1670                            });
1671                        }
1672                    } else {
1673                        self.apply_filter_operation(|f| {
1674                            f.pop();
1675                        });
1676                    }
1677                } else if self.mode == Mode::EventFilterInput {
1678                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1679                        self.log_groups_state.event_filter.pop();
1680                    } else {
1681                        self.log_groups_state.relative_amount.pop();
1682                    }
1683                }
1684            }
1685            Action::DeleteWord => {
1686                let text = if self.mode == Mode::ServicePicker {
1687                    &mut self.service_picker.filter
1688                } else if self.mode == Mode::InsightsInput {
1689                    use crate::app::InsightsFocus;
1690                    match self.insights_state.insights.insights_focus {
1691                        InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1692                        InsightsFocus::LogGroupSearch => {
1693                            &mut self.insights_state.insights.log_group_search
1694                        }
1695                        _ => return,
1696                    }
1697                } else if self.mode == Mode::FilterInput {
1698                    if let Some(filter) = self.get_active_filter_mut() {
1699                        filter
1700                    } else {
1701                        return;
1702                    }
1703                } else if self.mode == Mode::EventFilterInput {
1704                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1705                        &mut self.log_groups_state.event_filter
1706                    } else {
1707                        &mut self.log_groups_state.relative_amount
1708                    }
1709                } else {
1710                    return;
1711                };
1712
1713                if text.is_empty() {
1714                    return;
1715                }
1716
1717                let mut chars: Vec<char> = text.chars().collect();
1718                while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1719                    chars.pop();
1720                }
1721                while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1722                    chars.pop();
1723                }
1724                *text = chars.into_iter().collect();
1725            }
1726            Action::WordLeft => {
1727                // Not implemented - would need cursor position tracking
1728            }
1729            Action::WordRight => {
1730                // Not implemented - would need cursor position tracking
1731            }
1732            Action::OpenColumnSelector => {
1733                // Don't allow opening preferences in Template or GitSync tabs
1734                if self.current_service == Service::CloudFormationStacks
1735                    && self.cfn_state.current_stack.is_some()
1736                    && (self.cfn_state.detail_tab == CfnDetailTab::Template
1737                        || self.cfn_state.detail_tab == CfnDetailTab::GitSync)
1738                {
1739                    return;
1740                }
1741
1742                // Don't allow opening preferences for IAM user tabs without preferences
1743                if self.current_service == Service::IamUsers
1744                    && self.iam_state.current_user.is_some()
1745                    && self.iam_state.user_tab == UserTab::SecurityCredentials
1746                {
1747                    return;
1748                }
1749
1750                // Don't allow opening preferences for IAM role tabs without preferences
1751                if self.current_service == Service::IamRoles
1752                    && self.iam_state.current_role.is_some()
1753                    && (self.iam_state.role_tab == RoleTab::TrustRelationships
1754                        || self.iam_state.role_tab == RoleTab::RevokeSessions)
1755                {
1756                    return;
1757                }
1758
1759                // Don't allow opening preferences for certain SQS tabs
1760                if self.current_service == Service::SqsQueues
1761                    && self.sqs_state.current_queue.is_some()
1762                    && matches!(
1763                        self.sqs_state.detail_tab,
1764                        SqsQueueDetailTab::QueuePolicies
1765                            | SqsQueueDetailTab::Monitoring
1766                            | SqsQueueDetailTab::DeadLetterQueue
1767                            | SqsQueueDetailTab::Encryption
1768                            | SqsQueueDetailTab::DeadLetterQueueRedriveTasks
1769                    )
1770                {
1771                    return;
1772                }
1773
1774                // Don't allow opening preferences for EC2 instance detail tabs except Tags
1775                if self.current_service == Service::Ec2Instances
1776                    && self.ec2_state.table.expanded_item.is_some()
1777                    && self.ec2_state.detail_tab != Ec2DetailTab::Tags
1778                {
1779                    return;
1780                }
1781
1782                // Don't allow opening preferences for CloudTrail Event JSON
1783                if self.current_service == Service::CloudTrailEvents
1784                    && self.cloudtrail_state.current_event.is_some()
1785                    && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
1786                {
1787                    return;
1788                }
1789
1790                // If we have page input, apply it instead of opening column selector
1791                if !self.page_input.is_empty() {
1792                    if let Ok(page) = self.page_input.parse::<usize>() {
1793                        self.go_to_page(page);
1794                    }
1795                    self.page_input.clear();
1796                } else {
1797                    self.mode = Mode::ColumnSelector;
1798                    self.column_selector_index = 0;
1799                }
1800            }
1801            Action::ToggleColumn => {
1802                if self.current_service == Service::S3Buckets
1803                    && self.s3_state.current_bucket.is_none()
1804                {
1805                    crate::s3::actions::toggle_column(self);
1806                } else if self.current_service == Service::CloudWatchAlarms {
1807                    // Map flat list index to actual item
1808                    // 0: Columns header, 1-16: columns, 17: empty, 18: ViewAs header, 19-20: view options
1809                    // 21: empty, 22: PageSize header, 23-25: page sizes, 26: empty, 27: WrapLines header, 28: wrap option
1810                    let idx = self.column_selector_index;
1811                    if (1..=16).contains(&idx) {
1812                        // Column toggle
1813                        if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1814                            Self::toggle_column_visibility(
1815                                &mut self.cw_alarm_visible_column_ids,
1816                                &self.cw_alarm_column_ids,
1817                                *col,
1818                            );
1819                        }
1820                    } else if idx == 19 {
1821                        self.alarms_state.view_as = AlarmViewMode::Table;
1822                    } else if idx == 20 {
1823                        self.alarms_state.view_as = AlarmViewMode::Cards;
1824                    } else if idx == 23 {
1825                        self.alarms_state.table.page_size = PageSize::Ten;
1826                    } else if idx == 24 {
1827                        self.alarms_state.table.page_size = PageSize::TwentyFive;
1828                    } else if idx == 25 {
1829                        self.alarms_state.table.page_size = PageSize::Fifty;
1830                    } else if idx == 26 {
1831                        self.alarms_state.table.page_size = PageSize::OneHundred;
1832                    } else if idx == 29 {
1833                        self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1834                    }
1835                } else if self.current_service == Service::CloudTrailEvents {
1836                    if self.cloudtrail_state.current_event.is_some()
1837                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
1838                    {
1839                        // Resources table in detail view
1840                        let idx = self.column_selector_index;
1841                        if idx > 0 && idx <= self.cloudtrail_resource_column_ids.len() {
1842                            if let Some(col) = self.cloudtrail_resource_column_ids.get(idx - 1) {
1843                                Self::toggle_column_visibility(
1844                                    &mut self.cloudtrail_resource_visible_column_ids,
1845                                    &self.cloudtrail_resource_column_ids,
1846                                    *col,
1847                                );
1848                            }
1849                        }
1850                    } else {
1851                        // Main events table
1852                        let idx = self.column_selector_index;
1853                        if (1..=14).contains(&idx) {
1854                            if let Some(col) = self.cloudtrail_event_column_ids.get(idx - 1) {
1855                                Self::toggle_column_visibility(
1856                                    &mut self.cloudtrail_event_visible_column_ids,
1857                                    &self.cloudtrail_event_column_ids,
1858                                    *col,
1859                                );
1860                            }
1861                        } else if idx == 17 {
1862                            self.cloudtrail_state.table.page_size = PageSize::Ten;
1863                            self.cloudtrail_state.table.snap_to_page();
1864                        } else if idx == 18 {
1865                            self.cloudtrail_state.table.page_size = PageSize::TwentyFive;
1866                            self.cloudtrail_state.table.snap_to_page();
1867                        } else if idx == 19 {
1868                            self.cloudtrail_state.table.page_size = PageSize::Fifty;
1869                            self.cloudtrail_state.table.snap_to_page();
1870                        } else if idx == 20 {
1871                            self.cloudtrail_state.table.page_size = PageSize::OneHundred;
1872                            self.cloudtrail_state.table.snap_to_page();
1873                        }
1874                    }
1875                } else if self.current_service == Service::ApiGatewayApis {
1876                    if let Some(api) = &self.apig_state.current_api {
1877                        use crate::ui::apig::ApiDetailTab;
1878                        if self.apig_state.detail_tab == ApiDetailTab::Routes {
1879                            // Check if REST API (shows resources) or HTTP/WebSocket (shows routes)
1880                            if api.protocol_type.to_uppercase() == "REST" {
1881                                // REST API - toggle resource columns (skip first column - it's locked)
1882                                let idx = self.column_selector_index;
1883                                if idx > 1 && idx <= self.apig_resource_column_ids.len() {
1884                                    if let Some(col) = self.apig_resource_column_ids.get(idx - 1) {
1885                                        if let Some(pos) = self
1886                                            .apig_resource_visible_column_ids
1887                                            .iter()
1888                                            .position(|c| c == col)
1889                                        {
1890                                            self.apig_resource_visible_column_ids.remove(pos);
1891                                        } else {
1892                                            self.apig_resource_visible_column_ids.push(*col);
1893                                        }
1894                                    }
1895                                }
1896                            } else {
1897                                // HTTP/WebSocket API - toggle route columns (skip first column - it's locked)
1898                                let idx = self.column_selector_index;
1899                                if idx > 1 && idx <= self.apig_route_column_ids.len() {
1900                                    if let Some(col) = self.apig_route_column_ids.get(idx - 1) {
1901                                        if let Some(pos) = self
1902                                            .apig_route_visible_column_ids
1903                                            .iter()
1904                                            .position(|c| c == col)
1905                                        {
1906                                            self.apig_route_visible_column_ids.remove(pos);
1907                                        } else {
1908                                            self.apig_route_visible_column_ids.push(*col);
1909                                        }
1910                                    }
1911                                }
1912                            }
1913                            // idx == 1 is the locked first column, do nothing
1914                        }
1915                    } else {
1916                        // API list view - toggle API columns
1917                        let idx = self.column_selector_index;
1918                        if idx > 0 && idx <= self.apig_api_column_ids.len() {
1919                            if let Some(col) = self.apig_api_column_ids.get(idx - 1) {
1920                                if let Some(pos) = self
1921                                    .apig_api_visible_column_ids
1922                                    .iter()
1923                                    .position(|c| c == col)
1924                                {
1925                                    self.apig_api_visible_column_ids.remove(pos);
1926                                } else {
1927                                    self.apig_api_visible_column_ids.push(*col);
1928                                }
1929                            }
1930                        } else if idx == self.apig_api_column_ids.len() + 3 {
1931                            self.apig_state.apis.page_size = PageSize::Ten;
1932                        } else if idx == self.apig_api_column_ids.len() + 4 {
1933                            self.apig_state.apis.page_size = PageSize::TwentyFive;
1934                        } else if idx == self.apig_api_column_ids.len() + 5 {
1935                            self.apig_state.apis.page_size = PageSize::Fifty;
1936                        } else if idx == self.apig_api_column_ids.len() + 6 {
1937                            self.apig_state.apis.page_size = PageSize::OneHundred;
1938                        }
1939                    }
1940                } else if self.current_service == Service::EcrRepositories {
1941                    crate::ecr::actions::toggle_column(self);
1942                } else if self.current_service == Service::Ec2Instances {
1943                    if self.ec2_state.current_instance.is_some()
1944                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1945                    {
1946                        let idx = self.column_selector_index;
1947                        if idx > 0 && idx <= self.ec2_state.tag_column_ids.len() {
1948                            if let Some(col) = self.ec2_state.tag_column_ids.get(idx - 1) {
1949                                if let Some(pos) = self
1950                                    .ec2_state
1951                                    .tag_visible_column_ids
1952                                    .iter()
1953                                    .position(|c| c == col)
1954                                {
1955                                    self.ec2_state.tag_visible_column_ids.remove(pos);
1956                                } else {
1957                                    self.ec2_state.tag_visible_column_ids.push(col.clone());
1958                                }
1959                            }
1960                        } else if idx == self.ec2_state.tag_column_ids.len() + 3 {
1961                            self.ec2_state.tags.page_size = PageSize::Ten;
1962                        } else if idx == self.ec2_state.tag_column_ids.len() + 4 {
1963                            self.ec2_state.tags.page_size = PageSize::TwentyFive;
1964                        } else if idx == self.ec2_state.tag_column_ids.len() + 5 {
1965                            self.ec2_state.tags.page_size = PageSize::Fifty;
1966                        } else if idx == self.ec2_state.tag_column_ids.len() + 6 {
1967                            self.ec2_state.tags.page_size = PageSize::OneHundred;
1968                        }
1969                    } else {
1970                        let idx = self.column_selector_index;
1971                        if idx > 0 && idx <= self.ec2_column_ids.len() {
1972                            if let Some(col) = self.ec2_column_ids.get(idx - 1) {
1973                                if let Some(pos) =
1974                                    self.ec2_visible_column_ids.iter().position(|c| c == col)
1975                                {
1976                                    self.ec2_visible_column_ids.remove(pos);
1977                                } else {
1978                                    self.ec2_visible_column_ids.push(*col);
1979                                }
1980                            }
1981                        } else if idx == self.ec2_column_ids.len() + 3 {
1982                            self.ec2_state.table.page_size = PageSize::Ten;
1983                        } else if idx == self.ec2_column_ids.len() + 4 {
1984                            self.ec2_state.table.page_size = PageSize::TwentyFive;
1985                        } else if idx == self.ec2_column_ids.len() + 5 {
1986                            self.ec2_state.table.page_size = PageSize::Fifty;
1987                        } else if idx == self.ec2_column_ids.len() + 6 {
1988                            self.ec2_state.table.page_size = PageSize::OneHundred;
1989                        }
1990                    }
1991                } else if self.current_service == Service::SqsQueues {
1992                    if self.sqs_state.current_queue.is_some()
1993                        && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1994                    {
1995                        // Triggers tab - columns + page size
1996                        let idx = self.column_selector_index;
1997                        if idx > 0 && idx <= self.sqs_state.trigger_column_ids.len() {
1998                            if let Some(col) = self.sqs_state.trigger_column_ids.get(idx - 1) {
1999                                if let Some(pos) = self
2000                                    .sqs_state
2001                                    .trigger_visible_column_ids
2002                                    .iter()
2003                                    .position(|c| c == col)
2004                                {
2005                                    self.sqs_state.trigger_visible_column_ids.remove(pos);
2006                                } else {
2007                                    self.sqs_state.trigger_visible_column_ids.push(col.clone());
2008                                }
2009                            }
2010                        } else if idx == self.sqs_state.trigger_column_ids.len() + 3 {
2011                            self.sqs_state.triggers.page_size = PageSize::Ten;
2012                        } else if idx == self.sqs_state.trigger_column_ids.len() + 4 {
2013                            self.sqs_state.triggers.page_size = PageSize::TwentyFive;
2014                        } else if idx == self.sqs_state.trigger_column_ids.len() + 5 {
2015                            self.sqs_state.triggers.page_size = PageSize::Fifty;
2016                        } else if idx == self.sqs_state.trigger_column_ids.len() + 6 {
2017                            self.sqs_state.triggers.page_size = PageSize::OneHundred;
2018                        }
2019                    } else if self.sqs_state.current_queue.is_some()
2020                        && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2021                    {
2022                        // Pipes tab - columns + page size
2023                        let idx = self.column_selector_index;
2024                        if idx > 0 && idx <= self.sqs_state.pipe_column_ids.len() {
2025                            if let Some(col) = self.sqs_state.pipe_column_ids.get(idx - 1) {
2026                                if let Some(pos) = self
2027                                    .sqs_state
2028                                    .pipe_visible_column_ids
2029                                    .iter()
2030                                    .position(|c| c == col)
2031                                {
2032                                    self.sqs_state.pipe_visible_column_ids.remove(pos);
2033                                } else {
2034                                    self.sqs_state.pipe_visible_column_ids.push(col.clone());
2035                                }
2036                            }
2037                        } else if idx == self.sqs_state.pipe_column_ids.len() + 3 {
2038                            self.sqs_state.pipes.page_size = PageSize::Ten;
2039                        } else if idx == self.sqs_state.pipe_column_ids.len() + 4 {
2040                            self.sqs_state.pipes.page_size = PageSize::TwentyFive;
2041                        } else if idx == self.sqs_state.pipe_column_ids.len() + 5 {
2042                            self.sqs_state.pipes.page_size = PageSize::Fifty;
2043                        } else if idx == self.sqs_state.pipe_column_ids.len() + 6 {
2044                            self.sqs_state.pipes.page_size = PageSize::OneHundred;
2045                        }
2046                    } else if self.sqs_state.current_queue.is_some()
2047                        && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2048                    {
2049                        // Tags tab - columns + page size
2050                        let idx = self.column_selector_index;
2051                        if idx > 0 && idx <= self.sqs_state.tag_column_ids.len() {
2052                            if let Some(col) = self.sqs_state.tag_column_ids.get(idx - 1) {
2053                                if let Some(pos) = self
2054                                    .sqs_state
2055                                    .tag_visible_column_ids
2056                                    .iter()
2057                                    .position(|c| c == col)
2058                                {
2059                                    self.sqs_state.tag_visible_column_ids.remove(pos);
2060                                } else {
2061                                    self.sqs_state.tag_visible_column_ids.push(col.clone());
2062                                }
2063                            }
2064                        } else if idx == self.sqs_state.tag_column_ids.len() + 3 {
2065                            self.sqs_state.tags.page_size = PageSize::Ten;
2066                        } else if idx == self.sqs_state.tag_column_ids.len() + 4 {
2067                            self.sqs_state.tags.page_size = PageSize::TwentyFive;
2068                        } else if idx == self.sqs_state.tag_column_ids.len() + 5 {
2069                            self.sqs_state.tags.page_size = PageSize::Fifty;
2070                        } else if idx == self.sqs_state.tag_column_ids.len() + 6 {
2071                            self.sqs_state.tags.page_size = PageSize::OneHundred;
2072                        }
2073                    } else if self.sqs_state.current_queue.is_some()
2074                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2075                    {
2076                        // Subscriptions tab - columns + page size
2077                        let idx = self.column_selector_index;
2078                        if idx > 0 && idx <= self.sqs_state.subscription_column_ids.len() {
2079                            if let Some(col) = self.sqs_state.subscription_column_ids.get(idx - 1) {
2080                                if let Some(pos) = self
2081                                    .sqs_state
2082                                    .subscription_visible_column_ids
2083                                    .iter()
2084                                    .position(|c| c == col)
2085                                {
2086                                    self.sqs_state.subscription_visible_column_ids.remove(pos);
2087                                } else {
2088                                    self.sqs_state
2089                                        .subscription_visible_column_ids
2090                                        .push(col.clone());
2091                                }
2092                            }
2093                        } else if idx == self.sqs_state.subscription_column_ids.len() + 3 {
2094                            self.sqs_state.subscriptions.page_size = PageSize::Ten;
2095                        } else if idx == self.sqs_state.subscription_column_ids.len() + 4 {
2096                            self.sqs_state.subscriptions.page_size = PageSize::TwentyFive;
2097                        } else if idx == self.sqs_state.subscription_column_ids.len() + 5 {
2098                            self.sqs_state.subscriptions.page_size = PageSize::Fifty;
2099                        } else if idx == self.sqs_state.subscription_column_ids.len() + 6 {
2100                            self.sqs_state.subscriptions.page_size = PageSize::OneHundred;
2101                        }
2102                    } else {
2103                        // Queues list - columns + page size
2104                        let idx = self.column_selector_index;
2105                        if let Some(col) = self.sqs_column_ids.get(idx) {
2106                            if let Some(pos) =
2107                                self.sqs_visible_column_ids.iter().position(|c| c == col)
2108                            {
2109                                self.sqs_visible_column_ids.remove(pos);
2110                            } else {
2111                                self.sqs_visible_column_ids.push(*col);
2112                            }
2113                        } else if idx == self.sqs_column_ids.len() + 2 {
2114                            self.sqs_state.queues.page_size = PageSize::Ten;
2115                        } else if idx == self.sqs_column_ids.len() + 3 {
2116                            self.sqs_state.queues.page_size = PageSize::TwentyFive;
2117                        } else if idx == self.sqs_column_ids.len() + 4 {
2118                            self.sqs_state.queues.page_size = PageSize::Fifty;
2119                        } else if idx == self.sqs_column_ids.len() + 5 {
2120                            self.sqs_state.queues.page_size = PageSize::OneHundred;
2121                        }
2122                    }
2123                } else if self.current_service == Service::LambdaFunctions {
2124                    crate::lambda::functions::toggle_column(self);
2125                } else if self.current_service == Service::LambdaApplications {
2126                    crate::lambda::applications::toggle_column(self);
2127                } else if self.view_mode == ViewMode::Events {
2128                    if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
2129                    {
2130                        if let Some(pos) = self
2131                            .cw_log_event_visible_column_ids
2132                            .iter()
2133                            .position(|c| c == col)
2134                        {
2135                            self.cw_log_event_visible_column_ids.remove(pos);
2136                        } else {
2137                            self.cw_log_event_visible_column_ids.push(*col);
2138                        }
2139                    }
2140                } else if self.view_mode == ViewMode::Detail {
2141                    let idx = self.column_selector_index;
2142                    if self.log_groups_state.detail_tab == DetailTab::Tags {
2143                        // Tags tab
2144                        if idx > 0 && idx <= self.cw_log_tag_column_ids.len() {
2145                            if let Some(col) = self.cw_log_tag_column_ids.get(idx - 1) {
2146                                if let Some(pos) = self
2147                                    .cw_log_tag_visible_column_ids
2148                                    .iter()
2149                                    .position(|c| c == col)
2150                                {
2151                                    self.cw_log_tag_visible_column_ids.remove(pos);
2152                                } else {
2153                                    self.cw_log_tag_visible_column_ids.push(*col);
2154                                }
2155                            }
2156                        } else if idx == self.cw_log_tag_column_ids.len() + 3 {
2157                            self.log_groups_state.tags.page_size = PageSize::Ten;
2158                        } else if idx == self.cw_log_tag_column_ids.len() + 4 {
2159                            self.log_groups_state.tags.page_size = PageSize::TwentyFive;
2160                        } else if idx == self.cw_log_tag_column_ids.len() + 5 {
2161                            self.log_groups_state.tags.page_size = PageSize::Fifty;
2162                        } else if idx == self.cw_log_tag_column_ids.len() + 6 {
2163                            self.log_groups_state.tags.page_size = PageSize::OneHundred;
2164                        }
2165                    } else {
2166                        // Log streams tab
2167                        if idx > 0 && idx <= self.cw_log_stream_column_ids.len() {
2168                            if let Some(col) = self.cw_log_stream_column_ids.get(idx - 1) {
2169                                if let Some(pos) = self
2170                                    .cw_log_stream_visible_column_ids
2171                                    .iter()
2172                                    .position(|c| c == col)
2173                                {
2174                                    self.cw_log_stream_visible_column_ids.remove(pos);
2175                                } else {
2176                                    self.cw_log_stream_visible_column_ids.push(*col);
2177                                }
2178                            }
2179                        } else if idx == self.cw_log_stream_column_ids.len() + 3 {
2180                            self.log_groups_state.stream_page_size = 10;
2181                            self.log_groups_state.stream_current_page = 0;
2182                        } else if idx == self.cw_log_stream_column_ids.len() + 4 {
2183                            self.log_groups_state.stream_page_size = 25;
2184                            self.log_groups_state.stream_current_page = 0;
2185                        } else if idx == self.cw_log_stream_column_ids.len() + 5 {
2186                            self.log_groups_state.stream_page_size = 50;
2187                            self.log_groups_state.stream_current_page = 0;
2188                        } else if idx == self.cw_log_stream_column_ids.len() + 6 {
2189                            self.log_groups_state.stream_page_size = 100;
2190                            self.log_groups_state.stream_current_page = 0;
2191                        }
2192                    }
2193                } else if self.current_service == Service::CloudFormationStacks {
2194                    let idx = self.column_selector_index;
2195                    // Check if we're in StackInfo tab (tags)
2196                    if self.cfn_state.current_stack.is_some()
2197                        && self.cfn_state.detail_tab == CfnDetailTab::StackInfo
2198                    {
2199                        // Tags have 2 columns (Key, Value) - always visible, so only handle page size
2200                        if idx == 4 {
2201                            self.cfn_state.tags.page_size = PageSize::Ten;
2202                        } else if idx == 5 {
2203                            self.cfn_state.tags.page_size = PageSize::TwentyFive;
2204                        } else if idx == 6 {
2205                            self.cfn_state.tags.page_size = PageSize::Fifty;
2206                        } else if idx == 7 {
2207                            self.cfn_state.tags.page_size = PageSize::OneHundred;
2208                        }
2209                    } else if self.cfn_state.current_stack.is_some()
2210                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
2211                    {
2212                        if idx > 0 && idx <= self.cfn_parameter_column_ids.len() {
2213                            if let Some(col) = self.cfn_parameter_column_ids.get(idx - 1) {
2214                                if let Some(pos) = self
2215                                    .cfn_parameter_visible_column_ids
2216                                    .iter()
2217                                    .position(|c| c == col)
2218                                {
2219                                    self.cfn_parameter_visible_column_ids.remove(pos);
2220                                } else {
2221                                    self.cfn_parameter_visible_column_ids.push(col);
2222                                }
2223                            }
2224                        } else if idx == self.cfn_parameter_column_ids.len() + 3 {
2225                            self.cfn_state.parameters.page_size = PageSize::Ten;
2226                        } else if idx == self.cfn_parameter_column_ids.len() + 4 {
2227                            self.cfn_state.parameters.page_size = PageSize::TwentyFive;
2228                        } else if idx == self.cfn_parameter_column_ids.len() + 5 {
2229                            self.cfn_state.parameters.page_size = PageSize::Fifty;
2230                        } else if idx == self.cfn_parameter_column_ids.len() + 6 {
2231                            self.cfn_state.parameters.page_size = PageSize::OneHundred;
2232                        }
2233                    } else if self.cfn_state.current_stack.is_some()
2234                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
2235                    {
2236                        if idx > 0 && idx <= self.cfn_output_column_ids.len() {
2237                            if let Some(col) = self.cfn_output_column_ids.get(idx - 1) {
2238                                if let Some(pos) = self
2239                                    .cfn_output_visible_column_ids
2240                                    .iter()
2241                                    .position(|c| c == col)
2242                                {
2243                                    self.cfn_output_visible_column_ids.remove(pos);
2244                                } else {
2245                                    self.cfn_output_visible_column_ids.push(col);
2246                                }
2247                            }
2248                        } else if idx == self.cfn_output_column_ids.len() + 3 {
2249                            self.cfn_state.outputs.page_size = PageSize::Ten;
2250                        } else if idx == self.cfn_output_column_ids.len() + 4 {
2251                            self.cfn_state.outputs.page_size = PageSize::TwentyFive;
2252                        } else if idx == self.cfn_output_column_ids.len() + 5 {
2253                            self.cfn_state.outputs.page_size = PageSize::Fifty;
2254                        } else if idx == self.cfn_output_column_ids.len() + 6 {
2255                            self.cfn_state.outputs.page_size = PageSize::OneHundred;
2256                        }
2257                    } else if self.cfn_state.current_stack.is_some()
2258                        && self.cfn_state.detail_tab == CfnDetailTab::Resources
2259                    {
2260                        if idx > 0 && idx <= self.cfn_resource_column_ids.len() {
2261                            if let Some(col) = self.cfn_resource_column_ids.get(idx - 1) {
2262                                if let Some(pos) = self
2263                                    .cfn_resource_visible_column_ids
2264                                    .iter()
2265                                    .position(|c| c == col)
2266                                {
2267                                    self.cfn_resource_visible_column_ids.remove(pos);
2268                                } else {
2269                                    self.cfn_resource_visible_column_ids.push(col);
2270                                }
2271                            }
2272                        } else if idx == self.cfn_resource_column_ids.len() + 3 {
2273                            self.cfn_state.resources.page_size = PageSize::Ten;
2274                        } else if idx == self.cfn_resource_column_ids.len() + 4 {
2275                            self.cfn_state.resources.page_size = PageSize::TwentyFive;
2276                        } else if idx == self.cfn_resource_column_ids.len() + 5 {
2277                            self.cfn_state.resources.page_size = PageSize::Fifty;
2278                        } else if idx == self.cfn_resource_column_ids.len() + 6 {
2279                            self.cfn_state.resources.page_size = PageSize::OneHundred;
2280                        }
2281                    } else if self.cfn_state.current_stack.is_none() {
2282                        // Stack list view
2283                        if idx > 0 && idx <= self.cfn_column_ids.len() {
2284                            if let Some(col) = self.cfn_column_ids.get(idx - 1) {
2285                                if let Some(pos) =
2286                                    self.cfn_visible_column_ids.iter().position(|c| c == col)
2287                                {
2288                                    self.cfn_visible_column_ids.remove(pos);
2289                                } else {
2290                                    self.cfn_visible_column_ids.push(*col);
2291                                }
2292                            }
2293                        } else if idx == self.cfn_column_ids.len() + 3 {
2294                            self.cfn_state.table.page_size = PageSize::Ten;
2295                        } else if idx == self.cfn_column_ids.len() + 4 {
2296                            self.cfn_state.table.page_size = PageSize::TwentyFive;
2297                        } else if idx == self.cfn_column_ids.len() + 5 {
2298                            self.cfn_state.table.page_size = PageSize::Fifty;
2299                        } else if idx == self.cfn_column_ids.len() + 6 {
2300                            self.cfn_state.table.page_size = PageSize::OneHundred;
2301                        }
2302                    }
2303                    // Template tab: no column toggle
2304                } else if self.current_service == Service::IamUsers {
2305                    let idx = self.column_selector_index;
2306                    if self.iam_state.current_user.is_some() {
2307                        match self.iam_state.user_tab {
2308                            UserTab::Permissions => {
2309                                // Policy columns
2310                                if idx > 0 && idx <= self.iam_policy_column_ids.len() {
2311                                    if let Some(col) = self.iam_policy_column_ids.get(idx - 1) {
2312                                        if let Some(pos) = self
2313                                            .iam_policy_visible_column_ids
2314                                            .iter()
2315                                            .position(|c| c == col)
2316                                        {
2317                                            self.iam_policy_visible_column_ids.remove(pos);
2318                                        } else {
2319                                            self.iam_policy_visible_column_ids.push(col.clone());
2320                                        }
2321                                    }
2322                                } else if idx == self.iam_policy_column_ids.len() + 3 {
2323                                    self.iam_state.policies.page_size = PageSize::Ten;
2324                                } else if idx == self.iam_policy_column_ids.len() + 4 {
2325                                    self.iam_state.policies.page_size = PageSize::TwentyFive;
2326                                } else if idx == self.iam_policy_column_ids.len() + 5 {
2327                                    self.iam_state.policies.page_size = PageSize::Fifty;
2328                                }
2329                            }
2330                            UserTab::Groups => {
2331                                toggle_iam_page_size_only(
2332                                    idx,
2333                                    5,
2334                                    &mut self.iam_state.user_group_memberships.page_size,
2335                                );
2336                            }
2337                            UserTab::Tags => {
2338                                toggle_iam_page_size_only(
2339                                    idx,
2340                                    5,
2341                                    &mut self.iam_state.user_tags.page_size,
2342                                );
2343                            }
2344                            UserTab::LastAccessed => {
2345                                toggle_iam_page_size_only(
2346                                    idx,
2347                                    6,
2348                                    &mut self.iam_state.last_accessed_services.page_size,
2349                                );
2350                            }
2351                            _ => {}
2352                        }
2353                    } else {
2354                        // User list columns
2355                        toggle_iam_preference_static(
2356                            idx,
2357                            &self.iam_user_column_ids,
2358                            &mut self.iam_user_visible_column_ids,
2359                            &mut self.iam_state.users.page_size,
2360                        );
2361                    }
2362                } else if self.current_service == Service::IamRoles {
2363                    let idx = self.column_selector_index;
2364                    if self.iam_state.current_role.is_some() {
2365                        match self.iam_state.role_tab {
2366                            RoleTab::Permissions => {
2367                                // Policy columns for role detail
2368                                toggle_iam_preference(
2369                                    idx,
2370                                    &self.iam_policy_column_ids,
2371                                    &mut self.iam_policy_visible_column_ids,
2372                                    &mut self.iam_state.policies.page_size,
2373                                );
2374                            }
2375                            RoleTab::LastAccessed => {
2376                                // 0: header, 1-3: columns, 4: empty, 5: PageSize header, 6-8: page sizes
2377                                toggle_iam_page_size_only(
2378                                    idx,
2379                                    6,
2380                                    &mut self.iam_state.last_accessed_services.page_size,
2381                                );
2382                            }
2383                            _ => {}
2384                        }
2385                    } else {
2386                        // Role list columns
2387                        toggle_iam_preference_static(
2388                            idx,
2389                            &self.iam_role_column_ids,
2390                            &mut self.iam_role_visible_column_ids,
2391                            &mut self.iam_state.roles.page_size,
2392                        );
2393                    }
2394                } else if self.current_service == Service::IamUserGroups {
2395                    toggle_iam_preference(
2396                        self.column_selector_index,
2397                        &self.iam_group_column_ids,
2398                        &mut self.iam_group_visible_column_ids,
2399                        &mut self.iam_state.groups.page_size,
2400                    );
2401                } else {
2402                    let idx = self.column_selector_index;
2403                    if idx > 0 && idx <= self.cw_log_group_column_ids.len() {
2404                        if let Some(col) = self.cw_log_group_column_ids.get(idx - 1) {
2405                            if let Some(pos) = self
2406                                .cw_log_group_visible_column_ids
2407                                .iter()
2408                                .position(|c| c == col)
2409                            {
2410                                self.cw_log_group_visible_column_ids.remove(pos);
2411                            } else {
2412                                self.cw_log_group_visible_column_ids.push(*col);
2413                            }
2414                        }
2415                    } else if idx == self.cw_log_group_column_ids.len() + 3 {
2416                        self.log_groups_state.log_groups.page_size = PageSize::Ten;
2417                    } else if idx == self.cw_log_group_column_ids.len() + 4 {
2418                        self.log_groups_state.log_groups.page_size = PageSize::TwentyFive;
2419                    } else if idx == self.cw_log_group_column_ids.len() + 5 {
2420                        self.log_groups_state.log_groups.page_size = PageSize::Fifty;
2421                    } else if idx == self.cw_log_group_column_ids.len() + 6 {
2422                        self.log_groups_state.log_groups.page_size = PageSize::OneHundred;
2423                    }
2424                }
2425            }
2426            Action::NextPreferences => {
2427                if self.current_service == Service::ApiGatewayApis {
2428                    cycle_preference_next(
2429                        &mut self.column_selector_index,
2430                        self.apig_api_column_ids.len(),
2431                    );
2432                } else if self.current_service == Service::CloudWatchAlarms {
2433                    // Jump to next section: Columns(0), ViewAs(18), PageSize(22), WrapLines(28)
2434                    if self.column_selector_index < 18 {
2435                        self.column_selector_index = 18; // ViewAs header
2436                    } else if self.column_selector_index < 22 {
2437                        self.column_selector_index = 22; // PageSize header
2438                    } else if self.column_selector_index < 28 {
2439                        self.column_selector_index = 28; // WrapLines header
2440                    } else {
2441                        self.column_selector_index = 0; // Back to Columns header
2442                    }
2443                } else if self.current_service == Service::EcrRepositories
2444                    && self.ecr_state.current_repository.is_some()
2445                {
2446                    crate::ecr::actions::next_preferences(self);
2447                } else if self.current_service == Service::LambdaFunctions {
2448                    crate::lambda::functions::next_preferences(self);
2449                } else if self.current_service == Service::LambdaApplications {
2450                    crate::lambda::applications::next_preferences(self);
2451                } else if self.current_service == Service::CloudFormationStacks {
2452                    // CloudFormation: Columns(0), PageSize(columns.len() + 2)
2453                    let page_size_idx = self.cfn_column_ids.len() + 2;
2454                    if self.column_selector_index < page_size_idx {
2455                        self.column_selector_index = page_size_idx;
2456                    } else {
2457                        self.column_selector_index = 0;
2458                    }
2459                } else if self.current_service == Service::CloudTrailEvents {
2460                    if self.cloudtrail_state.current_event.is_some()
2461                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2462                    {
2463                        // Resources table has no page size, just columns
2464                        self.column_selector_index = 0;
2465                    } else {
2466                        let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2467                        if self.column_selector_index < page_size_idx {
2468                            self.column_selector_index = page_size_idx;
2469                        } else {
2470                            self.column_selector_index = 0;
2471                        }
2472                    }
2473                } else if self.current_service == Service::Ec2Instances {
2474                    let page_size_idx = self.ec2_column_ids.len() + 2;
2475                    if self.column_selector_index < page_size_idx {
2476                        self.column_selector_index = page_size_idx;
2477                    } else {
2478                        self.column_selector_index = 0;
2479                    }
2480                } else if self.current_service == Service::IamUsers {
2481                    if self.iam_state.current_user.is_some() {
2482                        match self.iam_state.user_tab {
2483                            UserTab::Permissions => {
2484                                // Columns(0), PageSize(columns.len() + 2)
2485                                let page_size_idx = self.iam_policy_column_ids.len() + 2;
2486                                if self.column_selector_index < page_size_idx {
2487                                    self.column_selector_index = page_size_idx;
2488                                } else {
2489                                    self.column_selector_index = 0;
2490                                }
2491                            }
2492                            UserTab::Groups | UserTab::Tags => {
2493                                // Columns(0), PageSize(4)
2494                                if self.column_selector_index < 4 {
2495                                    self.column_selector_index = 4;
2496                                } else {
2497                                    self.column_selector_index = 0;
2498                                }
2499                            }
2500                            UserTab::LastAccessed => {
2501                                // Columns(0), PageSize(5)
2502                                if self.column_selector_index < 5 {
2503                                    self.column_selector_index = 5;
2504                                } else {
2505                                    self.column_selector_index = 0;
2506                                }
2507                            }
2508                            _ => {}
2509                        }
2510                    } else {
2511                        // User columns: Columns(0), PageSize(columns.len() + 2)
2512                        let page_size_idx = self.iam_user_column_ids.len() + 2;
2513                        if self.column_selector_index < page_size_idx {
2514                            self.column_selector_index = page_size_idx;
2515                        } else {
2516                            self.column_selector_index = 0;
2517                        }
2518                    }
2519                } else if self.current_service == Service::IamRoles {
2520                    if self.iam_state.current_role.is_some() {
2521                        match self.iam_state.role_tab {
2522                            RoleTab::Permissions => {
2523                                // Columns(0), PageSize(columns.len() + 2)
2524                                let page_size_idx = self.iam_policy_column_ids.len() + 2;
2525                                if self.column_selector_index < page_size_idx {
2526                                    self.column_selector_index = page_size_idx;
2527                                } else {
2528                                    self.column_selector_index = 0;
2529                                }
2530                            }
2531                            RoleTab::Tags => {
2532                                // Columns(0), PageSize(4)
2533                                if self.column_selector_index < 4 {
2534                                    self.column_selector_index = 4;
2535                                } else {
2536                                    self.column_selector_index = 0;
2537                                }
2538                            }
2539                            RoleTab::LastAccessed => {
2540                                // Columns(0), PageSize(5)
2541                                if self.column_selector_index < 5 {
2542                                    self.column_selector_index = 5;
2543                                } else {
2544                                    self.column_selector_index = 0;
2545                                }
2546                            }
2547                            _ => {}
2548                        }
2549                    } else {
2550                        // Role columns: Columns(0), PageSize(columns.len() + 2)
2551                        let page_size_idx = self.iam_role_column_ids.len() + 2;
2552                        if self.column_selector_index < page_size_idx {
2553                            self.column_selector_index = page_size_idx;
2554                        } else {
2555                            self.column_selector_index = 0;
2556                        }
2557                    }
2558                } else if self.current_service == Service::IamUserGroups {
2559                    // Group columns: Columns(0), PageSize(columns.len() + 2)
2560                    let page_size_idx = self.iam_group_column_ids.len() + 2;
2561                    if self.column_selector_index < page_size_idx {
2562                        self.column_selector_index = page_size_idx;
2563                    } else {
2564                        self.column_selector_index = 0;
2565                    }
2566                } else if self.current_service == Service::SqsQueues
2567                    && self.sqs_state.current_queue.is_some()
2568                    && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2569                {
2570                    // Triggers tab: Columns(0), PageSize(columns.len() + 2)
2571                    let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
2572                    if self.column_selector_index < page_size_idx {
2573                        self.column_selector_index = page_size_idx;
2574                    } else {
2575                        self.column_selector_index = 0;
2576                    }
2577                } else if self.current_service == Service::SqsQueues
2578                    && self.sqs_state.current_queue.is_some()
2579                    && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2580                {
2581                    // Pipes tab: Columns(0), PageSize(columns.len() + 2)
2582                    let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
2583                    if self.column_selector_index < page_size_idx {
2584                        self.column_selector_index = page_size_idx;
2585                    } else {
2586                        self.column_selector_index = 0;
2587                    }
2588                } else if self.current_service == Service::SqsQueues
2589                    && self.sqs_state.current_queue.is_some()
2590                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2591                {
2592                    // Tags tab: Columns(0), PageSize(columns.len() + 2)
2593                    let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
2594                    if self.column_selector_index < page_size_idx {
2595                        self.column_selector_index = page_size_idx;
2596                    } else {
2597                        self.column_selector_index = 0;
2598                    }
2599                } else if self.current_service == Service::SqsQueues
2600                    && self.sqs_state.current_queue.is_some()
2601                    && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2602                {
2603                    // Subscriptions tab: Columns(0), PageSize(columns.len() + 2)
2604                    let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
2605                    if self.column_selector_index < page_size_idx {
2606                        self.column_selector_index = page_size_idx;
2607                    } else {
2608                        self.column_selector_index = 0;
2609                    }
2610                } else if self.current_service == Service::S3Buckets
2611                    && self.s3_state.current_bucket.is_none()
2612                {
2613                    crate::s3::actions::next_preferences(self);
2614                } else if self.current_service == Service::CloudWatchLogGroups {
2615                    if self.view_mode == ViewMode::Events {
2616                        // Events view: only columns, no sections to cycle
2617                    } else if self.view_mode == ViewMode::Detail {
2618                        if self.log_groups_state.detail_tab == DetailTab::Tags {
2619                            // Tags tab: Columns(0), PageSize(columns.len() + 2)
2620                            cycle_preference_next(
2621                                &mut self.column_selector_index,
2622                                self.cw_log_tag_column_ids.len(),
2623                            );
2624                        } else {
2625                            // Streams view: Columns(0), PageSize(columns.len() + 2)
2626                            cycle_preference_next(
2627                                &mut self.column_selector_index,
2628                                self.cw_log_stream_column_ids.len(),
2629                            );
2630                        }
2631                    } else {
2632                        // Log groups view: Columns(0), PageSize(columns.len() + 2)
2633                        cycle_preference_next(
2634                            &mut self.column_selector_index,
2635                            self.cw_log_group_column_ids.len(),
2636                        );
2637                    }
2638                }
2639            }
2640            Action::PrevPreferences => {
2641                if self.current_service == Service::ApiGatewayApis {
2642                    cycle_preference_prev(
2643                        &mut self.column_selector_index,
2644                        self.apig_api_column_ids.len(),
2645                    );
2646                } else if self.current_service == Service::CloudWatchAlarms {
2647                    // Jump to prev section: Columns(0), ViewAs(18), PageSize(22), WrapLines(28)
2648                    if self.column_selector_index >= 28 {
2649                        self.column_selector_index = 22;
2650                    } else if self.column_selector_index >= 22 {
2651                        self.column_selector_index = 18;
2652                    } else if self.column_selector_index >= 18 {
2653                        self.column_selector_index = 0;
2654                    } else {
2655                        self.column_selector_index = 28;
2656                    }
2657                } else if self.current_service == Service::EcrRepositories
2658                    && self.ecr_state.current_repository.is_some()
2659                {
2660                    crate::ecr::actions::prev_preferences(self);
2661                } else if self.current_service == Service::LambdaFunctions {
2662                    crate::lambda::functions::prev_preferences(self);
2663                } else if self.current_service == Service::LambdaApplications {
2664                    crate::lambda::applications::prev_preferences(self);
2665                } else if self.current_service == Service::CloudFormationStacks {
2666                    let page_size_idx = self.cfn_column_ids.len() + 2;
2667                    if self.column_selector_index >= page_size_idx {
2668                        self.column_selector_index = 0;
2669                    } else {
2670                        self.column_selector_index = page_size_idx;
2671                    }
2672                } else if self.current_service == Service::CloudTrailEvents {
2673                    if self.cloudtrail_state.current_event.is_some()
2674                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2675                    {
2676                        // Resources table has no page size, just columns
2677                        self.column_selector_index = 0;
2678                    } else {
2679                        let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2680                        if self.column_selector_index >= page_size_idx {
2681                            self.column_selector_index = 0;
2682                        } else {
2683                            self.column_selector_index = page_size_idx;
2684                        }
2685                    }
2686                } else if self.current_service == Service::Ec2Instances {
2687                    let page_size_idx = self.ec2_column_ids.len() + 2;
2688                    if self.column_selector_index >= page_size_idx {
2689                        self.column_selector_index = 0;
2690                    } else {
2691                        self.column_selector_index = page_size_idx;
2692                    }
2693                } else if self.current_service == Service::IamUsers {
2694                    if self.iam_state.current_user.is_some() {
2695                        match self.iam_state.user_tab {
2696                            UserTab::Permissions => {
2697                                let page_size_idx = self.iam_policy_column_ids.len() + 2;
2698                                if self.column_selector_index >= page_size_idx {
2699                                    self.column_selector_index = 0;
2700                                } else {
2701                                    self.column_selector_index = page_size_idx;
2702                                }
2703                            }
2704                            UserTab::Groups | UserTab::Tags => {
2705                                if self.column_selector_index >= 4 {
2706                                    self.column_selector_index = 0;
2707                                } else {
2708                                    self.column_selector_index = 4;
2709                                }
2710                            }
2711                            UserTab::LastAccessed => {
2712                                if self.column_selector_index >= 5 {
2713                                    self.column_selector_index = 0;
2714                                } else {
2715                                    self.column_selector_index = 5;
2716                                }
2717                            }
2718                            _ => {}
2719                        }
2720                    } else {
2721                        let page_size_idx = self.iam_user_column_ids.len() + 2;
2722                        if self.column_selector_index >= page_size_idx {
2723                            self.column_selector_index = 0;
2724                        } else {
2725                            self.column_selector_index = page_size_idx;
2726                        }
2727                    }
2728                } else if self.current_service == Service::IamRoles {
2729                    if self.iam_state.current_role.is_some() {
2730                        match self.iam_state.role_tab {
2731                            RoleTab::Permissions => {
2732                                let page_size_idx = self.iam_policy_column_ids.len() + 2;
2733                                if self.column_selector_index >= page_size_idx {
2734                                    self.column_selector_index = 0;
2735                                } else {
2736                                    self.column_selector_index = page_size_idx;
2737                                }
2738                            }
2739                            RoleTab::Tags => {
2740                                if self.column_selector_index >= 4 {
2741                                    self.column_selector_index = 0;
2742                                } else {
2743                                    self.column_selector_index = 4;
2744                                }
2745                            }
2746                            RoleTab::LastAccessed => {
2747                                if self.column_selector_index >= 5 {
2748                                    self.column_selector_index = 0;
2749                                } else {
2750                                    self.column_selector_index = 5;
2751                                }
2752                            }
2753                            _ => {}
2754                        }
2755                    } else {
2756                        let page_size_idx = self.iam_role_column_ids.len() + 2;
2757                        if self.column_selector_index >= page_size_idx {
2758                            self.column_selector_index = 0;
2759                        } else {
2760                            self.column_selector_index = page_size_idx;
2761                        }
2762                    }
2763                } else if self.current_service == Service::IamUserGroups {
2764                    let page_size_idx = self.iam_group_column_ids.len() + 2;
2765                    if self.column_selector_index >= page_size_idx {
2766                        self.column_selector_index = 0;
2767                    } else {
2768                        self.column_selector_index = page_size_idx;
2769                    }
2770                } else if self.current_service == Service::SqsQueues
2771                    && self.sqs_state.current_queue.is_some()
2772                {
2773                    let page_size_idx = match self.sqs_state.detail_tab {
2774                        SqsQueueDetailTab::LambdaTriggers => {
2775                            self.sqs_state.trigger_column_ids.len() + 2
2776                        }
2777                        SqsQueueDetailTab::EventBridgePipes => {
2778                            self.sqs_state.pipe_column_ids.len() + 2
2779                        }
2780                        SqsQueueDetailTab::Tagging => self.sqs_state.tag_column_ids.len() + 2,
2781                        SqsQueueDetailTab::SnsSubscriptions => {
2782                            self.sqs_state.subscription_column_ids.len() + 2
2783                        }
2784                        _ => 0,
2785                    };
2786                    if page_size_idx > 0 {
2787                        if self.column_selector_index >= page_size_idx {
2788                            self.column_selector_index = 0;
2789                        } else {
2790                            self.column_selector_index = page_size_idx;
2791                        }
2792                    }
2793                } else if self.current_service == Service::S3Buckets
2794                    && self.s3_state.current_bucket.is_none()
2795                {
2796                    crate::s3::actions::prev_preferences(self);
2797                } else if self.current_service == Service::CloudWatchLogGroups {
2798                    if self.view_mode == ViewMode::Events {
2799                        // Events view: only columns, no sections to cycle
2800                    } else if self.view_mode == ViewMode::Detail {
2801                        if self.log_groups_state.detail_tab == DetailTab::Tags {
2802                            // Tags tab: Columns(0), PageSize(columns.len() + 2)
2803                            cycle_preference_prev(
2804                                &mut self.column_selector_index,
2805                                self.cw_log_tag_column_ids.len(),
2806                            );
2807                        } else {
2808                            // Streams view: Columns(0), PageSize(columns.len() + 2)
2809                            cycle_preference_prev(
2810                                &mut self.column_selector_index,
2811                                self.cw_log_stream_column_ids.len(),
2812                            );
2813                        }
2814                    } else {
2815                        // Log groups view: Columns(0), PageSize(columns.len() + 2)
2816                        cycle_preference_prev(
2817                            &mut self.column_selector_index,
2818                            self.cw_log_group_column_ids.len(),
2819                        );
2820                    }
2821                }
2822            }
2823            Action::CloseColumnSelector => {
2824                self.mode = Mode::Normal;
2825                self.preference_section = Preferences::Columns;
2826            }
2827            Action::NextDetailTab => {
2828                if self.current_service == Service::CloudTrailEvents
2829                    && self.cloudtrail_state.current_event.is_some()
2830                {
2831                    self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
2832                } else if self.current_service == Service::ApiGatewayApis
2833                    && self.apig_state.current_api.is_some()
2834                {
2835                    self.apig_state.detail_tab = self.apig_state.detail_tab.next();
2836                } else if self.current_service == Service::SqsQueues
2837                    && self.sqs_state.current_queue.is_some()
2838                {
2839                    self.sqs_state.detail_tab = self.sqs_state.detail_tab.next();
2840                    if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
2841                        self.sqs_state.set_metrics_loading(true);
2842                        self.sqs_state.set_monitoring_scroll(0);
2843                        self.sqs_state.clear_metrics();
2844                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
2845                        self.sqs_state.triggers.loading = true;
2846                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
2847                        self.sqs_state.pipes.loading = true;
2848                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
2849                        self.sqs_state.tags.loading = true;
2850                    } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
2851                        self.sqs_state.subscriptions.loading = true;
2852                    }
2853                } else if self.current_service == Service::Ec2Instances
2854                    && self.ec2_state.current_instance.is_some()
2855                {
2856                    self.ec2_state.detail_tab = self.ec2_state.detail_tab.next();
2857                    if self.ec2_state.detail_tab == Ec2DetailTab::Tags {
2858                        self.ec2_state.tags.loading = true;
2859                    } else if self.ec2_state.detail_tab == Ec2DetailTab::Monitoring {
2860                        self.ec2_state.set_metrics_loading(true);
2861                        self.ec2_state.set_monitoring_scroll(0);
2862                        self.ec2_state.clear_metrics();
2863                    }
2864                } else if self.current_service == Service::LambdaApplications
2865                    && self.lambda_application_state.current_application.is_some()
2866                {
2867                    crate::lambda::applications::next_detail_tab(self);
2868                } else if self.current_service == Service::IamRoles
2869                    && self.iam_state.current_role.is_some()
2870                {
2871                    self.iam_state.role_tab = self.iam_state.role_tab.next();
2872                    if self.iam_state.role_tab == RoleTab::Tags {
2873                        self.iam_state.tags.loading = true;
2874                    }
2875                } else if self.current_service == Service::IamUsers
2876                    && self.iam_state.current_user.is_some()
2877                {
2878                    self.iam_state.user_tab = self.iam_state.user_tab.next();
2879                    if self.iam_state.user_tab == UserTab::Tags {
2880                        self.iam_state.user_tags.loading = true;
2881                    }
2882                } else if self.current_service == Service::IamUserGroups
2883                    && self.iam_state.current_group.is_some()
2884                {
2885                    self.iam_state.group_tab = self.iam_state.group_tab.next();
2886                } else if self.view_mode == ViewMode::Detail {
2887                    self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
2888                    if self.log_groups_state.detail_tab == DetailTab::Tags {
2889                        self.log_groups_state.tags.loading = true;
2890                    }
2891                } else if self.current_service == Service::S3Buckets {
2892                    crate::s3::actions::next_detail_tab(self);
2893                } else if self.current_service == Service::CloudWatchAlarms {
2894                    self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2895                        AlarmTab::AllAlarms => AlarmTab::InAlarm,
2896                        AlarmTab::InAlarm => AlarmTab::AllAlarms,
2897                    };
2898                    self.alarms_state.table.reset();
2899                } else if self.current_service == Service::EcrRepositories
2900                    && self.ecr_state.current_repository.is_none()
2901                {
2902                    crate::ecr::actions::next_detail_tab(self);
2903                } else if self.current_service == Service::LambdaFunctions
2904                    && self.lambda_state.current_function.is_some()
2905                {
2906                    crate::lambda::functions::next_detail_tab(self);
2907                } else if self.current_service == Service::CloudFormationStacks
2908                    && self.cfn_state.current_stack.is_some()
2909                {
2910                    self.cfn_state.detail_tab = self.cfn_state.detail_tab.next();
2911                }
2912            }
2913            Action::PrevDetailTab => {
2914                if self.current_service == Service::CloudTrailEvents
2915                    && self.cloudtrail_state.current_event.is_some()
2916                {
2917                    self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
2918                } else if self.current_service == Service::ApiGatewayApis
2919                    && self.apig_state.current_api.is_some()
2920                {
2921                    self.apig_state.detail_tab = self.apig_state.detail_tab.prev();
2922                } else if self.current_service == Service::SqsQueues
2923                    && self.sqs_state.current_queue.is_some()
2924                {
2925                    self.sqs_state.detail_tab = self.sqs_state.detail_tab.prev();
2926                    if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
2927                        self.sqs_state.set_metrics_loading(true);
2928                        self.sqs_state.set_monitoring_scroll(0);
2929                        self.sqs_state.clear_metrics();
2930                    }
2931                } else if self.current_service == Service::Ec2Instances
2932                    && self.ec2_state.current_instance.is_some()
2933                {
2934                    self.ec2_state.detail_tab = self.ec2_state.detail_tab.prev();
2935                    if self.ec2_state.detail_tab == Ec2DetailTab::Tags {
2936                        self.ec2_state.tags.loading = true;
2937                    } else if self.ec2_state.detail_tab == Ec2DetailTab::Monitoring {
2938                        self.ec2_state.set_metrics_loading(true);
2939                        self.ec2_state.set_monitoring_scroll(0);
2940                        self.ec2_state.clear_metrics();
2941                    }
2942                } else if self.current_service == Service::LambdaApplications
2943                    && self.lambda_application_state.current_application.is_some()
2944                {
2945                    crate::lambda::applications::prev_detail_tab(self);
2946                } else if self.current_service == Service::IamRoles
2947                    && self.iam_state.current_role.is_some()
2948                {
2949                    self.iam_state.role_tab = self.iam_state.role_tab.prev();
2950                } else if self.current_service == Service::IamUsers
2951                    && self.iam_state.current_user.is_some()
2952                {
2953                    self.iam_state.user_tab = self.iam_state.user_tab.prev();
2954                } else if self.current_service == Service::IamUserGroups
2955                    && self.iam_state.current_group.is_some()
2956                {
2957                    self.iam_state.group_tab = self.iam_state.group_tab.prev();
2958                } else if self.view_mode == ViewMode::Detail {
2959                    self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
2960                } else if self.current_service == Service::S3Buckets {
2961                    crate::s3::actions::prev_detail_tab(self);
2962                } else if self.current_service == Service::CloudWatchAlarms {
2963                    self.alarms_state.alarm_tab = match self.alarms_state.alarm_tab {
2964                        AlarmTab::AllAlarms => AlarmTab::InAlarm,
2965                        AlarmTab::InAlarm => AlarmTab::AllAlarms,
2966                    };
2967                } else if self.current_service == Service::EcrRepositories
2968                    && self.ecr_state.current_repository.is_none()
2969                {
2970                    crate::ecr::actions::prev_detail_tab(self);
2971                } else if self.current_service == Service::LambdaFunctions
2972                    && self.lambda_state.current_function.is_some()
2973                {
2974                    crate::lambda::functions::prev_detail_tab(self);
2975                } else if self.current_service == Service::CloudFormationStacks
2976                    && self.cfn_state.current_stack.is_some()
2977                {
2978                    self.cfn_state.detail_tab = self.cfn_state.detail_tab.prev();
2979                }
2980            }
2981            Action::StartFilter => {
2982                // Don't allow filter mode when no service is selected and no tabs open
2983                if !self.service_selected && self.tabs.is_empty() {
2984                    return;
2985                }
2986
2987                if self.current_service == Service::CloudWatchInsights {
2988                    self.mode = Mode::InsightsInput;
2989                } else if self.current_service == Service::CloudWatchAlarms {
2990                    self.mode = Mode::FilterInput;
2991                } else if self.current_service == Service::CloudTrailEvents {
2992                    self.mode = Mode::FilterInput;
2993                    self.cloudtrail_state.input_focus = InputFocus::Filter;
2994                } else if self.current_service == Service::S3Buckets {
2995                    self.mode = Mode::FilterInput;
2996                    crate::s3::actions::start_filter(self);
2997                } else if self.current_service == Service::ApiGatewayApis
2998                    || self.current_service == Service::EcrRepositories
2999                    || self.current_service == Service::IamUsers
3000                    || self.current_service == Service::IamUserGroups
3001                {
3002                    self.mode = Mode::FilterInput;
3003                    if self.current_service == Service::ApiGatewayApis {
3004                        self.apig_state.input_focus = InputFocus::Filter;
3005                    } else if self.current_service == Service::EcrRepositories
3006                        && self.ecr_state.current_repository.is_none()
3007                    {
3008                        self.ecr_state.input_focus = InputFocus::Filter;
3009                    }
3010                } else if self.current_service == Service::LambdaFunctions {
3011                    self.mode = Mode::FilterInput;
3012                    crate::lambda::functions::start_filter(self);
3013                } else if self.current_service == Service::LambdaApplications {
3014                    self.mode = Mode::FilterInput;
3015                    crate::lambda::applications::start_filter(self);
3016                } else if self.current_service == Service::IamRoles {
3017                    self.mode = Mode::FilterInput;
3018                } else if self.current_service == Service::CloudFormationStacks {
3019                    self.mode = Mode::FilterInput;
3020                    if self.cfn_state.current_stack.is_some()
3021                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3022                    {
3023                        self.cfn_state.parameters_input_focus = InputFocus::Filter;
3024                    } else if self.cfn_state.current_stack.is_some()
3025                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3026                    {
3027                        self.cfn_state.outputs_input_focus = InputFocus::Filter;
3028                    } else {
3029                        self.cfn_state.input_focus = InputFocus::Filter;
3030                    }
3031                } else if self.current_service == Service::SqsQueues {
3032                    self.mode = Mode::FilterInput;
3033                    self.sqs_state.input_focus = InputFocus::Filter;
3034                } else if self.view_mode == ViewMode::List
3035                    || (self.view_mode == ViewMode::Detail
3036                        && (self.log_groups_state.detail_tab == DetailTab::LogStreams
3037                            || self.log_groups_state.detail_tab == DetailTab::Tags))
3038                {
3039                    self.mode = Mode::FilterInput;
3040                    self.log_groups_state.filter_mode = true;
3041                    self.log_groups_state.input_focus = InputFocus::Filter;
3042                } else if self.view_mode == ViewMode::Events {
3043                    self.mode = Mode::EventFilterInput;
3044                }
3045            }
3046            Action::StartEventFilter => {
3047                if self.current_service == Service::CloudWatchInsights {
3048                    self.mode = Mode::InsightsInput;
3049                } else if self.view_mode == ViewMode::List {
3050                    self.mode = Mode::FilterInput;
3051                    self.log_groups_state.filter_mode = true;
3052                    self.log_groups_state.input_focus = InputFocus::Filter;
3053                } else if self.view_mode == ViewMode::Events {
3054                    self.mode = Mode::EventFilterInput;
3055                } else if self.view_mode == ViewMode::Detail
3056                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3057                {
3058                    self.mode = Mode::FilterInput;
3059                    self.log_groups_state.filter_mode = true;
3060                    self.log_groups_state.input_focus = InputFocus::Filter;
3061                }
3062            }
3063            Action::NextFilterFocus => {
3064                if self.current_service == Service::CloudTrailEvents
3065                    && self.cloudtrail_state.current_event.is_some()
3066                {
3067                    self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
3068                } else if self.mode == Mode::FilterInput
3069                    && self.current_service == Service::S3Buckets
3070                {
3071                    crate::s3::actions::next_filter_focus(self);
3072                } else if self.mode == Mode::FilterInput
3073                    && self.current_service == Service::Ec2Instances
3074                {
3075                    self.ec2_state.input_focus =
3076                        self.ec2_state.input_focus.next(&ec2::FILTER_CONTROLS);
3077                } else if self.mode == Mode::FilterInput
3078                    && self.current_service == Service::LambdaApplications
3079                {
3080                    crate::lambda::applications::next_filter_focus(self);
3081                } else if self.mode == Mode::FilterInput
3082                    && self.current_service == Service::IamRoles
3083                    && self.iam_state.current_role.is_some()
3084                {
3085                    use crate::ui::iam::POLICY_FILTER_CONTROLS;
3086                    self.iam_state.policy_input_focus = self
3087                        .iam_state
3088                        .policy_input_focus
3089                        .next(&POLICY_FILTER_CONTROLS);
3090                } else if self.mode == Mode::FilterInput
3091                    && self.current_service == Service::IamRoles
3092                    && self.iam_state.current_role.is_none()
3093                {
3094                    use crate::ui::iam::ROLE_FILTER_CONTROLS;
3095                    self.iam_state.role_input_focus =
3096                        self.iam_state.role_input_focus.next(&ROLE_FILTER_CONTROLS);
3097                } else if self.mode == Mode::FilterInput
3098                    && self.current_service == Service::IamUsers
3099                    && self.iam_state.current_user.is_some()
3100                {
3101                    use crate::ui::iam::{
3102                        POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3103                        USER_SIMPLE_FILTER_CONTROLS,
3104                    };
3105                    if self.iam_state.user_tab == UserTab::Permissions {
3106                        self.iam_state.policy_input_focus = self
3107                            .iam_state
3108                            .policy_input_focus
3109                            .next(&POLICY_FILTER_CONTROLS);
3110                    } else if self.iam_state.user_tab == UserTab::LastAccessed {
3111                        self.iam_state.last_accessed_input_focus = self
3112                            .iam_state
3113                            .last_accessed_input_focus
3114                            .next(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3115                    } else {
3116                        self.iam_state.user_input_focus = self
3117                            .iam_state
3118                            .user_input_focus
3119                            .next(&USER_SIMPLE_FILTER_CONTROLS);
3120                    }
3121                } else if self.mode == Mode::FilterInput
3122                    && self.current_service == Service::IamUserGroups
3123                {
3124                    use crate::ui::iam::GROUP_FILTER_CONTROLS;
3125                    self.iam_state.group_input_focus = self
3126                        .iam_state
3127                        .group_input_focus
3128                        .next(&GROUP_FILTER_CONTROLS);
3129                } else if self.mode == Mode::InsightsInput {
3130                    use crate::app::InsightsFocus;
3131                    self.insights_state.insights.insights_focus =
3132                        match self.insights_state.insights.insights_focus {
3133                            InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
3134                            InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
3135                            InsightsFocus::LogGroupSearch => InsightsFocus::Query,
3136                            InsightsFocus::Query => InsightsFocus::QueryLanguage,
3137                        };
3138                } else if self.mode == Mode::FilterInput
3139                    && self.current_service == Service::CloudFormationStacks
3140                {
3141                    if self.cfn_state.current_stack.is_some()
3142                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3143                    {
3144                        self.cfn_state.parameters_input_focus = self
3145                            .cfn_state
3146                            .parameters_input_focus
3147                            .next(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
3148                    } else if self.cfn_state.current_stack.is_some()
3149                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3150                    {
3151                        self.cfn_state.outputs_input_focus = self
3152                            .cfn_state
3153                            .outputs_input_focus
3154                            .next(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
3155                    } else if self.cfn_state.current_stack.is_some()
3156                        && self.cfn_state.detail_tab == CfnDetailTab::Resources
3157                    {
3158                        self.cfn_state.resources_input_focus = self
3159                            .cfn_state
3160                            .resources_input_focus
3161                            .next(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
3162                    } else {
3163                        self.cfn_state.input_focus = self
3164                            .cfn_state
3165                            .input_focus
3166                            .next(&CfnStateConstants::FILTER_CONTROLS);
3167                    }
3168                } else if self.mode == Mode::FilterInput
3169                    && self.current_service == Service::SqsQueues
3170                {
3171                    if self.sqs_state.current_queue.is_some()
3172                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3173                    {
3174                        use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
3175                        self.sqs_state.input_focus = self
3176                            .sqs_state
3177                            .input_focus
3178                            .next(SUBSCRIPTION_FILTER_CONTROLS);
3179                    } else {
3180                        use crate::ui::sqs::FILTER_CONTROLS;
3181                        self.sqs_state.input_focus =
3182                            self.sqs_state.input_focus.next(FILTER_CONTROLS);
3183                    }
3184                } else if self.mode == Mode::FilterInput
3185                    && self.current_service == Service::CloudWatchLogGroups
3186                {
3187                    use crate::ui::cw::logs::FILTER_CONTROLS;
3188                    self.log_groups_state.input_focus =
3189                        self.log_groups_state.input_focus.next(&FILTER_CONTROLS);
3190                } else if self.mode == Mode::EventFilterInput {
3191                    self.log_groups_state.event_input_focus =
3192                        self.log_groups_state.event_input_focus.next();
3193                } else if self.mode == Mode::FilterInput
3194                    && self.current_service == Service::CloudWatchAlarms
3195                {
3196                    use crate::ui::cw::alarms::FILTER_CONTROLS;
3197                    self.alarms_state.input_focus =
3198                        self.alarms_state.input_focus.next(&FILTER_CONTROLS);
3199                } else if self.mode == Mode::FilterInput
3200                    && self.current_service == Service::CloudTrailEvents
3201                {
3202                    const FILTER_CONTROLS: [InputFocus; 2] =
3203                        [InputFocus::Filter, InputFocus::Pagination];
3204                    self.cloudtrail_state.input_focus =
3205                        self.cloudtrail_state.input_focus.next(&FILTER_CONTROLS);
3206                } else if self.mode == Mode::FilterInput
3207                    && self.current_service == Service::ApiGatewayApis
3208                {
3209                    use crate::ui::apig::FILTER_CONTROLS;
3210                    self.apig_state.input_focus =
3211                        self.apig_state.input_focus.next(&FILTER_CONTROLS);
3212                } else if self.mode == Mode::FilterInput
3213                    && self.current_service == Service::EcrRepositories
3214                    && self.ecr_state.current_repository.is_none()
3215                {
3216                    crate::ecr::actions::next_filter_focus(self);
3217                } else if self.mode == Mode::FilterInput
3218                    && self.current_service == Service::LambdaFunctions
3219                {
3220                    crate::lambda::functions::next_filter_focus(self);
3221                }
3222            }
3223            Action::PrevFilterFocus => {
3224                if self.current_service == Service::CloudTrailEvents
3225                    && self.cloudtrail_state.current_event.is_some()
3226                {
3227                    self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
3228                } else if self.mode == Mode::FilterInput
3229                    && self.current_service == Service::ApiGatewayApis
3230                {
3231                    use crate::ui::apig::FILTER_CONTROLS;
3232                    self.apig_state.input_focus =
3233                        self.apig_state.input_focus.prev(&FILTER_CONTROLS);
3234                } else if self.mode == Mode::FilterInput
3235                    && self.current_service == Service::S3Buckets
3236                {
3237                    crate::s3::actions::prev_filter_focus(self);
3238                } else if self.mode == Mode::FilterInput
3239                    && self.current_service == Service::Ec2Instances
3240                {
3241                    self.ec2_state.input_focus =
3242                        self.ec2_state.input_focus.prev(&ec2::FILTER_CONTROLS);
3243                } else if self.mode == Mode::FilterInput
3244                    && self.current_service == Service::LambdaApplications
3245                {
3246                    crate::lambda::applications::prev_filter_focus(self);
3247                } else if self.mode == Mode::FilterInput
3248                    && self.current_service == Service::CloudFormationStacks
3249                {
3250                    if self.cfn_state.current_stack.is_some()
3251                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3252                    {
3253                        self.cfn_state.parameters_input_focus = self
3254                            .cfn_state
3255                            .parameters_input_focus
3256                            .prev(&CfnStateConstants::PARAMETERS_FILTER_CONTROLS);
3257                    } else if self.cfn_state.current_stack.is_some()
3258                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3259                    {
3260                        self.cfn_state.outputs_input_focus = self
3261                            .cfn_state
3262                            .outputs_input_focus
3263                            .prev(&CfnStateConstants::OUTPUTS_FILTER_CONTROLS);
3264                    } else if self.cfn_state.current_stack.is_some()
3265                        && self.cfn_state.detail_tab == CfnDetailTab::Resources
3266                    {
3267                        self.cfn_state.resources_input_focus = self
3268                            .cfn_state
3269                            .resources_input_focus
3270                            .prev(&CfnStateConstants::RESOURCES_FILTER_CONTROLS);
3271                    } else {
3272                        self.cfn_state.input_focus = self
3273                            .cfn_state
3274                            .input_focus
3275                            .prev(&CfnStateConstants::FILTER_CONTROLS);
3276                    }
3277                } else if self.mode == Mode::FilterInput
3278                    && self.current_service == Service::SqsQueues
3279                {
3280                    if self.sqs_state.current_queue.is_some()
3281                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
3282                    {
3283                        use crate::ui::sqs::SUBSCRIPTION_FILTER_CONTROLS;
3284                        self.sqs_state.input_focus = self
3285                            .sqs_state
3286                            .input_focus
3287                            .prev(SUBSCRIPTION_FILTER_CONTROLS);
3288                    } else {
3289                        use crate::ui::sqs::FILTER_CONTROLS;
3290                        self.sqs_state.input_focus =
3291                            self.sqs_state.input_focus.prev(FILTER_CONTROLS);
3292                    }
3293                } else if self.mode == Mode::FilterInput
3294                    && self.current_service == Service::IamRoles
3295                    && self.iam_state.current_role.is_none()
3296                {
3297                    use crate::ui::iam::ROLE_FILTER_CONTROLS;
3298                    self.iam_state.role_input_focus =
3299                        self.iam_state.role_input_focus.prev(&ROLE_FILTER_CONTROLS);
3300                } else if self.mode == Mode::FilterInput
3301                    && self.current_service == Service::IamUsers
3302                    && self.iam_state.current_user.is_some()
3303                {
3304                    use crate::ui::iam::{
3305                        POLICY_FILTER_CONTROLS, USER_LAST_ACCESSED_FILTER_CONTROLS,
3306                        USER_SIMPLE_FILTER_CONTROLS,
3307                    };
3308                    if self.iam_state.user_tab == UserTab::Permissions {
3309                        self.iam_state.policy_input_focus = self
3310                            .iam_state
3311                            .policy_input_focus
3312                            .prev(&POLICY_FILTER_CONTROLS);
3313                    } else if self.iam_state.user_tab == UserTab::LastAccessed {
3314                        self.iam_state.last_accessed_input_focus = self
3315                            .iam_state
3316                            .last_accessed_input_focus
3317                            .prev(&USER_LAST_ACCESSED_FILTER_CONTROLS);
3318                    } else {
3319                        self.iam_state.user_input_focus = self
3320                            .iam_state
3321                            .user_input_focus
3322                            .prev(&USER_SIMPLE_FILTER_CONTROLS);
3323                    }
3324                } else if self.mode == Mode::FilterInput
3325                    && self.current_service == Service::IamUserGroups
3326                {
3327                    use crate::ui::iam::GROUP_FILTER_CONTROLS;
3328                    self.iam_state.group_input_focus = self
3329                        .iam_state
3330                        .group_input_focus
3331                        .prev(&GROUP_FILTER_CONTROLS);
3332                } else if self.mode == Mode::FilterInput
3333                    && self.current_service == Service::CloudWatchLogGroups
3334                {
3335                    use crate::ui::cw::logs::FILTER_CONTROLS;
3336                    self.log_groups_state.input_focus =
3337                        self.log_groups_state.input_focus.prev(&FILTER_CONTROLS);
3338                } else if self.mode == Mode::EventFilterInput {
3339                    self.log_groups_state.event_input_focus =
3340                        self.log_groups_state.event_input_focus.prev();
3341                } else if self.mode == Mode::FilterInput
3342                    && self.current_service == Service::IamRoles
3343                    && self.iam_state.current_role.is_some()
3344                {
3345                    use crate::ui::iam::POLICY_FILTER_CONTROLS;
3346                    self.iam_state.policy_input_focus = self
3347                        .iam_state
3348                        .policy_input_focus
3349                        .prev(&POLICY_FILTER_CONTROLS);
3350                } else if self.mode == Mode::FilterInput
3351                    && self.current_service == Service::CloudWatchAlarms
3352                {
3353                    use crate::ui::cw::alarms::FILTER_CONTROLS;
3354                    self.alarms_state.input_focus =
3355                        self.alarms_state.input_focus.prev(&FILTER_CONTROLS);
3356                } else if self.mode == Mode::FilterInput
3357                    && self.current_service == Service::CloudTrailEvents
3358                {
3359                    const FILTER_CONTROLS: [InputFocus; 2] =
3360                        [InputFocus::Filter, InputFocus::Pagination];
3361                    self.cloudtrail_state.input_focus =
3362                        self.cloudtrail_state.input_focus.prev(&FILTER_CONTROLS);
3363                } else if self.mode == Mode::FilterInput
3364                    && self.current_service == Service::EcrRepositories
3365                    && self.ecr_state.current_repository.is_none()
3366                {
3367                    crate::ecr::actions::prev_filter_focus(self);
3368                } else if self.mode == Mode::FilterInput
3369                    && self.current_service == Service::LambdaFunctions
3370                {
3371                    crate::lambda::functions::prev_filter_focus(self);
3372                }
3373            }
3374            Action::ToggleFilterCheckbox => {
3375                if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
3376                    if self.ec2_state.input_focus == EC2_STATE_FILTER {
3377                        self.ec2_state.state_filter = self.ec2_state.state_filter.next();
3378                        self.ec2_state.table.reset();
3379                    }
3380                } else if self.mode == Mode::InsightsInput {
3381                    use crate::app::InsightsFocus;
3382                    if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3383                        && self.insights_state.insights.show_dropdown
3384                        && !self.insights_state.insights.log_group_matches.is_empty()
3385                    {
3386                        let selected_idx = self.insights_state.insights.dropdown_selected;
3387                        if let Some(group_name) = self
3388                            .insights_state
3389                            .insights
3390                            .log_group_matches
3391                            .get(selected_idx)
3392                        {
3393                            let group_name = group_name.clone();
3394                            if let Some(pos) = self
3395                                .insights_state
3396                                .insights
3397                                .selected_log_groups
3398                                .iter()
3399                                .position(|g| g == &group_name)
3400                            {
3401                                self.insights_state.insights.selected_log_groups.remove(pos);
3402                            } else if self.insights_state.insights.selected_log_groups.len() < 50 {
3403                                self.insights_state
3404                                    .insights
3405                                    .selected_log_groups
3406                                    .push(group_name);
3407                            }
3408                        }
3409                    }
3410                } else if self.mode == Mode::FilterInput
3411                    && self.current_service == Service::CloudFormationStacks
3412                {
3413                    use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
3414                    match self.cfn_state.input_focus {
3415                        STATUS_FILTER => {
3416                            self.cfn_state.status_filter = self.cfn_state.status_filter.next();
3417                            self.cfn_state.table.reset();
3418                        }
3419                        VIEW_NESTED => {
3420                            self.cfn_state.view_nested = !self.cfn_state.view_nested;
3421                            self.cfn_state.table.reset();
3422                        }
3423                        _ => {}
3424                    }
3425                } else if self.mode == Mode::FilterInput
3426                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3427                {
3428                    match self.log_groups_state.input_focus {
3429                        InputFocus::Checkbox("ExactMatch") => {
3430                            self.log_groups_state.exact_match = !self.log_groups_state.exact_match
3431                        }
3432                        InputFocus::Checkbox("ShowExpired") => {
3433                            self.log_groups_state.show_expired = !self.log_groups_state.show_expired
3434                        }
3435                        _ => {}
3436                    }
3437                } else if self.mode == Mode::EventFilterInput
3438                    && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
3439                {
3440                    self.log_groups_state.relative_unit =
3441                        self.log_groups_state.relative_unit.next();
3442                }
3443            }
3444            Action::CycleSortColumn => {
3445                if self.view_mode == ViewMode::Detail
3446                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3447                {
3448                    self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
3449                        StreamSort::Name => StreamSort::CreationTime,
3450                        StreamSort::CreationTime => StreamSort::LastEventTime,
3451                        StreamSort::LastEventTime => StreamSort::Name,
3452                    };
3453                }
3454            }
3455            Action::ToggleSortDirection => {
3456                if self.view_mode == ViewMode::Detail
3457                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3458                {
3459                    self.log_groups_state.stream_sort_desc =
3460                        !self.log_groups_state.stream_sort_desc;
3461                }
3462            }
3463            Action::ScrollUp => {
3464                if self.mode == Mode::ErrorModal {
3465                    self.error_scroll = self.error_scroll.saturating_sub(1);
3466                } else if self.current_service == Service::CloudTrailEvents
3467                    && self.cloudtrail_state.current_event.is_some()
3468                {
3469                    self.cloudtrail_state.event_json_scroll =
3470                        self.cloudtrail_state.event_json_scroll.saturating_sub(10);
3471                } else if self.current_service == Service::LambdaFunctions
3472                    && self.lambda_state.current_function.is_some()
3473                    && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3474                    && !self.lambda_state.is_metrics_loading()
3475                {
3476                    self.lambda_state.set_monitoring_scroll(
3477                        self.lambda_state.monitoring_scroll().saturating_sub(1),
3478                    );
3479                } else if self.current_service == Service::Ec2Instances
3480                    && self.ec2_state.current_instance.is_some()
3481                    && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
3482                    && !self.ec2_state.is_metrics_loading()
3483                {
3484                    self.ec2_state.set_monitoring_scroll(
3485                        self.ec2_state.monitoring_scroll().saturating_sub(1),
3486                    );
3487                } else if self.current_service == Service::SqsQueues
3488                    && self.sqs_state.current_queue.is_some()
3489                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3490                    && !self.sqs_state.is_metrics_loading()
3491                {
3492                    self.sqs_state.set_monitoring_scroll(
3493                        self.sqs_state.monitoring_scroll().saturating_sub(1),
3494                    );
3495                } else if self.view_mode == ViewMode::PolicyView {
3496                    self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
3497                } else if self.current_service == Service::IamRoles
3498                    && self.iam_state.current_role.is_some()
3499                    && self.iam_state.role_tab == RoleTab::TrustRelationships
3500                {
3501                    self.iam_state.trust_policy_scroll =
3502                        self.iam_state.trust_policy_scroll.saturating_sub(10);
3503                } else if self.view_mode == ViewMode::Events {
3504                    if self.log_groups_state.event_scroll_offset == 0
3505                        && self.log_groups_state.has_older_events
3506                    {
3507                        self.log_groups_state.loading = true;
3508                    } else {
3509                        self.log_groups_state.event_scroll_offset =
3510                            self.log_groups_state.event_scroll_offset.saturating_sub(1);
3511                    }
3512                } else if self.view_mode == ViewMode::InsightsResults {
3513                    self.insights_state.insights.results_selected = self
3514                        .insights_state
3515                        .insights
3516                        .results_selected
3517                        .saturating_sub(1);
3518                } else if self.view_mode == ViewMode::Detail {
3519                    self.log_groups_state.selected_stream =
3520                        self.log_groups_state.selected_stream.saturating_sub(1);
3521                    self.log_groups_state.expanded_stream = None;
3522                } else if self.view_mode == ViewMode::List
3523                    && self.current_service == Service::CloudWatchLogGroups
3524                {
3525                    self.log_groups_state.log_groups.selected =
3526                        self.log_groups_state.log_groups.selected.saturating_sub(1);
3527                    self.log_groups_state.log_groups.snap_to_page();
3528                } else if self.current_service == Service::EcrRepositories {
3529                    crate::ecr::actions::scroll_up(self);
3530                }
3531            }
3532            Action::ScrollDown => {
3533                if self.mode == Mode::ErrorModal {
3534                    if let Some(error_msg) = &self.error_message {
3535                        let lines = error_msg.lines().count();
3536                        let max_scroll = lines.saturating_sub(1);
3537                        self.error_scroll = (self.error_scroll + 1).min(max_scroll);
3538                    }
3539                } else if self.current_service == Service::CloudTrailEvents
3540                    && self.cloudtrail_state.current_event.is_some()
3541                {
3542                    if let Some(event) = &self.cloudtrail_state.current_event {
3543                        let lines = event.cloud_trail_event_json.lines().count();
3544                        let max_scroll = lines.saturating_sub(1);
3545                        self.cloudtrail_state.event_json_scroll =
3546                            (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
3547                    }
3548                } else if self.current_service == Service::SqsQueues
3549                    && self.sqs_state.current_queue.is_some()
3550                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3551                {
3552                    self.sqs_state
3553                        .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
3554                } else if self.view_mode == ViewMode::PolicyView {
3555                    let lines = self.iam_state.policy_document.lines().count();
3556                    let max_scroll = lines.saturating_sub(1);
3557                    self.iam_state.policy_scroll =
3558                        (self.iam_state.policy_scroll + 10).min(max_scroll);
3559                } else if self.current_service == Service::IamRoles
3560                    && self.iam_state.current_role.is_some()
3561                    && self.iam_state.role_tab == RoleTab::TrustRelationships
3562                {
3563                    let lines = self.iam_state.trust_policy_document.lines().count();
3564                    let max_scroll = lines.saturating_sub(1);
3565                    self.iam_state.trust_policy_scroll =
3566                        (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
3567                } else if self.view_mode == ViewMode::Events {
3568                    let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3569                    if self.log_groups_state.event_scroll_offset >= max_scroll {
3570                        // At the end, do nothing
3571                    } else {
3572                        self.log_groups_state.event_scroll_offset =
3573                            (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3574                    }
3575                } else if self.view_mode == ViewMode::InsightsResults {
3576                    let max = self
3577                        .insights_state
3578                        .insights
3579                        .query_results
3580                        .len()
3581                        .saturating_sub(1);
3582                    self.insights_state.insights.results_selected =
3583                        (self.insights_state.insights.results_selected + 1).min(max);
3584                } else if self.view_mode == ViewMode::Detail {
3585                    let filtered_streams = filtered_log_streams(self);
3586                    let max = filtered_streams.len().saturating_sub(1);
3587                    self.log_groups_state.selected_stream =
3588                        (self.log_groups_state.selected_stream + 1).min(max);
3589                } else if self.view_mode == ViewMode::List
3590                    && self.current_service == Service::CloudWatchLogGroups
3591                {
3592                    let filtered_groups = filtered_log_groups(self);
3593                    self.log_groups_state
3594                        .log_groups
3595                        .next_item(filtered_groups.len());
3596                } else if self.current_service == Service::EcrRepositories {
3597                    crate::ecr::actions::scroll_down(self);
3598                }
3599            }
3600
3601            Action::Refresh => {
3602                if self.mode == Mode::ProfilePicker {
3603                    self.log_groups_state.loading = true;
3604                    self.log_groups_state.loading_message = "Refreshing...".to_string();
3605                } else if self.mode == Mode::RegionPicker {
3606                    self.measure_region_latencies();
3607                } else if self.mode == Mode::SessionPicker {
3608                    self.sessions = Session::list_all().unwrap_or_default();
3609                } else if self.current_service == Service::CloudWatchAlarms
3610                    && self.alarms_state.current_alarm.is_some()
3611                {
3612                    // Refresh alarm metrics — keep old data visible until new data arrives
3613                    self.alarms_state.metrics_loading = true;
3614                } else if self.current_service == Service::CloudWatchInsights
3615                    && !self.insights_state.insights.selected_log_groups.is_empty()
3616                {
3617                    self.log_groups_state.loading = true;
3618                    self.insights_state.insights.query_completed = true;
3619                } else if self.current_service == Service::LambdaFunctions {
3620                    crate::lambda::functions::refresh(self);
3621                } else if self.current_service == Service::LambdaApplications {
3622                    crate::lambda::applications::refresh(self);
3623                } else if matches!(
3624                    self.view_mode,
3625                    ViewMode::Events | ViewMode::Detail | ViewMode::List
3626                ) {
3627                    self.log_groups_state.loading = true;
3628                }
3629            }
3630            Action::Yank => {
3631                if self.mode == Mode::ErrorModal {
3632                    // Copy error message
3633                    if let Some(error) = &self.error_message {
3634                        copy_to_clipboard(error);
3635                    }
3636                } else if self.view_mode == ViewMode::Events {
3637                    if let Some(event) = self
3638                        .log_groups_state
3639                        .log_events
3640                        .get(self.log_groups_state.event_scroll_offset)
3641                    {
3642                        copy_to_clipboard(&event.message);
3643                    }
3644                } else if self.current_service == Service::EcrRepositories {
3645                    crate::ecr::actions::yank(self);
3646                } else if self.current_service == Service::LambdaFunctions {
3647                    crate::lambda::functions::yank(self);
3648                } else if self.current_service == Service::CloudFormationStacks {
3649                    if let Some(stack_name) = &self.cfn_state.current_stack {
3650                        // In detail view - copy current stack ARN
3651                        if let Some(stack) = self
3652                            .cfn_state
3653                            .table
3654                            .items
3655                            .iter()
3656                            .find(|s| &s.name == stack_name)
3657                        {
3658                            copy_to_clipboard(&stack.stack_id);
3659                        }
3660                    } else {
3661                        // In list view - copy selected stack ARN
3662                        let filtered_stacks = filtered_cloudformation_stacks(self);
3663                        if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
3664                            copy_to_clipboard(&stack.stack_id);
3665                        }
3666                    }
3667                } else if self.current_service == Service::IamUsers {
3668                    if self.iam_state.current_user.is_some() {
3669                        if let Some(user_name) = &self.iam_state.current_user {
3670                            if let Some(user) = self
3671                                .iam_state
3672                                .users
3673                                .items
3674                                .iter()
3675                                .find(|u| u.user_name == *user_name)
3676                            {
3677                                copy_to_clipboard(&user.arn);
3678                            }
3679                        }
3680                    } else {
3681                        let filtered_users = filtered_iam_users(self);
3682                        if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
3683                            copy_to_clipboard(&user.arn);
3684                        }
3685                    }
3686                } else if self.current_service == Service::IamRoles {
3687                    if self.iam_state.current_role.is_some() {
3688                        if let Some(role_name) = &self.iam_state.current_role {
3689                            if let Some(role) = self
3690                                .iam_state
3691                                .roles
3692                                .items
3693                                .iter()
3694                                .find(|r| r.role_name == *role_name)
3695                            {
3696                                copy_to_clipboard(&role.arn);
3697                            }
3698                        }
3699                    } else {
3700                        let filtered_roles = filtered_iam_roles(self);
3701                        if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
3702                            copy_to_clipboard(&role.arn);
3703                        }
3704                    }
3705                } else if self.current_service == Service::IamUserGroups {
3706                    if self.iam_state.current_group.is_some() {
3707                        if let Some(group_name) = &self.iam_state.current_group {
3708                            let arn = iam::format_arn(&self.config.account_id, "group", group_name);
3709                            copy_to_clipboard(&arn);
3710                        }
3711                    } else {
3712                        let filtered_groups: Vec<_> = self
3713                            .iam_state
3714                            .groups
3715                            .items
3716                            .iter()
3717                            .filter(|g| {
3718                                if self.iam_state.groups.filter.is_empty() {
3719                                    true
3720                                } else {
3721                                    g.group_name
3722                                        .to_lowercase()
3723                                        .contains(&self.iam_state.groups.filter.to_lowercase())
3724                                }
3725                            })
3726                            .collect();
3727                        if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
3728                            let arn = iam::format_arn(
3729                                &self.config.account_id,
3730                                "group",
3731                                &group.group_name,
3732                            );
3733                            copy_to_clipboard(&arn);
3734                        }
3735                    }
3736                } else if self.current_service == Service::SqsQueues {
3737                    if self.sqs_state.current_queue.is_some() {
3738                        // In queue detail view - copy queue ARN
3739                        if let Some(queue) = self
3740                            .sqs_state
3741                            .queues
3742                            .items
3743                            .iter()
3744                            .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
3745                        {
3746                            let arn = format!(
3747                                "arn:aws:sqs:{}:{}:{}",
3748                                extract_region(&queue.url),
3749                                extract_account_id(&queue.url),
3750                                queue.name
3751                            );
3752                            copy_to_clipboard(&arn);
3753                        }
3754                    } else {
3755                        // In list view - copy selected queue ARN
3756                        let filtered_queues = filtered_queues(
3757                            &self.sqs_state.queues.items,
3758                            &self.sqs_state.queues.filter,
3759                        );
3760                        if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
3761                            let arn = format!(
3762                                "arn:aws:sqs:{}:{}:{}",
3763                                extract_region(&queue.url),
3764                                extract_account_id(&queue.url),
3765                                queue.name
3766                            );
3767                            copy_to_clipboard(&arn);
3768                        }
3769                    }
3770                } else if self.current_service == Service::ApiGatewayApis {
3771                    if let Some(_api) = &self.apig_state.current_api {
3772                        use crate::ui::apig::ApiDetailTab;
3773                        if self.apig_state.detail_tab == ApiDetailTab::Routes {
3774                            // In routes view - copy selected route ARN
3775                            let (filtered_routes, _) = crate::ui::apig::filter_tree_items(
3776                                &self.apig_state.routes.items,
3777                                &self.apig_state.route_children,
3778                                &self.apig_state.route_filter,
3779                            );
3780                            let filtered_refs: Vec<&Route> = filtered_routes.iter().collect();
3781                            if let Some(route) = self.apig_state.routes.get_selected(&filtered_refs)
3782                            {
3783                                if !route.arn.is_empty() {
3784                                    copy_to_clipboard(&route.arn);
3785                                }
3786                            }
3787                        }
3788                    }
3789                }
3790            }
3791            Action::CopyToClipboard => {
3792                // Request snapshot - will be captured after next render
3793                self.snapshot_requested = true;
3794            }
3795            Action::RetryLoad => {
3796                self.error_message = None;
3797                self.mode = Mode::Normal;
3798                self.log_groups_state.loading = true;
3799            }
3800            Action::ApplyFilter => {
3801                if self.mode == Mode::FilterInput
3802                    && self.current_service == Service::SqsQueues
3803                    && self.sqs_state.input_focus == InputFocus::Dropdown("SubscriptionRegion")
3804                {
3805                    let regions = AwsRegion::all();
3806                    if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
3807                        self.sqs_state.subscription_region_filter = region.code.to_string();
3808                    }
3809                    self.mode = Mode::Normal;
3810                } else if self.mode == Mode::InsightsInput {
3811                    use crate::app::InsightsFocus;
3812                    if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
3813                        && self.insights_state.insights.show_dropdown
3814                    {
3815                        // Close dropdown, exit input mode, and execute query
3816                        self.insights_state.insights.show_dropdown = false;
3817                        self.mode = Mode::Normal;
3818                        if !self.insights_state.insights.selected_log_groups.is_empty() {
3819                            self.log_groups_state.loading = true;
3820                            self.insights_state.insights.query_completed = true;
3821                        }
3822                    }
3823                } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
3824                    if let Ok(page) = self.page_input.parse::<usize>() {
3825                        self.go_to_page(page);
3826                    }
3827                    self.page_input.clear();
3828                } else {
3829                    self.mode = Mode::Normal;
3830                    self.log_groups_state.filter_mode = false;
3831                }
3832            }
3833            Action::ToggleExactMatch => {
3834                if self.view_mode == ViewMode::Detail
3835                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3836                {
3837                    self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
3838                }
3839            }
3840            Action::ToggleShowExpired => {
3841                if self.view_mode == ViewMode::Detail
3842                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3843                {
3844                    self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
3845                }
3846            }
3847            Action::GoBack => {
3848                // ServicePicker: close if we have tabs
3849                if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
3850                    self.mode = Mode::Normal;
3851                    self.service_picker.filter.clear();
3852                }
3853                // API Gateway: go back from API detail to list
3854                else if self.current_service == Service::ApiGatewayApis
3855                    && self.apig_state.current_api.is_some()
3856                {
3857                    self.apig_state.current_api = None;
3858                    self.apig_state.routes.items.clear();
3859                    self.apig_state.detail_tab = crate::ui::apig::ApiDetailTab::Routes;
3860                    self.update_current_tab_breadcrumb();
3861                }
3862                // S3: pop navigation stack first, then exit bucket
3863                else if self.current_service == Service::S3Buckets
3864                    && self.s3_state.current_bucket.is_some()
3865                {
3866                    crate::s3::actions::go_back(self);
3867                }
3868                // ECR: go back from images to repositories
3869                else if self.current_service == Service::EcrRepositories
3870                    && self.ecr_state.current_repository.is_some()
3871                {
3872                    crate::ecr::actions::go_back(self);
3873                }
3874                // EC2: go back from instance detail to list
3875                else if self.current_service == Service::Ec2Instances
3876                    && self.ec2_state.current_instance.is_some()
3877                {
3878                    self.ec2_state.current_instance = None;
3879                    self.view_mode = ViewMode::List;
3880                    self.update_current_tab_breadcrumb();
3881                }
3882                // SQS: go back from queue detail to list
3883                else if self.current_service == Service::SqsQueues
3884                    && self.sqs_state.current_queue.is_some()
3885                {
3886                    self.sqs_state.current_queue = None;
3887                }
3888                // IAM: go back from user detail to list
3889                else if self.current_service == Service::IamUsers
3890                    && self.iam_state.current_user.is_some()
3891                {
3892                    self.iam_state.current_user = None;
3893                    self.iam_state.policies.items.clear();
3894                    self.iam_state.policies.reset();
3895                    self.update_current_tab_breadcrumb();
3896                }
3897                // IAM: go back from group detail to list
3898                else if self.current_service == Service::IamUserGroups
3899                    && self.iam_state.current_group.is_some()
3900                {
3901                    self.iam_state.current_group = None;
3902                    self.update_current_tab_breadcrumb();
3903                }
3904                // IAM: go back from role detail to list
3905                else if self.current_service == Service::IamRoles {
3906                    if self.view_mode == ViewMode::PolicyView {
3907                        // Go back from policy view to role detail
3908                        self.view_mode = ViewMode::Detail;
3909                        self.iam_state.current_policy = None;
3910                        self.iam_state.policy_document.clear();
3911                        self.iam_state.policy_scroll = 0;
3912                        self.update_current_tab_breadcrumb();
3913                    } else if self.iam_state.current_role.is_some() {
3914                        self.iam_state.current_role = None;
3915                        self.iam_state.policies.items.clear();
3916                        self.iam_state.policies.reset();
3917                        self.update_current_tab_breadcrumb();
3918                    }
3919                }
3920                // Lambda: go back from version/alias/function detail
3921                else if self.current_service == Service::LambdaFunctions
3922                    && (self.lambda_state.current_version.is_some()
3923                        || self.lambda_state.current_alias.is_some()
3924                        || self.lambda_state.current_function.is_some())
3925                {
3926                    crate::lambda::functions::go_back(self);
3927                }
3928                // CloudWatch Alarms: go back from alarm detail to list
3929                else if self.current_service == Service::CloudWatchAlarms
3930                    && self.alarms_state.current_alarm.is_some()
3931                {
3932                    self.alarms_state.current_alarm = None;
3933                    self.alarms_state.metric_data.clear();
3934                    self.view_mode = ViewMode::List;
3935                    self.update_current_tab_breadcrumb();
3936                }
3937                // Lambda Applications: go back from application detail to list
3938                else if self.current_service == Service::LambdaApplications
3939                    && self.lambda_application_state.current_application.is_some()
3940                {
3941                    crate::lambda::applications::go_back(self);
3942                }
3943                // CloudFormation: go back from stack detail to list
3944                else if self.current_service == Service::CloudFormationStacks
3945                    && self.cfn_state.current_stack.is_some()
3946                {
3947                    self.cfn_state.current_stack = None;
3948                    self.update_current_tab_breadcrumb();
3949                }
3950                // CloudTrail: go back from event detail to list
3951                else if self.current_service == Service::CloudTrailEvents
3952                    && self.cloudtrail_state.current_event.is_some()
3953                {
3954                    self.cloudtrail_state.current_event = None;
3955                    self.update_current_tab_breadcrumb();
3956                }
3957                // From insights results -> collapse if expanded, otherwise back to sidebar
3958                else if self.view_mode == ViewMode::InsightsResults {
3959                    if self.insights_state.insights.expanded_result.is_some() {
3960                        self.insights_state.insights.expanded_result = None;
3961                    }
3962                }
3963                // From alarms view -> collapse if expanded
3964                else if self.current_service == Service::CloudWatchAlarms {
3965                    if self.alarms_state.table.has_expanded_item() {
3966                        self.alarms_state.table.collapse();
3967                    }
3968                }
3969                // From EC2 instances view -> always collapse
3970                else if self.current_service == Service::Ec2Instances {
3971                    if self.ec2_state.current_instance.is_some()
3972                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
3973                    {
3974                        self.ec2_state.tags.collapse();
3975                    } else {
3976                        self.ec2_state.table.collapse();
3977                    }
3978                }
3979                // From events view -> collapse if expanded, otherwise back to detail view
3980                else if self.view_mode == ViewMode::Events {
3981                    if self.log_groups_state.expanded_event.is_some() {
3982                        self.log_groups_state.expanded_event = None;
3983                    } else {
3984                        self.view_mode = ViewMode::Detail;
3985                        self.log_groups_state.event_filter.clear();
3986                    }
3987                }
3988                // From detail view -> back to list view
3989                else if self.view_mode == ViewMode::Detail {
3990                    self.view_mode = ViewMode::List;
3991                    self.log_groups_state.stream_filter.clear();
3992                    self.log_groups_state.exact_match = false;
3993                    self.log_groups_state.show_expired = false;
3994                }
3995            }
3996            Action::OpenInConsole | Action::OpenInBrowser => {
3997                let url = self.get_console_url();
3998                let _ = webbrowser::open(&url);
3999            }
4000            Action::ShowHelp => {
4001                self.mode = Mode::HelpModal;
4002            }
4003            Action::OpenRegionPicker => {
4004                self.region_filter.clear();
4005                self.region_picker_selected = 0;
4006                self.mode = Mode::RegionPicker;
4007            }
4008            Action::OpenProfilePicker => {
4009                self.profile_filter.clear();
4010                self.profile_picker_selected = 0;
4011                self.available_profiles = Self::load_aws_profiles();
4012                self.mode = Mode::ProfilePicker;
4013            }
4014            Action::OpenCalendar => {
4015                self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
4016                self.calendar_selecting = CalendarField::StartDate;
4017                self.mode = Mode::CalendarPicker;
4018            }
4019            Action::CloseCalendar => {
4020                self.mode = Mode::Normal;
4021                self.calendar_date = None;
4022            }
4023            Action::CalendarPrevDay => {
4024                if let Some(date) = self.calendar_date {
4025                    self.calendar_date = date.checked_sub(time::Duration::days(1));
4026                }
4027            }
4028            Action::CalendarNextDay => {
4029                if let Some(date) = self.calendar_date {
4030                    self.calendar_date = date.checked_add(time::Duration::days(1));
4031                }
4032            }
4033            Action::CalendarPrevWeek => {
4034                if let Some(date) = self.calendar_date {
4035                    self.calendar_date = date.checked_sub(time::Duration::weeks(1));
4036                }
4037            }
4038            Action::CalendarNextWeek => {
4039                if let Some(date) = self.calendar_date {
4040                    self.calendar_date = date.checked_add(time::Duration::weeks(1));
4041                }
4042            }
4043            Action::CalendarPrevMonth => {
4044                if let Some(date) = self.calendar_date {
4045                    self.calendar_date = Some(if date.month() == time::Month::January {
4046                        date.replace_month(time::Month::December)
4047                            .unwrap()
4048                            .replace_year(date.year() - 1)
4049                            .unwrap()
4050                    } else {
4051                        date.replace_month(date.month().previous()).unwrap()
4052                    });
4053                }
4054            }
4055            Action::CalendarNextMonth => {
4056                if let Some(date) = self.calendar_date {
4057                    self.calendar_date = Some(if date.month() == time::Month::December {
4058                        date.replace_month(time::Month::January)
4059                            .unwrap()
4060                            .replace_year(date.year() + 1)
4061                            .unwrap()
4062                    } else {
4063                        date.replace_month(date.month().next()).unwrap()
4064                    });
4065                }
4066            }
4067            Action::CalendarSelect => {
4068                if let Some(date) = self.calendar_date {
4069                    let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
4070                        .unix_timestamp()
4071                        * 1000;
4072                    match self.calendar_selecting {
4073                        CalendarField::StartDate => {
4074                            self.log_groups_state.start_time = Some(timestamp);
4075                            self.calendar_selecting = CalendarField::EndDate;
4076                        }
4077                        CalendarField::EndDate => {
4078                            self.log_groups_state.end_time = Some(timestamp);
4079                            self.mode = Mode::Normal;
4080                            self.calendar_date = None;
4081                        }
4082                    }
4083                }
4084            }
4085        }
4086    }
4087
4088    pub fn filtered_services(&self) -> Vec<&'static str> {
4089        let mut services = if self.service_picker.filter.is_empty() {
4090            self.service_picker.services.clone()
4091        } else {
4092            self.service_picker
4093                .services
4094                .iter()
4095                .filter(|s| {
4096                    s.to_lowercase()
4097                        .contains(&self.service_picker.filter.to_lowercase())
4098                })
4099                .copied()
4100                .collect()
4101        };
4102        services.sort();
4103        services
4104    }
4105
4106    pub fn breadcrumbs(&self) -> String {
4107        if !self.service_selected {
4108            return String::new();
4109        }
4110
4111        let mut parts = vec![];
4112
4113        match self.current_service {
4114            Service::CloudWatchLogGroups => {
4115                parts.push("CloudWatch".to_string());
4116                parts.push("Log groups".to_string());
4117
4118                if self.view_mode != ViewMode::List {
4119                    if let Some(group) = selected_log_group(self) {
4120                        parts.push(group.name.clone());
4121                    }
4122                }
4123
4124                if self.view_mode == ViewMode::Events {
4125                    if let Some(stream) = self
4126                        .log_groups_state
4127                        .log_streams
4128                        .get(self.log_groups_state.selected_stream)
4129                    {
4130                        parts.push(stream.name.clone());
4131                    }
4132                }
4133            }
4134            Service::CloudWatchInsights => {
4135                parts.push("CloudWatch".to_string());
4136                parts.push("Insights".to_string());
4137            }
4138            Service::CloudWatchAlarms => {
4139                parts.push("CloudWatch".to_string());
4140                parts.push("Alarms".to_string());
4141            }
4142            Service::CloudTrailEvents => {
4143                parts.push("CloudTrail".to_string());
4144                parts.push("Event History".to_string());
4145            }
4146            Service::S3Buckets => {
4147                parts.extend(crate::s3::actions::breadcrumb(self));
4148            }
4149            Service::SqsQueues => {
4150                parts.push("SQS".to_string());
4151                parts.push("Queues".to_string());
4152            }
4153            Service::EcrRepositories => {
4154                parts.extend(crate::ecr::actions::breadcrumb(self));
4155            }
4156            Service::LambdaFunctions => {
4157                parts.extend(crate::lambda::functions::breadcrumb(self));
4158            }
4159            Service::LambdaApplications => {
4160                parts.extend(crate::lambda::applications::breadcrumb());
4161            }
4162            Service::CloudFormationStacks => {
4163                parts.push("CloudFormation".to_string());
4164                if let Some(stack_name) = &self.cfn_state.current_stack {
4165                    parts.push(stack_name.clone());
4166                } else {
4167                    parts.push("Stacks".to_string());
4168                }
4169            }
4170            Service::IamUsers => {
4171                parts.push("IAM".to_string());
4172                parts.push("Users".to_string());
4173            }
4174            Service::IamRoles => {
4175                parts.push("IAM".to_string());
4176                parts.push("Roles".to_string());
4177                if let Some(role_name) = &self.iam_state.current_role {
4178                    parts.push(role_name.clone());
4179                    if let Some(policy_name) = &self.iam_state.current_policy {
4180                        parts.push(policy_name.clone());
4181                    }
4182                }
4183            }
4184            Service::IamUserGroups => {
4185                parts.push("IAM".to_string());
4186                parts.push("User Groups".to_string());
4187                if let Some(group_name) = &self.iam_state.current_group {
4188                    parts.push(group_name.clone());
4189                }
4190            }
4191            Service::Ec2Instances => {
4192                parts.push("EC2".to_string());
4193                parts.push("Instances".to_string());
4194            }
4195            Service::ApiGatewayApis => {
4196                parts.push("API Gateway".to_string());
4197                if let Some(api) = &self.apig_state.current_api {
4198                    parts.push(api.name.clone());
4199                } else {
4200                    parts.push("APIs".to_string());
4201                }
4202            }
4203        }
4204
4205        parts.join(" > ")
4206    }
4207
4208    pub fn update_current_tab_breadcrumb(&mut self) {
4209        if !self.tabs.is_empty() {
4210            self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
4211        }
4212    }
4213
4214    pub fn get_console_url(&self) -> String {
4215        use crate::{cfn, cw, iam};
4216
4217        match self.current_service {
4218            Service::CloudWatchLogGroups => {
4219                if self.view_mode == ViewMode::Events {
4220                    if let Some(group) = selected_log_group(self) {
4221                        if let Some(stream) = self
4222                            .log_groups_state
4223                            .log_streams
4224                            .get(self.log_groups_state.selected_stream)
4225                        {
4226                            return cw::logs::console_url_stream(
4227                                &self.config.region,
4228                                &group.name,
4229                                &stream.name,
4230                            );
4231                        }
4232                    }
4233                } else if self.view_mode == ViewMode::Detail {
4234                    if let Some(group) = selected_log_group(self) {
4235                        return cw::logs::console_url_detail(&self.config.region, &group.name);
4236                    }
4237                }
4238                cw::logs::console_url_list(&self.config.region)
4239            }
4240            Service::CloudWatchInsights => cw::insights::console_url(
4241                &self.config.region,
4242                &self.config.account_id,
4243                &self.insights_state.insights.query_text,
4244                &self.insights_state.insights.selected_log_groups,
4245            ),
4246            Service::CloudWatchAlarms => {
4247                let view_type = match self.alarms_state.view_as {
4248                    AlarmViewMode::Table | AlarmViewMode::Detail => "table",
4249                    AlarmViewMode::Cards => "card",
4250                };
4251                cw::alarms::console_url(
4252                    &self.config.region,
4253                    view_type,
4254                    self.alarms_state.table.page_size.value(),
4255                    &self.alarms_state.sort_column,
4256                    self.alarms_state.sort_direction.as_str(),
4257                )
4258            }
4259            Service::CloudTrailEvents => {
4260                if let Some(event) = &self.cloudtrail_state.current_event {
4261                    format!(
4262                        "https://{}.console.aws.amazon.com/cloudtrailv2/home?region={}#/events/{}",
4263                        self.config.region, self.config.region, event.event_id
4264                    )
4265                } else {
4266                    format!(
4267                        "https://{}.console.aws.amazon.com/cloudtrail/home?region={}#/events",
4268                        self.config.region, self.config.region
4269                    )
4270                }
4271            }
4272            Service::S3Buckets => crate::s3::actions::console_url(self),
4273            Service::SqsQueues => {
4274                if let Some(queue_url) = &self.sqs_state.current_queue {
4275                    console_url_queue_detail(&self.config.region, queue_url)
4276                } else {
4277                    console_url_queues(&self.config.region)
4278                }
4279            }
4280            Service::EcrRepositories => crate::ecr::actions::console_url(self),
4281            Service::LambdaFunctions => crate::lambda::functions::console_url(self),
4282            Service::LambdaApplications => crate::lambda::applications::console_url(self),
4283            Service::CloudFormationStacks => {
4284                if let Some(stack_name) = &self.cfn_state.current_stack {
4285                    if let Some(stack) = self
4286                        .cfn_state
4287                        .table
4288                        .items
4289                        .iter()
4290                        .find(|s| &s.name == stack_name)
4291                    {
4292                        return cfn::console_url_stack_detail_with_tab(
4293                            &self.config.region,
4294                            &stack.stack_id,
4295                            &self.cfn_state.detail_tab,
4296                        );
4297                    }
4298                }
4299                cfn::console_url_stacks(&self.config.region)
4300            }
4301            Service::IamUsers => {
4302                if let Some(user_name) = &self.iam_state.current_user {
4303                    let section = match self.iam_state.user_tab {
4304                        UserTab::Permissions => "permissions",
4305                        UserTab::Groups => "groups",
4306                        UserTab::Tags => "tags",
4307                        UserTab::SecurityCredentials => "security_credentials",
4308                        UserTab::LastAccessed => "access_advisor",
4309                    };
4310                    iam::console_url_user_detail(&self.config.region, user_name, section)
4311                } else {
4312                    iam::console_url_users(&self.config.region)
4313                }
4314            }
4315            Service::IamRoles => {
4316                if let Some(policy_name) = &self.iam_state.current_policy {
4317                    if let Some(role_name) = &self.iam_state.current_role {
4318                        return iam::console_url_role_policy(
4319                            &self.config.region,
4320                            role_name,
4321                            policy_name,
4322                        );
4323                    }
4324                }
4325                if let Some(role_name) = &self.iam_state.current_role {
4326                    let section = match self.iam_state.role_tab {
4327                        RoleTab::Permissions => "permissions",
4328                        RoleTab::TrustRelationships => "trust_relationships",
4329                        RoleTab::Tags => "tags",
4330                        RoleTab::LastAccessed => "access_advisor",
4331                        RoleTab::RevokeSessions => "revoke_sessions",
4332                    };
4333                    iam::console_url_role_detail(&self.config.region, role_name, section)
4334                } else {
4335                    iam::console_url_roles(&self.config.region)
4336                }
4337            }
4338            Service::IamUserGroups => iam::console_url_groups(&self.config.region),
4339            Service::Ec2Instances => {
4340                if let Some(instance_id) = &self.ec2_state.current_instance {
4341                    format!(
4342                        "https://{}.console.aws.amazon.com/ec2/home?region={}#InstanceDetails:instanceId={}",
4343                        self.config.region, self.config.region, instance_id
4344                    )
4345                } else {
4346                    format!(
4347                        "https://{}.console.aws.amazon.com/ec2/home?region={}#Instances:",
4348                        self.config.region, self.config.region
4349                    )
4350                }
4351            }
4352            Service::ApiGatewayApis => {
4353                use crate::apig;
4354                if let Some(api) = &self.apig_state.current_api {
4355                    if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
4356                        let protocol = api.protocol_type.to_uppercase();
4357                        if protocol == "REST" {
4358                            // Resources view - use filtered items to get correct resource_id
4359                            let (filtered_items, _) = crate::ui::apig::filter_tree_items(
4360                                &self.apig_state.resources.items,
4361                                &self.apig_state.resource_children,
4362                                &self.apig_state.route_filter,
4363                            );
4364
4365                            let resource_id =
4366                                if self.apig_state.resources.selected < filtered_items.len() {
4367                                    // Find the actual resource at this row in the filtered tree
4368                                    let mut current_row = 0;
4369                                    self.find_resource_at_row(
4370                                        &filtered_items,
4371                                        self.apig_state.resources.selected,
4372                                        &mut current_row,
4373                                    )
4374                                } else {
4375                                    None
4376                                };
4377                            apig::console_url_resources(
4378                                &self.config.region,
4379                                &api.id,
4380                                resource_id.as_deref(),
4381                            )
4382                        } else {
4383                            // Routes view - use filtered items to get correct route_id
4384                            let (filtered_items, filtered_children) =
4385                                crate::ui::apig::filter_tree_items(
4386                                    &self.apig_state.routes.items,
4387                                    &self.apig_state.route_children,
4388                                    &self.apig_state.route_filter,
4389                                );
4390
4391                            let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
4392                                &filtered_items,
4393                                &self.apig_state.expanded_routes,
4394                                &filtered_children,
4395                            );
4396
4397                            let route_id = if self.apig_state.routes.selected < total_rows {
4398                                // Find the actual route_id at this row in the filtered tree
4399                                let mut current_row = 0;
4400                                self.find_route_id_at_row_with_children(
4401                                    &filtered_items,
4402                                    &filtered_children,
4403                                    self.apig_state.routes.selected,
4404                                    &mut current_row,
4405                                )
4406                            } else {
4407                                None
4408                            };
4409                            apig::console_url_routes(
4410                                &self.config.region,
4411                                &api.id,
4412                                route_id.as_deref(),
4413                            )
4414                        }
4415                    } else {
4416                        apig::console_url_api(&self.config.region, &api.id)
4417                    }
4418                } else {
4419                    apig::console_url_apis(&self.config.region)
4420                }
4421            }
4422        }
4423    }
4424
4425    pub fn calculate_total_bucket_rows(&self) -> usize {
4426        calculate_total_bucket_rows(self)
4427    }
4428
4429    fn calculate_total_object_rows(&self) -> usize {
4430        calculate_total_object_rows(self)
4431    }
4432
4433    fn get_column_selector_max(&self) -> usize {
4434        if self.current_service == Service::ApiGatewayApis {
4435            self.apig_api_column_ids.len() + 6
4436        } else if self.current_service == Service::S3Buckets
4437            && self.s3_state.current_bucket.is_none()
4438        {
4439            crate::s3::actions::column_selector_max(self)
4440        } else if self.view_mode == ViewMode::Events {
4441            self.cw_log_event_column_ids.len() - 1
4442        } else if self.view_mode == ViewMode::Detail {
4443            self.cw_log_stream_column_ids.len() + 6
4444        } else if self.current_service == Service::CloudWatchAlarms {
4445            29
4446        } else if self.current_service == Service::CloudTrailEvents {
4447            if self.cloudtrail_state.current_event.is_some()
4448                && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
4449            {
4450                self.cloudtrail_resource_column_ids.len()
4451            } else {
4452                self.cloudtrail_event_column_ids.len() + 6
4453            }
4454        } else if self.current_service == Service::Ec2Instances {
4455            if self.ec2_state.current_instance.is_some()
4456                && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4457            {
4458                self.ec2_state.tag_column_ids.len() + 6
4459            } else {
4460                self.ec2_column_ids.len() + 6
4461            }
4462        } else if self.current_service == Service::EcrRepositories {
4463            crate::ecr::actions::column_selector_max(self)
4464        } else if self.current_service == Service::SqsQueues {
4465            self.sqs_column_ids.len() - 1
4466        } else if self.current_service == Service::LambdaFunctions {
4467            crate::lambda::functions::column_selector_max(self)
4468        } else if self.current_service == Service::LambdaApplications {
4469            crate::lambda::applications::column_selector_max(self)
4470        } else if self.current_service == Service::CloudFormationStacks {
4471            self.cfn_column_ids.len() + 6
4472        } else if self.current_service == Service::IamUsers {
4473            if self.iam_state.current_user.is_some() {
4474                self.iam_policy_column_ids.len() + 5
4475            } else {
4476                self.iam_user_column_ids.len() + 5
4477            }
4478        } else if self.current_service == Service::IamRoles {
4479            if self.iam_state.current_role.is_some() {
4480                self.iam_policy_column_ids.len() + 5
4481            } else {
4482                self.iam_role_column_ids.len() + 5
4483            }
4484        } else {
4485            self.cw_log_group_column_ids.len() + 6
4486        }
4487    }
4488
4489    fn get_column_count(&self) -> usize {
4490        if self.current_service == Service::ApiGatewayApis {
4491            self.apig_api_column_ids.len()
4492        } else if self.current_service == Service::S3Buckets
4493            && self.s3_state.current_bucket.is_none()
4494        {
4495            crate::s3::actions::column_count(self)
4496        } else if self.view_mode == ViewMode::Events {
4497            self.cw_log_event_column_ids.len()
4498        } else if self.view_mode == ViewMode::Detail {
4499            self.cw_log_stream_column_ids.len()
4500        } else if self.current_service == Service::CloudWatchAlarms {
4501            14
4502        } else if self.current_service == Service::CloudTrailEvents {
4503            if self.cloudtrail_state.current_event.is_some()
4504                && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
4505            {
4506                self.cloudtrail_resource_column_ids.len()
4507            } else {
4508                self.cloudtrail_event_column_ids.len()
4509            }
4510        } else if self.current_service == Service::Ec2Instances {
4511            if self.ec2_state.current_instance.is_some()
4512                && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4513            {
4514                self.ec2_state.tag_column_ids.len()
4515            } else {
4516                self.ec2_column_ids.len()
4517            }
4518        } else if self.current_service == Service::EcrRepositories {
4519            crate::ecr::actions::column_count(self)
4520        } else if self.current_service == Service::SqsQueues {
4521            self.sqs_column_ids.len()
4522        } else if self.current_service == Service::LambdaFunctions {
4523            crate::lambda::functions::column_count(self)
4524        } else if self.current_service == Service::LambdaApplications {
4525            crate::lambda::applications::column_count(self)
4526        } else if self.current_service == Service::CloudFormationStacks {
4527            self.cfn_column_ids.len()
4528        } else if self.current_service == Service::IamUsers {
4529            if self.iam_state.current_user.is_some() {
4530                self.iam_policy_column_ids.len()
4531            } else {
4532                self.iam_user_column_ids.len()
4533            }
4534        } else if self.current_service == Service::IamRoles {
4535            if self.iam_state.current_role.is_some() {
4536                self.iam_policy_column_ids.len()
4537            } else {
4538                self.iam_role_column_ids.len()
4539            }
4540        } else {
4541            self.cw_log_group_column_ids.len()
4542        }
4543    }
4544
4545    fn is_blank_row_index(&self, idx: usize) -> bool {
4546        let column_count = self.get_column_count();
4547        // Blank row is at: header(0) + columns(1..=column_count) + blank(column_count+1)
4548        idx == column_count + 1
4549    }
4550
4551    fn next_item(&mut self) {
4552        match self.mode {
4553            Mode::FilterInput => {
4554                if self.current_service == Service::S3Buckets
4555                    && crate::s3::actions::is_pagination_focused(self)
4556                {
4557                    crate::s3::actions::page_down_filter_input(self);
4558                } else if self.current_service == Service::CloudTrailEvents
4559                    && self.cloudtrail_state.input_focus == InputFocus::Pagination
4560                {
4561                    // Navigate to next page
4562                    let page_size = self.cloudtrail_state.table.page_size.value();
4563                    let total_items = self.cloudtrail_state.table.items.len();
4564                    let current_page = self.cloudtrail_state.table.selected / page_size;
4565                    let total_pages = total_items.div_ceil(page_size);
4566                    if current_page + 1 < total_pages {
4567                        self.cloudtrail_state.table.selected = (current_page + 1) * page_size;
4568                    }
4569                } else if self.current_service == Service::CloudFormationStacks {
4570                    use crate::ui::cfn::STATUS_FILTER;
4571                    if self.cfn_state.input_focus == STATUS_FILTER {
4572                        self.cfn_state.status_filter = self.cfn_state.status_filter.next();
4573                        self.cfn_state.table.reset();
4574                    }
4575                } else if self.current_service == Service::IamUsers
4576                    && self.iam_state.current_user.is_some()
4577                {
4578                    use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
4579                    if self.iam_state.user_tab == UserTab::Permissions
4580                        && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
4581                    {
4582                        self.cycle_policy_type_next();
4583                    } else if self.iam_state.user_tab == UserTab::LastAccessed
4584                        && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
4585                    {
4586                        self.iam_state.last_accessed_history_filter =
4587                            self.iam_state.last_accessed_history_filter.next();
4588                        self.iam_state.last_accessed_services.reset();
4589                    }
4590                } else if self.current_service == Service::IamRoles
4591                    && self.iam_state.current_role.is_some()
4592                    && self.iam_state.role_tab == RoleTab::Permissions
4593                {
4594                    use crate::ui::iam::POLICY_TYPE_DROPDOWN;
4595                    if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
4596                        self.cycle_policy_type_next();
4597                    }
4598                } else if self.current_service == Service::Ec2Instances {
4599                    if self.ec2_state.input_focus == EC2_STATE_FILTER {
4600                        self.ec2_state.state_filter = self.ec2_state.state_filter.next();
4601                        self.ec2_state.table.reset();
4602                    }
4603                } else if self.current_service == Service::SqsQueues {
4604                    use crate::ui::sqs::SUBSCRIPTION_REGION;
4605                    if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
4606                        let regions = AwsRegion::all();
4607                        self.sqs_state.subscription_region_selected =
4608                            (self.sqs_state.subscription_region_selected + 1)
4609                                .min(regions.len() - 1);
4610                        self.sqs_state.subscriptions.reset();
4611                    }
4612                }
4613            }
4614            Mode::RegionPicker => {
4615                let filtered = self.get_filtered_regions();
4616                if !filtered.is_empty() {
4617                    self.region_picker_selected =
4618                        (self.region_picker_selected + 1).min(filtered.len() - 1);
4619                }
4620            }
4621            Mode::ProfilePicker => {
4622                let filtered = self.get_filtered_profiles();
4623                if !filtered.is_empty() {
4624                    self.profile_picker_selected =
4625                        (self.profile_picker_selected + 1).min(filtered.len() - 1);
4626                }
4627            }
4628            Mode::SessionPicker => {
4629                let filtered = self.get_filtered_sessions();
4630                if !filtered.is_empty() {
4631                    self.session_picker_selected =
4632                        (self.session_picker_selected + 1).min(filtered.len() - 1);
4633                }
4634            }
4635            Mode::InsightsInput => {
4636                use crate::app::InsightsFocus;
4637                if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
4638                    && self.insights_state.insights.show_dropdown
4639                    && !self.insights_state.insights.log_group_matches.is_empty()
4640                {
4641                    let max = self.insights_state.insights.log_group_matches.len() - 1;
4642                    self.insights_state.insights.dropdown_selected =
4643                        (self.insights_state.insights.dropdown_selected + 1).min(max);
4644                }
4645            }
4646            Mode::ColumnSelector => {
4647                let max = self.get_column_selector_max();
4648                let mut next_idx = (self.column_selector_index + 1).min(max);
4649                // Skip blank row
4650                if self.is_blank_row_index(next_idx) {
4651                    next_idx = (next_idx + 1).min(max);
4652                }
4653                self.column_selector_index = next_idx;
4654            }
4655            Mode::ServicePicker => {
4656                let filtered = self.filtered_services();
4657                if !filtered.is_empty() {
4658                    self.service_picker.selected =
4659                        (self.service_picker.selected + 1).min(filtered.len() - 1);
4660                }
4661            }
4662            Mode::TabPicker => {
4663                let filtered = self.get_filtered_tabs();
4664                if !filtered.is_empty() {
4665                    self.tab_picker_selected =
4666                        (self.tab_picker_selected + 1).min(filtered.len() - 1);
4667                }
4668            }
4669            Mode::Normal => {
4670                if !self.service_selected {
4671                    let filtered = self.filtered_services();
4672                    if !filtered.is_empty() {
4673                        self.service_picker.selected =
4674                            (self.service_picker.selected + 1).min(filtered.len() - 1);
4675                    }
4676                } else if self.current_service == Service::S3Buckets {
4677                    if self.s3_state.current_bucket.is_some() {
4678                        if self.s3_state.object_tab == S3ObjectTab::Properties {
4679                            // Scroll properties view
4680                            self.s3_state.properties_scroll =
4681                                self.s3_state.properties_scroll.saturating_add(1);
4682                        } else {
4683                            // Calculate total rows including all nested preview items
4684                            let total_rows = self.calculate_total_object_rows();
4685                            let max = total_rows.saturating_sub(1);
4686                            self.s3_state.selected_object =
4687                                (self.s3_state.selected_object + 1).min(max);
4688
4689                            // Adjust scroll offset if selection goes below viewport
4690                            let visible_rows = self.s3_state.object_visible_rows.get();
4691                            if self.s3_state.selected_object
4692                                >= self.s3_state.object_scroll_offset + visible_rows
4693                            {
4694                                self.s3_state.object_scroll_offset =
4695                                    self.s3_state.selected_object - visible_rows + 1;
4696                            }
4697                        }
4698                    } else {
4699                        // Navigate rows in bucket list
4700                        let total_rows = crate::ui::s3::calculate_filtered_bucket_rows(self);
4701                        if total_rows > 0 {
4702                            self.s3_state.selected_row =
4703                                (self.s3_state.selected_row + 1).min(total_rows - 1);
4704
4705                            // Adjust scroll offset if selection goes below viewport
4706                            let visible_rows = self.s3_state.bucket_visible_rows.get();
4707                            if self.s3_state.selected_row
4708                                >= self.s3_state.bucket_scroll_offset + visible_rows
4709                            {
4710                                self.s3_state.bucket_scroll_offset =
4711                                    self.s3_state.selected_row - visible_rows + 1;
4712                            }
4713                        }
4714                    }
4715                } else if self.view_mode == ViewMode::InsightsResults {
4716                    let max = self
4717                        .insights_state
4718                        .insights
4719                        .query_results
4720                        .len()
4721                        .saturating_sub(1);
4722                    if self.insights_state.insights.results_selected < max {
4723                        self.insights_state.insights.results_selected += 1;
4724                    }
4725                } else if self.current_service == Service::ApiGatewayApis {
4726                    if let Some(api) = &self.apig_state.current_api {
4727                        if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
4728                            let protocol = api.protocol_type.to_uppercase();
4729                            if protocol == "REST" {
4730                                // Navigate resources tree
4731                                let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
4732                                    &self.apig_state.resources.items,
4733                                    &self.apig_state.expanded_resources,
4734                                    &self.apig_state.resource_children,
4735                                );
4736                                if total_rows > 0 {
4737                                    self.apig_state.resources.selected =
4738                                        (self.apig_state.resources.selected + 1)
4739                                            .min(total_rows - 1);
4740                                }
4741                            } else {
4742                                // Navigate routes tree
4743                                let total_rows = crate::ui::tree::TreeRenderer::count_visible_rows(
4744                                    &self.apig_state.routes.items,
4745                                    &self.apig_state.expanded_routes,
4746                                    &self.apig_state.route_children,
4747                                );
4748                                if total_rows > 0 {
4749                                    self.apig_state.routes.selected =
4750                                        (self.apig_state.routes.selected + 1).min(total_rows - 1);
4751                                }
4752                            }
4753                        }
4754                    } else {
4755                        let filtered = crate::ui::apig::filtered_apis(self);
4756                        if !filtered.is_empty() {
4757                            self.apig_state.apis.next_item(filtered.len());
4758                        }
4759                    }
4760                } else if self.view_mode == ViewMode::PolicyView {
4761                    let lines = self.iam_state.policy_document.lines().count();
4762                    let max_scroll = lines.saturating_sub(1);
4763                    self.iam_state.policy_scroll =
4764                        (self.iam_state.policy_scroll + 1).min(max_scroll);
4765                } else if self.current_service == Service::CloudFormationStacks
4766                    && self.cfn_state.current_stack.is_some()
4767                    && self.cfn_state.detail_tab == CfnDetailTab::Template
4768                {
4769                    let lines = self.cfn_state.template_body.lines().count();
4770                    let max_scroll = lines.saturating_sub(1);
4771                    self.cfn_state.template_scroll =
4772                        (self.cfn_state.template_scroll + 1).min(max_scroll);
4773                } else if self.current_service == Service::SqsQueues
4774                    && self.sqs_state.current_queue.is_some()
4775                    && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
4776                {
4777                    let lines = self.sqs_state.policy_document.lines().count();
4778                    let max_scroll = lines.saturating_sub(1);
4779                    self.sqs_state.policy_scroll =
4780                        (self.sqs_state.policy_scroll + 1).min(max_scroll);
4781                } else if self.current_service == Service::LambdaFunctions
4782                    && self.lambda_state.current_function.is_some()
4783                    && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
4784                    && !self.lambda_state.is_metrics_loading()
4785                {
4786                    self.lambda_state
4787                        .set_monitoring_scroll((self.lambda_state.monitoring_scroll() + 1).min(9));
4788                } else if self.current_service == Service::Ec2Instances
4789                    && self.ec2_state.current_instance.is_some()
4790                    && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
4791                    && !self.ec2_state.is_metrics_loading()
4792                {
4793                    self.ec2_state
4794                        .set_monitoring_scroll((self.ec2_state.monitoring_scroll() + 1).min(5));
4795                } else if self.current_service == Service::SqsQueues
4796                    && self.sqs_state.current_queue.is_some()
4797                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
4798                    && !self.sqs_state.is_metrics_loading()
4799                {
4800                    self.sqs_state
4801                        .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
4802                } else if self.view_mode == ViewMode::Events {
4803                    let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
4804                    if self.log_groups_state.event_scroll_offset >= max_scroll {
4805                        // At the end, do nothing
4806                    } else {
4807                        self.log_groups_state.event_scroll_offset =
4808                            (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
4809                    }
4810                } else if self.current_service == Service::CloudWatchLogGroups {
4811                    if self.view_mode == ViewMode::List {
4812                        let filtered_groups = filtered_log_groups(self);
4813                        self.log_groups_state
4814                            .log_groups
4815                            .next_item(filtered_groups.len());
4816                    } else if self.view_mode == ViewMode::Detail {
4817                        let filtered_streams = filtered_log_streams(self);
4818                        if !filtered_streams.is_empty() {
4819                            let max = filtered_streams.len() - 1;
4820                            if self.log_groups_state.selected_stream >= max {
4821                                // At the end, do nothing
4822                            } else {
4823                                self.log_groups_state.selected_stream =
4824                                    (self.log_groups_state.selected_stream + 1).min(max);
4825                                self.log_groups_state.expanded_stream = None;
4826                            }
4827                        }
4828                    }
4829                } else if self.current_service == Service::CloudWatchAlarms {
4830                    let filtered_alarms = match self.alarms_state.alarm_tab {
4831                        AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
4832                        AlarmTab::InAlarm => self
4833                            .alarms_state
4834                            .table
4835                            .items
4836                            .iter()
4837                            .filter(|a| a.state.to_uppercase() == "ALARM")
4838                            .count(),
4839                    };
4840                    if filtered_alarms > 0 {
4841                        self.alarms_state.table.next_item(filtered_alarms);
4842                    }
4843                } else if self.current_service == Service::CloudTrailEvents {
4844                    if self.cloudtrail_state.current_event.is_some()
4845                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
4846                    {
4847                        // Scroll JSON down
4848                        if let Some(event) = &self.cloudtrail_state.current_event {
4849                            let line_count = event.cloud_trail_event_json.lines().count();
4850                            let max_scroll = line_count.saturating_sub(1);
4851                            self.cloudtrail_state.event_json_scroll =
4852                                (self.cloudtrail_state.event_json_scroll + 1).min(max_scroll);
4853                        }
4854                    } else {
4855                        // For CloudTrail with infinite pagination, navigate within current page
4856                        let page_size = self.cloudtrail_state.table.page_size.value();
4857                        let current_page = self.cloudtrail_state.table.selected / page_size;
4858                        let page_start = current_page * page_size;
4859                        let page_end = page_start + page_size;
4860                        let filtered_count = self.cloudtrail_state.table.items.len();
4861
4862                        // Only navigate if within loaded items
4863                        if self.cloudtrail_state.table.selected < filtered_count.saturating_sub(1) {
4864                            self.cloudtrail_state.table.selected =
4865                                (self.cloudtrail_state.table.selected + 1)
4866                                    .min(page_end - 1)
4867                                    .min(filtered_count - 1);
4868                        }
4869                    }
4870                } else if self.current_service == Service::Ec2Instances {
4871                    if self.ec2_state.current_instance.is_some()
4872                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
4873                    {
4874                        let filtered = crate::ui::ec2::filtered_tags(self);
4875                        if !filtered.is_empty() {
4876                            self.ec2_state.tags.next_item(filtered.len());
4877                        }
4878                    } else {
4879                        let filtered: Vec<_> = self
4880                            .ec2_state
4881                            .table
4882                            .items
4883                            .iter()
4884                            .filter(|i| self.ec2_state.state_filter.matches(&i.state))
4885                            .filter(|i| {
4886                                if self.ec2_state.table.filter.is_empty() {
4887                                    return true;
4888                                }
4889                                i.name.contains(&self.ec2_state.table.filter)
4890                                    || i.instance_id.contains(&self.ec2_state.table.filter)
4891                                    || i.state.contains(&self.ec2_state.table.filter)
4892                                    || i.instance_type.contains(&self.ec2_state.table.filter)
4893                                    || i.availability_zone.contains(&self.ec2_state.table.filter)
4894                                    || i.security_groups.contains(&self.ec2_state.table.filter)
4895                                    || i.key_name.contains(&self.ec2_state.table.filter)
4896                            })
4897                            .collect();
4898                        if !filtered.is_empty() {
4899                            self.ec2_state.table.next_item(filtered.len());
4900                        }
4901                    }
4902                } else if self.current_service == Service::EcrRepositories {
4903                    crate::ecr::actions::next_item(self);
4904                } else if self.current_service == Service::SqsQueues {
4905                    if self.sqs_state.current_queue.is_some()
4906                        && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
4907                    {
4908                        let filtered = filtered_lambda_triggers(self);
4909                        if !filtered.is_empty() {
4910                            self.sqs_state.triggers.next_item(filtered.len());
4911                        }
4912                    } else if self.sqs_state.current_queue.is_some()
4913                        && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
4914                    {
4915                        let filtered = filtered_eventbridge_pipes(self);
4916                        if !filtered.is_empty() {
4917                            self.sqs_state.pipes.next_item(filtered.len());
4918                        }
4919                    } else if self.sqs_state.current_queue.is_some()
4920                        && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
4921                    {
4922                        let filtered = filtered_tags(self);
4923                        if !filtered.is_empty() {
4924                            self.sqs_state.tags.next_item(filtered.len());
4925                        }
4926                    } else if self.sqs_state.current_queue.is_some()
4927                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
4928                    {
4929                        let filtered = filtered_subscriptions(self);
4930                        if !filtered.is_empty() {
4931                            self.sqs_state.subscriptions.next_item(filtered.len());
4932                        }
4933                    } else {
4934                        let filtered_queues = filtered_queues(
4935                            &self.sqs_state.queues.items,
4936                            &self.sqs_state.queues.filter,
4937                        );
4938                        if !filtered_queues.is_empty() {
4939                            self.sqs_state.queues.next_item(filtered_queues.len());
4940                        }
4941                    }
4942                } else if self.current_service == Service::LambdaFunctions {
4943                    crate::lambda::functions::next_item(self);
4944                } else if self.current_service == Service::LambdaApplications {
4945                    crate::lambda::applications::next_item(self);
4946                } else if self.current_service == Service::CloudFormationStacks {
4947                    if self.cfn_state.current_stack.is_some()
4948                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
4949                    {
4950                        let filtered = filtered_parameters(self);
4951                        self.cfn_state.parameters.next_item(filtered.len());
4952                    } else if self.cfn_state.current_stack.is_some()
4953                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
4954                    {
4955                        let filtered = filtered_outputs(self);
4956                        self.cfn_state.outputs.next_item(filtered.len());
4957                    } else if self.cfn_state.current_stack.is_some()
4958                        && self.cfn_state.detail_tab == CfnDetailTab::Resources
4959                    {
4960                        let filtered = filtered_resources(self);
4961                        self.cfn_state.resources.next_item(filtered.len());
4962                    } else {
4963                        let filtered = filtered_cloudformation_stacks(self);
4964                        self.cfn_state.table.next_item(filtered.len());
4965                    }
4966                } else if self.current_service == Service::IamUsers {
4967                    if self.iam_state.current_user.is_some() {
4968                        if self.iam_state.user_tab == UserTab::Tags {
4969                            let filtered = filtered_user_tags(self);
4970                            if !filtered.is_empty() {
4971                                self.iam_state.user_tags.next_item(filtered.len());
4972                            }
4973                        } else {
4974                            let filtered = filtered_iam_policies(self);
4975                            if !filtered.is_empty() {
4976                                self.iam_state.policies.next_item(filtered.len());
4977                            }
4978                        }
4979                    } else {
4980                        let filtered = filtered_iam_users(self);
4981                        if !filtered.is_empty() {
4982                            self.iam_state.users.next_item(filtered.len());
4983                        }
4984                    }
4985                } else if self.current_service == Service::IamRoles {
4986                    if self.iam_state.current_role.is_some() {
4987                        if self.iam_state.role_tab == RoleTab::TrustRelationships {
4988                            let lines = self.iam_state.trust_policy_document.lines().count();
4989                            let max_scroll = lines.saturating_sub(1);
4990                            self.iam_state.trust_policy_scroll =
4991                                (self.iam_state.trust_policy_scroll + 1).min(max_scroll);
4992                        } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
4993                            self.iam_state.revoke_sessions_scroll =
4994                                (self.iam_state.revoke_sessions_scroll + 1).min(19);
4995                        } else if self.iam_state.role_tab == RoleTab::Tags {
4996                            let filtered = filtered_iam_tags(self);
4997                            if !filtered.is_empty() {
4998                                self.iam_state.tags.next_item(filtered.len());
4999                            }
5000                        } else if self.iam_state.role_tab == RoleTab::LastAccessed {
5001                            let filtered = filtered_last_accessed(self);
5002                            if !filtered.is_empty() {
5003                                self.iam_state
5004                                    .last_accessed_services
5005                                    .next_item(filtered.len());
5006                            }
5007                        } else {
5008                            let filtered = filtered_iam_policies(self);
5009                            if !filtered.is_empty() {
5010                                self.iam_state.policies.next_item(filtered.len());
5011                            }
5012                        }
5013                    } else {
5014                        let filtered = filtered_iam_roles(self);
5015                        if !filtered.is_empty() {
5016                            self.iam_state.roles.next_item(filtered.len());
5017                        }
5018                    }
5019                } else if self.current_service == Service::IamUserGroups {
5020                    if self.iam_state.current_group.is_some() {
5021                        if self.iam_state.group_tab == GroupTab::Users {
5022                            let filtered: Vec<_> = self
5023                                .iam_state
5024                                .group_users
5025                                .items
5026                                .iter()
5027                                .filter(|u| {
5028                                    if self.iam_state.group_users.filter.is_empty() {
5029                                        true
5030                                    } else {
5031                                        u.user_name.to_lowercase().contains(
5032                                            &self.iam_state.group_users.filter.to_lowercase(),
5033                                        )
5034                                    }
5035                                })
5036                                .collect();
5037                            if !filtered.is_empty() {
5038                                self.iam_state.group_users.next_item(filtered.len());
5039                            }
5040                        } else if self.iam_state.group_tab == GroupTab::Permissions {
5041                            let filtered = filtered_iam_policies(self);
5042                            if !filtered.is_empty() {
5043                                self.iam_state.policies.next_item(filtered.len());
5044                            }
5045                        } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5046                            let filtered = filtered_last_accessed(self);
5047                            if !filtered.is_empty() {
5048                                self.iam_state
5049                                    .last_accessed_services
5050                                    .next_item(filtered.len());
5051                            }
5052                        }
5053                    } else {
5054                        let filtered: Vec<_> = self
5055                            .iam_state
5056                            .groups
5057                            .items
5058                            .iter()
5059                            .filter(|g| {
5060                                if self.iam_state.groups.filter.is_empty() {
5061                                    true
5062                                } else {
5063                                    g.group_name
5064                                        .to_lowercase()
5065                                        .contains(&self.iam_state.groups.filter.to_lowercase())
5066                                }
5067                            })
5068                            .collect();
5069                        if !filtered.is_empty() {
5070                            self.iam_state.groups.next_item(filtered.len());
5071                        }
5072                    }
5073                }
5074            }
5075            _ => {}
5076        }
5077    }
5078
5079    fn prev_item(&mut self) {
5080        match self.mode {
5081            Mode::FilterInput => {
5082                if self.current_service == Service::S3Buckets
5083                    && crate::s3::actions::is_pagination_focused(self)
5084                {
5085                    crate::s3::actions::page_up_filter_input(self);
5086                } else if self.current_service == Service::CloudTrailEvents
5087                    && self.cloudtrail_state.input_focus == InputFocus::Pagination
5088                {
5089                    // Navigate to previous page
5090                    let page_size = self.cloudtrail_state.table.page_size.value();
5091                    let current_page = self.cloudtrail_state.table.selected / page_size;
5092                    if current_page > 0 {
5093                        self.cloudtrail_state.table.selected = (current_page - 1) * page_size;
5094                    }
5095                } else if self.current_service == Service::CloudFormationStacks {
5096                    use crate::ui::cfn::STATUS_FILTER;
5097                    if self.cfn_state.input_focus == STATUS_FILTER {
5098                        self.cfn_state.status_filter = self.cfn_state.status_filter.prev();
5099                        self.cfn_state.table.reset();
5100                    }
5101                } else if self.current_service == Service::IamUsers
5102                    && self.iam_state.current_user.is_some()
5103                {
5104                    use crate::ui::iam::{HISTORY_FILTER, POLICY_TYPE_DROPDOWN};
5105                    if self.iam_state.user_tab == UserTab::Permissions
5106                        && self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN
5107                    {
5108                        self.cycle_policy_type_prev();
5109                    } else if self.iam_state.user_tab == UserTab::LastAccessed
5110                        && self.iam_state.last_accessed_input_focus == HISTORY_FILTER
5111                    {
5112                        self.iam_state.last_accessed_history_filter =
5113                            self.iam_state.last_accessed_history_filter.prev();
5114                        self.iam_state.last_accessed_services.reset();
5115                    }
5116                } else if self.current_service == Service::IamRoles
5117                    && self.iam_state.current_role.is_some()
5118                    && self.iam_state.role_tab == RoleTab::Permissions
5119                {
5120                    use crate::ui::iam::POLICY_TYPE_DROPDOWN;
5121                    if self.iam_state.policy_input_focus == POLICY_TYPE_DROPDOWN {
5122                        self.cycle_policy_type_prev();
5123                    }
5124                } else if self.current_service == Service::Ec2Instances {
5125                    if self.ec2_state.input_focus == EC2_STATE_FILTER {
5126                        self.ec2_state.state_filter = self.ec2_state.state_filter.prev();
5127                        self.ec2_state.table.reset();
5128                    }
5129                } else if self.current_service == Service::SqsQueues {
5130                    use crate::ui::sqs::SUBSCRIPTION_REGION;
5131                    if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
5132                        self.sqs_state.subscription_region_selected = self
5133                            .sqs_state
5134                            .subscription_region_selected
5135                            .saturating_sub(1);
5136                        self.sqs_state.subscriptions.reset();
5137                    }
5138                }
5139            }
5140            Mode::RegionPicker => {
5141                self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
5142            }
5143            Mode::ProfilePicker => {
5144                self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
5145            }
5146            Mode::SessionPicker => {
5147                self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
5148            }
5149            Mode::InsightsInput => {
5150                use crate::app::InsightsFocus;
5151                if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
5152                    && self.insights_state.insights.show_dropdown
5153                    && !self.insights_state.insights.log_group_matches.is_empty()
5154                {
5155                    self.insights_state.insights.dropdown_selected = self
5156                        .insights_state
5157                        .insights
5158                        .dropdown_selected
5159                        .saturating_sub(1);
5160                }
5161            }
5162            Mode::ColumnSelector => {
5163                let mut prev_idx = self.column_selector_index.saturating_sub(1);
5164                // Skip blank row
5165                if self.is_blank_row_index(prev_idx) {
5166                    prev_idx = prev_idx.saturating_sub(1);
5167                }
5168                self.column_selector_index = prev_idx;
5169            }
5170            Mode::ServicePicker => {
5171                self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
5172            }
5173            Mode::TabPicker => {
5174                self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
5175            }
5176            Mode::Normal => {
5177                if !self.service_selected {
5178                    self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
5179                } else if self.current_service == Service::S3Buckets {
5180                    if self.s3_state.current_bucket.is_some() {
5181                        if self.s3_state.object_tab == S3ObjectTab::Properties {
5182                            self.s3_state.properties_scroll =
5183                                self.s3_state.properties_scroll.saturating_sub(1);
5184                        } else {
5185                            self.s3_state.selected_object =
5186                                self.s3_state.selected_object.saturating_sub(1);
5187
5188                            // Adjust scroll offset if selection goes above viewport
5189                            if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5190                                self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5191                            }
5192                        }
5193                    } else {
5194                        self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(1);
5195
5196                        // Adjust scroll offset if selection goes above viewport
5197                        if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5198                            self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5199                        }
5200                    }
5201                } else if self.current_service == Service::ApiGatewayApis {
5202                    if let Some(api) = &self.apig_state.current_api {
5203                        if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
5204                            let protocol = api.protocol_type.to_uppercase();
5205                            if protocol == "REST" {
5206                                // Navigate resources tree
5207                                if self.apig_state.resources.selected > 0 {
5208                                    self.apig_state.resources.selected -= 1;
5209                                }
5210                            } else {
5211                                // Navigate routes tree
5212                                if self.apig_state.routes.selected > 0 {
5213                                    self.apig_state.routes.selected -= 1;
5214                                }
5215                            }
5216                        }
5217                    } else {
5218                        self.apig_state.apis.prev_item();
5219                    }
5220                } else if self.view_mode == ViewMode::InsightsResults {
5221                    if self.insights_state.insights.results_selected > 0 {
5222                        self.insights_state.insights.results_selected -= 1;
5223                    }
5224                } else if self.view_mode == ViewMode::PolicyView {
5225                    self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(1);
5226                } else if self.current_service == Service::CloudFormationStacks
5227                    && self.cfn_state.current_stack.is_some()
5228                    && self.cfn_state.detail_tab == CfnDetailTab::Template
5229                {
5230                    self.cfn_state.template_scroll =
5231                        self.cfn_state.template_scroll.saturating_sub(1);
5232                } else if self.current_service == Service::SqsQueues
5233                    && self.sqs_state.current_queue.is_some()
5234                    && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
5235                {
5236                } else if self.current_service == Service::LambdaFunctions
5237                    && self.lambda_state.current_function.is_some()
5238                    && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
5239                    && !self.lambda_state.is_metrics_loading()
5240                {
5241                    crate::lambda::functions::scroll_up(self);
5242                } else if self.current_service == Service::Ec2Instances
5243                    && self.ec2_state.current_instance.is_some()
5244                    && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
5245                    && !self.ec2_state.is_metrics_loading()
5246                {
5247                    self.ec2_state.set_monitoring_scroll(
5248                        self.ec2_state.monitoring_scroll().saturating_sub(1),
5249                    );
5250                } else if self.current_service == Service::SqsQueues
5251                    && self.sqs_state.current_queue.is_some()
5252                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
5253                    && !self.sqs_state.is_metrics_loading()
5254                {
5255                    self.sqs_state.set_monitoring_scroll(
5256                        self.sqs_state.monitoring_scroll().saturating_sub(1),
5257                    );
5258                } else if self.view_mode == ViewMode::Events {
5259                    if self.log_groups_state.event_scroll_offset == 0 {
5260                        if self.log_groups_state.has_older_events {
5261                            self.log_groups_state.loading = true;
5262                        }
5263                        // Don't move if at position 0
5264                    } else {
5265                        self.log_groups_state.event_scroll_offset =
5266                            self.log_groups_state.event_scroll_offset.saturating_sub(1);
5267                    }
5268                } else if self.current_service == Service::CloudWatchLogGroups {
5269                    if self.view_mode == ViewMode::List {
5270                        self.log_groups_state.log_groups.prev_item();
5271                    } else if self.view_mode == ViewMode::Detail
5272                        && self.log_groups_state.selected_stream > 0
5273                    {
5274                        self.log_groups_state.selected_stream =
5275                            self.log_groups_state.selected_stream.saturating_sub(1);
5276                        self.log_groups_state.expanded_stream = None;
5277                    }
5278                } else if self.current_service == Service::CloudWatchAlarms {
5279                    self.alarms_state.table.prev_item();
5280                } else if self.current_service == Service::CloudTrailEvents {
5281                    if self.cloudtrail_state.current_event.is_some()
5282                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
5283                    {
5284                        // Scroll JSON up
5285                        self.cloudtrail_state.event_json_scroll =
5286                            self.cloudtrail_state.event_json_scroll.saturating_sub(1);
5287                    } else {
5288                        // For CloudTrail with infinite pagination, navigate within current page
5289                        let page_size = self.cloudtrail_state.table.page_size.value();
5290                        let current_page = self.cloudtrail_state.table.selected / page_size;
5291                        let page_start = current_page * page_size;
5292
5293                        // Only navigate if not at page start
5294                        if self.cloudtrail_state.table.selected > page_start {
5295                            self.cloudtrail_state.table.selected -= 1;
5296                        }
5297                    }
5298                } else if self.current_service == Service::Ec2Instances {
5299                    if self.ec2_state.current_instance.is_some()
5300                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
5301                    {
5302                        self.ec2_state.tags.prev_item();
5303                    } else {
5304                        self.ec2_state.table.prev_item();
5305                    }
5306                } else if self.current_service == Service::EcrRepositories {
5307                    crate::ecr::actions::prev_item(self);
5308                } else if self.current_service == Service::SqsQueues {
5309                    if self.sqs_state.current_queue.is_some()
5310                        && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
5311                    {
5312                        self.sqs_state.triggers.prev_item();
5313                    } else if self.sqs_state.current_queue.is_some()
5314                        && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
5315                    {
5316                        self.sqs_state.pipes.prev_item();
5317                    } else if self.sqs_state.current_queue.is_some()
5318                        && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
5319                    {
5320                        self.sqs_state.tags.prev_item();
5321                    } else if self.sqs_state.current_queue.is_some()
5322                        && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
5323                    {
5324                        self.sqs_state.subscriptions.prev_item();
5325                    } else {
5326                        self.sqs_state.queues.prev_item();
5327                    }
5328                } else if self.current_service == Service::LambdaFunctions {
5329                    crate::lambda::functions::prev_item(self);
5330                } else if self.current_service == Service::LambdaApplications {
5331                    crate::lambda::applications::prev_item(self);
5332                } else if self.current_service == Service::CloudFormationStacks {
5333                    if self.cfn_state.current_stack.is_some()
5334                        && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5335                    {
5336                        self.cfn_state.parameters.prev_item();
5337                    } else if self.cfn_state.current_stack.is_some()
5338                        && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5339                    {
5340                        self.cfn_state.outputs.prev_item();
5341                    } else if self.cfn_state.current_stack.is_some()
5342                        && self.cfn_state.detail_tab == CfnDetailTab::Resources
5343                    {
5344                        self.cfn_state.resources.prev_item();
5345                    } else {
5346                        self.cfn_state.table.prev_item();
5347                    }
5348                } else if self.current_service == Service::IamUsers {
5349                    self.iam_state.users.prev_item();
5350                } else if self.current_service == Service::IamRoles {
5351                    if self.iam_state.current_role.is_some() {
5352                        if self.iam_state.role_tab == RoleTab::TrustRelationships {
5353                            self.iam_state.trust_policy_scroll =
5354                                self.iam_state.trust_policy_scroll.saturating_sub(1);
5355                        } else if self.iam_state.role_tab == RoleTab::RevokeSessions {
5356                            self.iam_state.revoke_sessions_scroll =
5357                                self.iam_state.revoke_sessions_scroll.saturating_sub(1);
5358                        } else if self.iam_state.role_tab == RoleTab::Tags {
5359                            self.iam_state.tags.prev_item();
5360                        } else if self.iam_state.role_tab == RoleTab::LastAccessed {
5361                            self.iam_state.last_accessed_services.prev_item();
5362                        } else {
5363                            self.iam_state.policies.prev_item();
5364                        }
5365                    } else {
5366                        self.iam_state.roles.prev_item();
5367                    }
5368                } else if self.current_service == Service::IamUserGroups {
5369                    if self.iam_state.current_group.is_some() {
5370                        if self.iam_state.group_tab == GroupTab::Users {
5371                            self.iam_state.group_users.prev_item();
5372                        } else if self.iam_state.group_tab == GroupTab::Permissions {
5373                            self.iam_state.policies.prev_item();
5374                        } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5375                            self.iam_state.last_accessed_services.prev_item();
5376                        }
5377                    } else {
5378                        self.iam_state.groups.prev_item();
5379                    }
5380                }
5381            }
5382            _ => {}
5383        }
5384    }
5385
5386    fn page_down(&mut self) {
5387        if self.current_service == Service::CloudTrailEvents
5388            && self.cloudtrail_state.current_event.is_some()
5389        {
5390            if let Some(event) = &self.cloudtrail_state.current_event {
5391                let lines = event.cloud_trail_event_json.lines().count();
5392                let max_scroll = lines.saturating_sub(1);
5393                self.cloudtrail_state.event_json_scroll =
5394                    (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
5395            }
5396        } else if self.mode == Mode::ColumnSelector {
5397            let max = self.get_column_selector_max();
5398            let mut next_idx = (self.column_selector_index + 10).min(max);
5399            // Skip blank row if we land on it
5400            if self.is_blank_row_index(next_idx) {
5401                next_idx = (next_idx + 1).min(max);
5402            }
5403            self.column_selector_index = next_idx;
5404        } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
5405            crate::s3::actions::page_down_filter_input(self);
5406        } else if self.mode == Mode::FilterInput
5407            && self.current_service == Service::CloudFormationStacks
5408        {
5409            if self.cfn_state.current_stack.is_some()
5410                && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5411            {
5412                let page_size = self.cfn_state.parameters.page_size.value();
5413                let filtered_count = filtered_parameters(self).len();
5414                self.cfn_state.parameters_input_focus.handle_page_down(
5415                    &mut self.cfn_state.parameters.selected,
5416                    &mut self.cfn_state.parameters.scroll_offset,
5417                    page_size,
5418                    filtered_count,
5419                );
5420            } else if self.cfn_state.current_stack.is_some()
5421                && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5422            {
5423                let page_size = self.cfn_state.outputs.page_size.value();
5424                let filtered_count = filtered_outputs(self).len();
5425                self.cfn_state.outputs_input_focus.handle_page_down(
5426                    &mut self.cfn_state.outputs.selected,
5427                    &mut self.cfn_state.outputs.scroll_offset,
5428                    page_size,
5429                    filtered_count,
5430                );
5431            } else {
5432                use crate::ui::cfn::filtered_cloudformation_stacks;
5433                let page_size = self.cfn_state.table.page_size.value();
5434                let filtered_count = filtered_cloudformation_stacks(self).len();
5435                self.cfn_state.input_focus.handle_page_down(
5436                    &mut self.cfn_state.table.selected,
5437                    &mut self.cfn_state.table.scroll_offset,
5438                    page_size,
5439                    filtered_count,
5440                );
5441            }
5442        } else if self.mode == Mode::FilterInput
5443            && self.current_service == Service::IamRoles
5444            && self.iam_state.current_role.is_none()
5445        {
5446            let page_size = self.iam_state.roles.page_size.value();
5447            let filtered_count = filtered_iam_roles(self).len();
5448            self.iam_state.role_input_focus.handle_page_down(
5449                &mut self.iam_state.roles.selected,
5450                &mut self.iam_state.roles.scroll_offset,
5451                page_size,
5452                filtered_count,
5453            );
5454        } else if self.mode == Mode::FilterInput
5455            && self.current_service == Service::CloudWatchAlarms
5456        {
5457            let page_size = self.alarms_state.table.page_size.value();
5458            let filtered_count = self.alarms_state.table.items.len();
5459            self.alarms_state.input_focus.handle_page_down(
5460                &mut self.alarms_state.table.selected,
5461                &mut self.alarms_state.table.scroll_offset,
5462                page_size,
5463                filtered_count,
5464            );
5465        } else if self.mode == Mode::FilterInput
5466            && self.current_service == Service::CloudTrailEvents
5467        {
5468            let page_size = self.cloudtrail_state.table.page_size.value();
5469            let filtered_count = self.cloudtrail_state.table.items.len();
5470            self.cloudtrail_state.input_focus.handle_page_down(
5471                &mut self.cloudtrail_state.table.selected,
5472                &mut self.cloudtrail_state.table.scroll_offset,
5473                page_size,
5474                filtered_count,
5475            );
5476        } else if self.mode == Mode::FilterInput
5477            && self.current_service == Service::CloudWatchLogGroups
5478        {
5479            if self.view_mode == ViewMode::List {
5480                // Log groups list pagination
5481                let filtered = filtered_log_groups(self);
5482                let page_size = self.log_groups_state.log_groups.page_size.value();
5483                let filtered_count = filtered.len();
5484                self.log_groups_state.input_focus.handle_page_down(
5485                    &mut self.log_groups_state.log_groups.selected,
5486                    &mut self.log_groups_state.log_groups.scroll_offset,
5487                    page_size,
5488                    filtered_count,
5489                );
5490            } else {
5491                // Log streams pagination
5492                let filtered = filtered_log_streams(self);
5493                let page_size = self.log_groups_state.stream_page_size;
5494                let filtered_count = filtered.len();
5495                self.log_groups_state.input_focus.handle_page_down(
5496                    &mut self.log_groups_state.selected_stream,
5497                    &mut self.log_groups_state.stream_current_page,
5498                    page_size,
5499                    filtered_count,
5500                );
5501                self.log_groups_state.expanded_stream = None;
5502            }
5503        } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
5504        {
5505            crate::lambda::functions::page_down_filter_input(self);
5506        } else if self.mode == Mode::FilterInput
5507            && self.current_service == Service::LambdaApplications
5508        {
5509            crate::lambda::applications::page_down_filter_input(self);
5510        } else if self.mode == Mode::FilterInput
5511            && self.current_service == Service::EcrRepositories
5512            && self.ecr_state.current_repository.is_none()
5513        {
5514            crate::ecr::actions::page_down_filter_input(self);
5515        } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
5516            let page_size = self.iam_state.policies.page_size.value();
5517            let filtered_count = filtered_iam_policies(self).len();
5518            self.iam_state.policy_input_focus.handle_page_down(
5519                &mut self.iam_state.policies.selected,
5520                &mut self.iam_state.policies.scroll_offset,
5521                page_size,
5522                filtered_count,
5523            );
5524        } else if self.view_mode == ViewMode::PolicyView {
5525            let lines = self.iam_state.policy_document.lines().count();
5526            let max_scroll = lines.saturating_sub(1);
5527            self.iam_state.policy_scroll = (self.iam_state.policy_scroll + 10).min(max_scroll);
5528        } else if self.current_service == Service::CloudFormationStacks
5529            && self.cfn_state.current_stack.is_some()
5530            && self.cfn_state.detail_tab == CfnDetailTab::Template
5531        {
5532            let lines = self.cfn_state.template_body.lines().count();
5533            let max_scroll = lines.saturating_sub(1);
5534            self.cfn_state.template_scroll = (self.cfn_state.template_scroll + 10).min(max_scroll);
5535        } else if self.current_service == Service::LambdaFunctions
5536            && self.lambda_state.current_function.is_some()
5537            && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
5538            && !self.lambda_state.is_metrics_loading()
5539        {
5540            crate::lambda::functions::scroll_down(self);
5541        } else if self.current_service == Service::Ec2Instances
5542            && self.ec2_state.current_instance.is_some()
5543            && self.ec2_state.detail_tab == Ec2DetailTab::Monitoring
5544            && !self.ec2_state.is_metrics_loading()
5545        {
5546            self.ec2_state
5547                .set_monitoring_scroll((self.ec2_state.monitoring_scroll() + 1).min(5));
5548        } else if self.current_service == Service::SqsQueues
5549            && self.sqs_state.current_queue.is_some()
5550        {
5551            if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
5552                self.sqs_state
5553                    .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(8));
5554            } else {
5555                let lines = self.sqs_state.policy_document.lines().count();
5556                let max_scroll = lines.saturating_sub(1);
5557                self.sqs_state.policy_scroll = (self.sqs_state.policy_scroll + 10).min(max_scroll);
5558            }
5559        } else if self.current_service == Service::IamRoles
5560            && self.iam_state.current_role.is_some()
5561            && self.iam_state.role_tab == RoleTab::TrustRelationships
5562        {
5563            let lines = self.iam_state.trust_policy_document.lines().count();
5564            let max_scroll = lines.saturating_sub(1);
5565            self.iam_state.trust_policy_scroll =
5566                (self.iam_state.trust_policy_scroll + 10).min(max_scroll);
5567        } else if self.current_service == Service::IamRoles
5568            && self.iam_state.current_role.is_some()
5569            && self.iam_state.role_tab == RoleTab::RevokeSessions
5570        {
5571            self.iam_state.revoke_sessions_scroll =
5572                (self.iam_state.revoke_sessions_scroll + 10).min(19);
5573        } else if self.mode == Mode::Normal {
5574            if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
5575            {
5576                let total_rows = self.calculate_total_bucket_rows();
5577                self.s3_state.selected_row = self
5578                    .s3_state
5579                    .selected_row
5580                    .saturating_add(10)
5581                    .min(total_rows.saturating_sub(1));
5582
5583                // Adjust scroll offset if selection goes below viewport
5584                let visible_rows = self.s3_state.bucket_visible_rows.get();
5585                if self.s3_state.selected_row >= self.s3_state.bucket_scroll_offset + visible_rows {
5586                    self.s3_state.bucket_scroll_offset =
5587                        self.s3_state.selected_row - visible_rows + 1;
5588                }
5589            } else if self.current_service == Service::S3Buckets
5590                && self.s3_state.current_bucket.is_some()
5591            {
5592                let total_rows = self.calculate_total_object_rows();
5593                self.s3_state.selected_object = self
5594                    .s3_state
5595                    .selected_object
5596                    .saturating_add(10)
5597                    .min(total_rows.saturating_sub(1));
5598
5599                // Adjust scroll offset if selection goes below viewport
5600                let visible_rows = self.s3_state.object_visible_rows.get();
5601                if self.s3_state.selected_object
5602                    >= self.s3_state.object_scroll_offset + visible_rows
5603                {
5604                    self.s3_state.object_scroll_offset =
5605                        self.s3_state.selected_object - visible_rows + 1;
5606                }
5607            } else if self.current_service == Service::CloudWatchLogGroups
5608                && self.view_mode == ViewMode::List
5609            {
5610                let filtered = filtered_log_groups(self);
5611                self.log_groups_state.log_groups.page_down(filtered.len());
5612            } else if self.current_service == Service::CloudWatchLogGroups
5613                && self.view_mode == ViewMode::Detail
5614            {
5615                let len = filtered_log_streams(self).len();
5616                nav_page_down(&mut self.log_groups_state.selected_stream, len, 10);
5617            } else if self.view_mode == ViewMode::Events {
5618                let max = self.log_groups_state.log_events.len();
5619                nav_page_down(&mut self.log_groups_state.event_scroll_offset, max, 10);
5620            } else if self.view_mode == ViewMode::InsightsResults {
5621                let max = self.insights_state.insights.query_results.len();
5622                nav_page_down(&mut self.insights_state.insights.results_selected, max, 10);
5623            } else if self.current_service == Service::CloudWatchAlarms {
5624                let filtered = match self.alarms_state.alarm_tab {
5625                    AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
5626                    AlarmTab::InAlarm => self
5627                        .alarms_state
5628                        .table
5629                        .items
5630                        .iter()
5631                        .filter(|a| a.state.to_uppercase() == "ALARM")
5632                        .count(),
5633                };
5634                if filtered > 0 {
5635                    self.alarms_state.table.page_down(filtered);
5636                }
5637            } else if self.current_service == Service::CloudTrailEvents {
5638                let filtered_count = self.cloudtrail_state.table.items.len();
5639                if filtered_count > 0 {
5640                    self.cloudtrail_state.table.page_down(filtered_count);
5641                }
5642            } else if self.current_service == Service::Ec2Instances {
5643                let filtered: Vec<_> = self
5644                    .ec2_state
5645                    .table
5646                    .items
5647                    .iter()
5648                    .filter(|i| self.ec2_state.state_filter.matches(&i.state))
5649                    .filter(|i| {
5650                        if self.ec2_state.table.filter.is_empty() {
5651                            return true;
5652                        }
5653                        i.name.contains(&self.ec2_state.table.filter)
5654                            || i.instance_id.contains(&self.ec2_state.table.filter)
5655                            || i.state.contains(&self.ec2_state.table.filter)
5656                            || i.instance_type.contains(&self.ec2_state.table.filter)
5657                            || i.availability_zone.contains(&self.ec2_state.table.filter)
5658                            || i.security_groups.contains(&self.ec2_state.table.filter)
5659                            || i.key_name.contains(&self.ec2_state.table.filter)
5660                    })
5661                    .collect();
5662                if !filtered.is_empty() {
5663                    self.ec2_state.table.page_down(filtered.len());
5664                }
5665            } else if self.current_service == Service::EcrRepositories {
5666                crate::ecr::actions::page_down_normal(self);
5667            } else if self.current_service == Service::SqsQueues {
5668                let filtered =
5669                    filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter);
5670                self.sqs_state.queues.page_down(filtered.len());
5671            } else if self.current_service == Service::LambdaFunctions {
5672                crate::lambda::functions::page_down_normal(self);
5673            } else if self.current_service == Service::LambdaApplications {
5674                crate::lambda::applications::page_down_normal(self);
5675            } else if self.current_service == Service::CloudFormationStacks {
5676                if self.cfn_state.current_stack.is_some()
5677                    && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5678                {
5679                    let filtered = filtered_parameters(self);
5680                    self.cfn_state.parameters.page_down(filtered.len());
5681                } else if self.cfn_state.current_stack.is_some()
5682                    && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5683                {
5684                    let filtered = filtered_outputs(self);
5685                    self.cfn_state.outputs.page_down(filtered.len());
5686                } else {
5687                    let filtered = filtered_cloudformation_stacks(self);
5688                    self.cfn_state.table.page_down(filtered.len());
5689                }
5690            } else if self.current_service == Service::IamUsers {
5691                let len = filtered_iam_users(self).len();
5692                nav_page_down(&mut self.iam_state.users.selected, len, 10);
5693            } else if self.current_service == Service::IamRoles {
5694                if self.iam_state.current_role.is_some() {
5695                    let filtered = filtered_iam_policies(self);
5696                    if !filtered.is_empty() {
5697                        self.iam_state.policies.page_down(filtered.len());
5698                    }
5699                } else {
5700                    let filtered = filtered_iam_roles(self);
5701                    self.iam_state.roles.page_down(filtered.len());
5702                }
5703            } else if self.current_service == Service::IamUserGroups {
5704                if self.iam_state.current_group.is_some() {
5705                    if self.iam_state.group_tab == GroupTab::Users {
5706                        let filtered: Vec<_> = self
5707                            .iam_state
5708                            .group_users
5709                            .items
5710                            .iter()
5711                            .filter(|u| {
5712                                if self.iam_state.group_users.filter.is_empty() {
5713                                    true
5714                                } else {
5715                                    u.user_name
5716                                        .to_lowercase()
5717                                        .contains(&self.iam_state.group_users.filter.to_lowercase())
5718                                }
5719                            })
5720                            .collect();
5721                        if !filtered.is_empty() {
5722                            self.iam_state.group_users.page_down(filtered.len());
5723                        }
5724                    } else if self.iam_state.group_tab == GroupTab::Permissions {
5725                        let filtered = filtered_iam_policies(self);
5726                        if !filtered.is_empty() {
5727                            self.iam_state.policies.page_down(filtered.len());
5728                        }
5729                    } else if self.iam_state.group_tab == GroupTab::AccessAdvisor {
5730                        let filtered = filtered_last_accessed(self);
5731                        if !filtered.is_empty() {
5732                            self.iam_state
5733                                .last_accessed_services
5734                                .page_down(filtered.len());
5735                        }
5736                    }
5737                } else {
5738                    let filtered: Vec<_> = self
5739                        .iam_state
5740                        .groups
5741                        .items
5742                        .iter()
5743                        .filter(|g| {
5744                            if self.iam_state.groups.filter.is_empty() {
5745                                true
5746                            } else {
5747                                g.group_name
5748                                    .to_lowercase()
5749                                    .contains(&self.iam_state.groups.filter.to_lowercase())
5750                            }
5751                        })
5752                        .collect();
5753                    if !filtered.is_empty() {
5754                        self.iam_state.groups.page_down(filtered.len());
5755                    }
5756                }
5757            }
5758        }
5759    }
5760
5761    fn cycle_policy_type_next(&mut self) {
5762        let types = ["All types", "AWS managed", "Customer managed"];
5763        let current_idx = types
5764            .iter()
5765            .position(|&t| t == self.iam_state.policy_type_filter)
5766            .unwrap_or(0);
5767        let next_idx = (current_idx + 1) % types.len();
5768        self.iam_state.policy_type_filter = types[next_idx].to_string();
5769        self.iam_state.policies.reset();
5770    }
5771
5772    fn cycle_policy_type_prev(&mut self) {
5773        let types = ["All types", "AWS managed", "Customer managed"];
5774        let current_idx = types
5775            .iter()
5776            .position(|&t| t == self.iam_state.policy_type_filter)
5777            .unwrap_or(0);
5778        let prev_idx = if current_idx == 0 {
5779            types.len() - 1
5780        } else {
5781            current_idx - 1
5782        };
5783        self.iam_state.policy_type_filter = types[prev_idx].to_string();
5784        self.iam_state.policies.reset();
5785    }
5786
5787    fn page_up(&mut self) {
5788        if self.current_service == Service::CloudTrailEvents
5789            && self.cloudtrail_state.current_event.is_some()
5790        {
5791            self.cloudtrail_state.event_json_scroll =
5792                self.cloudtrail_state.event_json_scroll.saturating_sub(10);
5793        } else if self.mode == Mode::ColumnSelector {
5794            let mut prev_idx = self.column_selector_index.saturating_sub(10);
5795            // Skip blank row if we land on it
5796            if self.is_blank_row_index(prev_idx) {
5797                prev_idx = prev_idx.saturating_sub(1);
5798            }
5799            self.column_selector_index = prev_idx;
5800        } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
5801            crate::s3::actions::page_up_filter_input(self);
5802        } else if self.mode == Mode::FilterInput
5803            && self.current_service == Service::CloudFormationStacks
5804        {
5805            if self.cfn_state.current_stack.is_some()
5806                && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5807            {
5808                let page_size = self.cfn_state.parameters.page_size.value();
5809                self.cfn_state.parameters_input_focus.handle_page_up(
5810                    &mut self.cfn_state.parameters.selected,
5811                    &mut self.cfn_state.parameters.scroll_offset,
5812                    page_size,
5813                );
5814            } else if self.cfn_state.current_stack.is_some()
5815                && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5816            {
5817                let page_size = self.cfn_state.outputs.page_size.value();
5818                self.cfn_state.outputs_input_focus.handle_page_up(
5819                    &mut self.cfn_state.outputs.selected,
5820                    &mut self.cfn_state.outputs.scroll_offset,
5821                    page_size,
5822                );
5823            } else {
5824                let page_size = self.cfn_state.table.page_size.value();
5825                self.cfn_state.input_focus.handle_page_up(
5826                    &mut self.cfn_state.table.selected,
5827                    &mut self.cfn_state.table.scroll_offset,
5828                    page_size,
5829                );
5830            }
5831        } else if self.mode == Mode::FilterInput
5832            && self.current_service == Service::IamRoles
5833            && self.iam_state.current_role.is_none()
5834        {
5835            let page_size = self.iam_state.roles.page_size.value();
5836            self.iam_state.role_input_focus.handle_page_up(
5837                &mut self.iam_state.roles.selected,
5838                &mut self.iam_state.roles.scroll_offset,
5839                page_size,
5840            );
5841        } else if self.mode == Mode::FilterInput
5842            && self.current_service == Service::CloudWatchAlarms
5843        {
5844            let page_size = self.alarms_state.table.page_size.value();
5845            self.alarms_state.input_focus.handle_page_up(
5846                &mut self.alarms_state.table.selected,
5847                &mut self.alarms_state.table.scroll_offset,
5848                page_size,
5849            );
5850        } else if self.mode == Mode::FilterInput
5851            && self.current_service == Service::CloudTrailEvents
5852        {
5853            let page_size = self.cloudtrail_state.table.page_size.value();
5854            self.cloudtrail_state.input_focus.handle_page_up(
5855                &mut self.cloudtrail_state.table.selected,
5856                &mut self.cloudtrail_state.table.scroll_offset,
5857                page_size,
5858            );
5859        } else if self.mode == Mode::FilterInput
5860            && self.current_service == Service::CloudWatchLogGroups
5861        {
5862            if self.view_mode == ViewMode::List {
5863                // Log groups list pagination
5864                let page_size = self.log_groups_state.log_groups.page_size.value();
5865                self.log_groups_state.input_focus.handle_page_up(
5866                    &mut self.log_groups_state.log_groups.selected,
5867                    &mut self.log_groups_state.log_groups.scroll_offset,
5868                    page_size,
5869                );
5870            } else {
5871                // Log streams pagination
5872                let page_size = self.log_groups_state.stream_page_size;
5873                self.log_groups_state.input_focus.handle_page_up(
5874                    &mut self.log_groups_state.selected_stream,
5875                    &mut self.log_groups_state.stream_current_page,
5876                    page_size,
5877                );
5878                self.log_groups_state.expanded_stream = None;
5879            }
5880        } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
5881        {
5882            crate::lambda::functions::page_up_filter_input(self);
5883        } else if self.mode == Mode::FilterInput
5884            && self.current_service == Service::LambdaApplications
5885        {
5886            crate::lambda::applications::page_up_filter_input(self);
5887        } else if self.mode == Mode::FilterInput
5888            && self.current_service == Service::EcrRepositories
5889            && self.ecr_state.current_repository.is_none()
5890        {
5891            crate::ecr::actions::page_up_filter_input(self);
5892        } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
5893            let page_size = self.iam_state.policies.page_size.value();
5894            self.iam_state.policy_input_focus.handle_page_up(
5895                &mut self.iam_state.policies.selected,
5896                &mut self.iam_state.policies.scroll_offset,
5897                page_size,
5898            );
5899        } else if self.view_mode == ViewMode::PolicyView {
5900            self.iam_state.policy_scroll = self.iam_state.policy_scroll.saturating_sub(10);
5901        } else if self.current_service == Service::CloudFormationStacks
5902            && self.cfn_state.current_stack.is_some()
5903            && self.cfn_state.detail_tab == CfnDetailTab::Template
5904        {
5905            self.cfn_state.template_scroll = self.cfn_state.template_scroll.saturating_sub(10);
5906        } else if self.current_service == Service::SqsQueues
5907            && self.sqs_state.current_queue.is_some()
5908        {
5909            if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
5910                self.sqs_state
5911                    .set_monitoring_scroll(self.sqs_state.monitoring_scroll().saturating_sub(1));
5912            } else {
5913                self.sqs_state.policy_scroll = self.sqs_state.policy_scroll.saturating_sub(10);
5914            }
5915        } else if self.current_service == Service::IamRoles
5916            && self.iam_state.current_role.is_some()
5917            && self.iam_state.role_tab == RoleTab::TrustRelationships
5918        {
5919            self.iam_state.trust_policy_scroll =
5920                self.iam_state.trust_policy_scroll.saturating_sub(10);
5921        } else if self.current_service == Service::IamRoles
5922            && self.iam_state.current_role.is_some()
5923            && self.iam_state.role_tab == RoleTab::RevokeSessions
5924        {
5925            self.iam_state.revoke_sessions_scroll =
5926                self.iam_state.revoke_sessions_scroll.saturating_sub(10);
5927        } else if self.mode == Mode::Normal {
5928            if self.current_service == Service::S3Buckets && self.s3_state.current_bucket.is_none()
5929            {
5930                self.s3_state.selected_row = self.s3_state.selected_row.saturating_sub(10);
5931
5932                // Adjust scroll offset if selection goes above viewport
5933                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
5934                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
5935                }
5936            } else if self.current_service == Service::S3Buckets
5937                && self.s3_state.current_bucket.is_some()
5938            {
5939                self.s3_state.selected_object = self.s3_state.selected_object.saturating_sub(10);
5940
5941                // Adjust scroll offset if selection goes above viewport
5942                if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
5943                    self.s3_state.object_scroll_offset = self.s3_state.selected_object;
5944                }
5945            } else if self.current_service == Service::CloudWatchLogGroups
5946                && self.view_mode == ViewMode::List
5947            {
5948                self.log_groups_state.log_groups.page_up();
5949            } else if self.current_service == Service::CloudWatchLogGroups
5950                && self.view_mode == ViewMode::Detail
5951            {
5952                self.log_groups_state.selected_stream =
5953                    self.log_groups_state.selected_stream.saturating_sub(10);
5954            } else if self.view_mode == ViewMode::Events {
5955                if self.log_groups_state.event_scroll_offset < 10
5956                    && self.log_groups_state.has_older_events
5957                {
5958                    self.log_groups_state.loading = true;
5959                }
5960                self.log_groups_state.event_scroll_offset =
5961                    self.log_groups_state.event_scroll_offset.saturating_sub(10);
5962            } else if self.view_mode == ViewMode::InsightsResults {
5963                self.insights_state.insights.results_selected = self
5964                    .insights_state
5965                    .insights
5966                    .results_selected
5967                    .saturating_sub(10);
5968            } else if self.current_service == Service::CloudWatchAlarms {
5969                self.alarms_state.table.page_up();
5970            } else if self.current_service == Service::CloudTrailEvents {
5971                self.cloudtrail_state.table.page_up();
5972            } else if self.current_service == Service::Ec2Instances {
5973                self.ec2_state.table.page_up();
5974            } else if self.current_service == Service::EcrRepositories {
5975                crate::ecr::actions::page_up_normal(self);
5976            } else if self.current_service == Service::SqsQueues {
5977                self.sqs_state.queues.page_up();
5978            } else if self.current_service == Service::LambdaFunctions {
5979                crate::lambda::functions::page_up_normal(self);
5980            } else if self.current_service == Service::LambdaApplications {
5981                crate::lambda::applications::page_up_normal(self);
5982            } else if self.current_service == Service::CloudFormationStacks {
5983                if self.cfn_state.current_stack.is_some()
5984                    && self.cfn_state.detail_tab == CfnDetailTab::Parameters
5985                {
5986                    self.cfn_state.parameters.page_up();
5987                } else if self.cfn_state.current_stack.is_some()
5988                    && self.cfn_state.detail_tab == CfnDetailTab::Outputs
5989                {
5990                    self.cfn_state.outputs.page_up();
5991                } else {
5992                    self.cfn_state.table.page_up();
5993                }
5994            } else if self.current_service == Service::IamUsers {
5995                self.iam_state.users.page_up();
5996            } else if self.current_service == Service::IamRoles {
5997                if self.iam_state.current_role.is_some() {
5998                    self.iam_state.policies.page_up();
5999                } else {
6000                    self.iam_state.roles.page_up();
6001                }
6002            }
6003        }
6004    }
6005
6006    fn next_pane(&mut self) {
6007        if self.current_service == Service::S3Buckets {
6008            if self.s3_state.current_bucket.is_some() {
6009                // In objects view - expand prefix and trigger preview load
6010                // Map visual index to actual object (including nested items)
6011                let mut visual_idx = 0;
6012                let mut found_obj: Option<S3Object> = None;
6013
6014                // Helper to recursively check nested items
6015                fn check_nested(
6016                    obj: &S3Object,
6017                    visual_idx: &mut usize,
6018                    target_idx: usize,
6019                    expanded_prefixes: &std::collections::HashSet<String>,
6020                    prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6021                    found_obj: &mut Option<S3Object>,
6022                ) {
6023                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6024                        if let Some(preview) = prefix_preview.get(&obj.key) {
6025                            for nested_obj in preview {
6026                                if *visual_idx == target_idx {
6027                                    *found_obj = Some(nested_obj.clone());
6028                                    return;
6029                                }
6030                                *visual_idx += 1;
6031
6032                                // Recursively check deeper levels
6033                                check_nested(
6034                                    nested_obj,
6035                                    visual_idx,
6036                                    target_idx,
6037                                    expanded_prefixes,
6038                                    prefix_preview,
6039                                    found_obj,
6040                                );
6041                                if found_obj.is_some() {
6042                                    return;
6043                                }
6044                            }
6045                        } else {
6046                            // Loading row
6047                            *visual_idx += 1;
6048                        }
6049                    }
6050                }
6051
6052                for obj in &self.s3_state.objects {
6053                    if visual_idx == self.s3_state.selected_object {
6054                        found_obj = Some(obj.clone());
6055                        break;
6056                    }
6057                    visual_idx += 1;
6058
6059                    // Check nested items recursively
6060                    check_nested(
6061                        obj,
6062                        &mut visual_idx,
6063                        self.s3_state.selected_object,
6064                        &self.s3_state.expanded_prefixes,
6065                        &self.s3_state.prefix_preview,
6066                        &mut found_obj,
6067                    );
6068                    if found_obj.is_some() {
6069                        break;
6070                    }
6071                }
6072
6073                if let Some(obj) = found_obj {
6074                    if obj.is_prefix {
6075                        if !self.s3_state.expanded_prefixes.contains(&obj.key) {
6076                            self.s3_state.expanded_prefixes.insert(obj.key.clone());
6077                            // Trigger preview load if not already cached
6078                            if !self.s3_state.prefix_preview.contains_key(&obj.key) {
6079                                self.s3_state.buckets.loading = true;
6080                            }
6081                        }
6082                        // Move to first child if expanded and has children
6083                        if self.s3_state.expanded_prefixes.contains(&obj.key) {
6084                            if let Some(preview) = self.s3_state.prefix_preview.get(&obj.key) {
6085                                if !preview.is_empty() {
6086                                    self.s3_state.selected_object += 1;
6087                                }
6088                            }
6089                        }
6090                    }
6091                }
6092            } else {
6093                // In bucket list - find which bucket/prefix the selected row corresponds to
6094                let mut row_idx = 0;
6095                let mut found = false;
6096                for bucket in &self.s3_state.buckets.items {
6097                    if row_idx == self.s3_state.selected_row {
6098                        // Selected row is a bucket - expand and move to first child
6099                        if !self.s3_state.expanded_prefixes.contains(&bucket.name) {
6100                            self.s3_state.expanded_prefixes.insert(bucket.name.clone());
6101                            if !self.s3_state.bucket_preview.contains_key(&bucket.name)
6102                                && !self.s3_state.bucket_errors.contains_key(&bucket.name)
6103                            {
6104                                self.s3_state.buckets.loading = true;
6105                            }
6106                        }
6107                        // Move to first child if expanded and has children
6108                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6109                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6110                                if !preview.is_empty() {
6111                                    self.s3_state.selected_row = row_idx + 1;
6112                                    // Adjust scroll so new selection stays visible
6113                                    let visible = self.s3_state.bucket_visible_rows.get();
6114                                    if self.s3_state.selected_row
6115                                        >= self.s3_state.bucket_scroll_offset + visible
6116                                    {
6117                                        self.s3_state.bucket_scroll_offset =
6118                                            self.s3_state.selected_row.saturating_sub(visible - 1);
6119                                    }
6120                                }
6121                            }
6122                        }
6123                        break;
6124                    }
6125                    row_idx += 1;
6126
6127                    // Count error rows in traversal so row indices match the render
6128                    if self.s3_state.bucket_errors.contains_key(&bucket.name)
6129                        && self.s3_state.expanded_prefixes.contains(&bucket.name)
6130                    {
6131                        if let Some(err) = self.s3_state.bucket_errors.get(&bucket.name) {
6132                            let max_width = 120;
6133                            let error_rows = if err.len() > max_width {
6134                                err.len().div_ceil(max_width)
6135                            } else {
6136                                1
6137                            };
6138                            row_idx += error_rows;
6139                        }
6140                        continue;
6141                    }
6142
6143                    if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6144                        if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6145                            // Recursive function to check nested items at any depth
6146                            #[allow(clippy::too_many_arguments)]
6147                            fn check_nested_expansion(
6148                                objects: &[S3Object],
6149                                row_idx: &mut usize,
6150                                target_row: usize,
6151                                expanded_prefixes: &mut std::collections::HashSet<String>,
6152                                prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6153                                found: &mut bool,
6154                                loading: &mut bool,
6155                                selected_row: &mut usize,
6156                            ) {
6157                                for obj in objects {
6158                                    if *row_idx == target_row {
6159                                        // Selected this item - expand and move to first child
6160                                        if obj.is_prefix {
6161                                            if !expanded_prefixes.contains(&obj.key) {
6162                                                expanded_prefixes.insert(obj.key.clone());
6163                                                if !prefix_preview.contains_key(&obj.key) {
6164                                                    *loading = true;
6165                                                }
6166                                            }
6167                                            // Move to first child if expanded and has children
6168                                            if expanded_prefixes.contains(&obj.key) {
6169                                                if let Some(preview) = prefix_preview.get(&obj.key)
6170                                                {
6171                                                    if !preview.is_empty() {
6172                                                        *selected_row = *row_idx + 1;
6173                                                    }
6174                                                }
6175                                            }
6176                                        }
6177                                        *found = true;
6178                                        return;
6179                                    }
6180                                    *row_idx += 1;
6181
6182                                    // Recursively check nested items if expanded
6183                                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6184                                        if let Some(nested) = prefix_preview.get(&obj.key) {
6185                                            check_nested_expansion(
6186                                                nested,
6187                                                row_idx,
6188                                                target_row,
6189                                                expanded_prefixes,
6190                                                prefix_preview,
6191                                                found,
6192                                                loading,
6193                                                selected_row,
6194                                            );
6195                                            if *found {
6196                                                return;
6197                                            }
6198                                        } else {
6199                                            *row_idx += 1; // Loading row
6200                                        }
6201                                    }
6202                                }
6203                            }
6204
6205                            check_nested_expansion(
6206                                preview,
6207                                &mut row_idx,
6208                                self.s3_state.selected_row,
6209                                &mut self.s3_state.expanded_prefixes,
6210                                &self.s3_state.prefix_preview,
6211                                &mut found,
6212                                &mut self.s3_state.buckets.loading,
6213                                &mut self.s3_state.selected_row,
6214                            );
6215                            if found || row_idx > self.s3_state.selected_row {
6216                                break;
6217                            }
6218                        } else {
6219                            // Preview not loaded yet — this bucket has no visible child rows,
6220                            // so row_idx must not advance past the bucket itself.
6221                            if row_idx > self.s3_state.selected_row {
6222                                break;
6223                            }
6224                        }
6225                    }
6226                    if found {
6227                        break;
6228                    }
6229                }
6230            }
6231        } else if self.view_mode == ViewMode::InsightsResults {
6232            // Right arrow scrolls horizontally by 1 column
6233            let max_cols = self
6234                .insights_state
6235                .insights
6236                .query_results
6237                .first()
6238                .map(|r| r.len())
6239                .unwrap_or(0);
6240            if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
6241                self.insights_state.insights.results_horizontal_scroll += 1;
6242            }
6243        } else if self.current_service == Service::CloudWatchLogGroups
6244            && self.view_mode == ViewMode::List
6245        {
6246            // Expand selected log group
6247            if self.log_groups_state.log_groups.expanded_item
6248                != Some(self.log_groups_state.log_groups.selected)
6249            {
6250                self.log_groups_state.log_groups.expanded_item =
6251                    Some(self.log_groups_state.log_groups.selected);
6252            }
6253        } else if self.current_service == Service::CloudWatchLogGroups
6254            && self.view_mode == ViewMode::Detail
6255        {
6256            // Expand selected log stream
6257            if self.log_groups_state.expanded_stream != Some(self.log_groups_state.selected_stream)
6258            {
6259                self.log_groups_state.expanded_stream = Some(self.log_groups_state.selected_stream);
6260            }
6261        } else if self.view_mode == ViewMode::Events {
6262            // Only scroll if there are hidden columns
6263            // Expand selected event
6264            if self.log_groups_state.expanded_event
6265                != Some(self.log_groups_state.event_scroll_offset)
6266            {
6267                self.log_groups_state.expanded_event =
6268                    Some(self.log_groups_state.event_scroll_offset);
6269            }
6270        } else if self.current_service == Service::CloudWatchAlarms {
6271            // Right arrow expands alarm row
6272            if !self.alarms_state.table.is_expanded() {
6273                self.alarms_state.table.toggle_expand();
6274            }
6275        } else if self.current_service == Service::Ec2Instances {
6276            if self.ec2_state.current_instance.is_some()
6277                && self.ec2_state.detail_tab == Ec2DetailTab::Tags
6278            {
6279                self.ec2_state.tags.toggle_expand();
6280            } else if !self.ec2_state.table.is_expanded() {
6281                self.ec2_state.table.toggle_expand();
6282            }
6283        } else if self.current_service == Service::EcrRepositories {
6284            crate::ecr::actions::next_pane(self);
6285        } else if self.current_service == Service::SqsQueues {
6286            if self.sqs_state.current_queue.is_some()
6287                && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
6288            {
6289                self.sqs_state.triggers.toggle_expand();
6290            } else if self.sqs_state.current_queue.is_some()
6291                && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
6292            {
6293                self.sqs_state.pipes.toggle_expand();
6294            } else if self.sqs_state.current_queue.is_some()
6295                && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
6296            {
6297                self.sqs_state.tags.toggle_expand();
6298            } else if self.sqs_state.current_queue.is_some()
6299                && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
6300            {
6301                self.sqs_state.subscriptions.toggle_expand();
6302            } else {
6303                self.sqs_state.queues.expand();
6304            }
6305        } else if self.current_service == Service::LambdaFunctions {
6306            crate::lambda::functions::expand_row(self);
6307        } else if self.current_service == Service::LambdaApplications {
6308            crate::lambda::applications::expand_row(self);
6309        } else if self.current_service == Service::CloudFormationStacks
6310            && self.cfn_state.current_stack.is_none()
6311        {
6312            self.cfn_state.table.toggle_expand();
6313        } else if self.current_service == Service::CloudFormationStacks
6314            && self.cfn_state.current_stack.is_some()
6315            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6316        {
6317            self.cfn_state.parameters.toggle_expand();
6318        } else if self.current_service == Service::CloudFormationStacks
6319            && self.cfn_state.current_stack.is_some()
6320            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6321        {
6322            self.cfn_state.outputs.toggle_expand();
6323        } else if self.current_service == Service::CloudFormationStacks
6324            && self.cfn_state.current_stack.is_some()
6325            && self.cfn_state.detail_tab == CfnDetailTab::Resources
6326        {
6327            self.cfn_state.resources.toggle_expand();
6328        } else if self.current_service == Service::IamUsers {
6329            if self.iam_state.current_user.is_some() {
6330                if self.iam_state.user_tab == UserTab::Tags {
6331                    if self.iam_state.user_tags.expanded_item
6332                        != Some(self.iam_state.user_tags.selected)
6333                    {
6334                        self.iam_state.user_tags.expanded_item =
6335                            Some(self.iam_state.user_tags.selected);
6336                    }
6337                } else if self.iam_state.policies.expanded_item
6338                    != Some(self.iam_state.policies.selected)
6339                {
6340                    self.iam_state.policies.toggle_expand();
6341                }
6342            } else if !self.iam_state.users.is_expanded() {
6343                self.iam_state.users.toggle_expand();
6344            }
6345        } else if self.current_service == Service::IamRoles {
6346            if self.iam_state.current_role.is_some() {
6347                // Handle expansion based on current tab
6348                if self.iam_state.role_tab == RoleTab::Tags {
6349                    if !self.iam_state.tags.is_expanded() {
6350                        self.iam_state.tags.expand();
6351                    }
6352                } else if self.iam_state.role_tab == RoleTab::LastAccessed {
6353                    if !self.iam_state.last_accessed_services.is_expanded() {
6354                        self.iam_state.last_accessed_services.expand();
6355                    }
6356                } else if !self.iam_state.policies.is_expanded() {
6357                    self.iam_state.policies.expand();
6358                }
6359            } else if !self.iam_state.roles.is_expanded() {
6360                self.iam_state.roles.expand();
6361            }
6362        } else if self.current_service == Service::IamUserGroups {
6363            if self.iam_state.current_group.is_some() {
6364                if self.iam_state.group_tab == GroupTab::Users {
6365                    if !self.iam_state.group_users.is_expanded() {
6366                        self.iam_state.group_users.expand();
6367                    }
6368                } else if self.iam_state.group_tab == GroupTab::Permissions {
6369                    if !self.iam_state.policies.is_expanded() {
6370                        self.iam_state.policies.expand();
6371                    }
6372                } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
6373                    && !self.iam_state.last_accessed_services.is_expanded()
6374                {
6375                    self.iam_state.last_accessed_services.expand();
6376                }
6377            } else if !self.iam_state.groups.is_expanded() {
6378                self.iam_state.groups.expand();
6379            }
6380        }
6381    }
6382
6383    fn go_to_page(&mut self, page: usize) {
6384        if page == 0 {
6385            return;
6386        }
6387
6388        match self.current_service {
6389            Service::CloudWatchAlarms => {
6390                let alarm_page_size = self.alarms_state.table.page_size.value();
6391                let target = (page - 1) * alarm_page_size;
6392                let filtered_count = match self.alarms_state.alarm_tab {
6393                    AlarmTab::AllAlarms => self.alarms_state.table.items.len(),
6394                    AlarmTab::InAlarm => self
6395                        .alarms_state
6396                        .table
6397                        .items
6398                        .iter()
6399                        .filter(|a| a.state.to_uppercase() == "ALARM")
6400                        .count(),
6401                };
6402                let max_offset = filtered_count.saturating_sub(alarm_page_size);
6403                self.alarms_state.table.scroll_offset = target.min(max_offset);
6404                self.alarms_state.table.selected = self
6405                    .alarms_state
6406                    .table
6407                    .scroll_offset
6408                    .min(filtered_count.saturating_sub(1));
6409            }
6410            Service::CloudTrailEvents => {
6411                let page_size = self.cloudtrail_state.table.page_size.value();
6412                let filtered_count = self.cloudtrail_state.table.items.len();
6413                let max_page = (filtered_count / page_size) + 1; // Allow one page beyond loaded
6414
6415                // Only navigate if page is within valid range
6416                if page <= max_page {
6417                    let target = (page - 1) * page_size;
6418                    self.cloudtrail_state.table.scroll_offset = target;
6419                    self.cloudtrail_state.table.selected = target;
6420                    self.cloudtrail_state.table.expanded_item = None; // Reset expansion
6421                }
6422                // Otherwise do nothing (ignore invalid page numbers)
6423            }
6424            Service::CloudWatchLogGroups => match self.view_mode {
6425                ViewMode::Events => {
6426                    let page_size = 20;
6427                    let target = (page - 1) * page_size;
6428                    let max = self.log_groups_state.log_events.len().saturating_sub(1);
6429                    self.log_groups_state.event_scroll_offset = target.min(max);
6430                }
6431                ViewMode::Detail => {
6432                    let page_size = self.log_groups_state.stream_page_size;
6433                    self.log_groups_state.stream_current_page = (page - 1).min(
6434                        self.log_groups_state
6435                            .log_streams
6436                            .len()
6437                            .div_ceil(page_size)
6438                            .saturating_sub(1),
6439                    );
6440                    self.log_groups_state.selected_stream = 0;
6441                }
6442                ViewMode::List => {
6443                    let total = self.log_groups_state.log_groups.items.len();
6444                    self.log_groups_state.log_groups.goto_page(page, total);
6445                }
6446                _ => {}
6447            },
6448            Service::EcrRepositories => {
6449                crate::ecr::actions::go_to_page(self, page);
6450            }
6451            Service::SqsQueues => {
6452                let filtered_count =
6453                    filtered_queues(&self.sqs_state.queues.items, &self.sqs_state.queues.filter)
6454                        .len();
6455                self.sqs_state.queues.goto_page(page, filtered_count);
6456            }
6457            Service::S3Buckets => {
6458                if self.s3_state.current_bucket.is_some() {
6459                    let page_size = 50; // S3 objects use fixed page size
6460                    let target = (page - 1) * page_size;
6461                    let total_rows = self.calculate_total_object_rows();
6462                    let max = total_rows.saturating_sub(1);
6463                    self.s3_state.selected_object = target.min(max);
6464                } else {
6465                    let page_size = self.s3_state.buckets.page_size.value();
6466                    let target = (page - 1) * page_size;
6467                    let total_rows = self.calculate_total_bucket_rows();
6468                    let max = total_rows.saturating_sub(1);
6469                    self.s3_state.selected_row = target.min(max);
6470                    // Adjust scroll offset to show the target page
6471                    self.s3_state.bucket_scroll_offset =
6472                        target.min(total_rows.saturating_sub(page_size));
6473                }
6474            }
6475            Service::LambdaFunctions => {
6476                crate::lambda::functions::go_to_page(self, page);
6477            }
6478            Service::LambdaApplications => {
6479                crate::lambda::applications::go_to_page(self, page);
6480            }
6481            Service::CloudFormationStacks => {
6482                let filtered_count = filtered_cloudformation_stacks(self).len();
6483                self.cfn_state.table.goto_page(page, filtered_count);
6484            }
6485            Service::IamUsers => {
6486                let filtered_count = filtered_iam_users(self).len();
6487                self.iam_state.users.goto_page(page, filtered_count);
6488            }
6489            Service::IamRoles => {
6490                let filtered_count = filtered_iam_roles(self).len();
6491                self.iam_state.roles.goto_page(page, filtered_count);
6492            }
6493            _ => {}
6494        }
6495    }
6496
6497    fn prev_pane(&mut self) {
6498        if self.current_service == Service::S3Buckets {
6499            if self.s3_state.current_bucket.is_some() {
6500                // In objects view - collapse prefix or jump to parent
6501                // Map visual index to actual object (including nested items)
6502                let mut visual_idx = 0;
6503                let mut found_obj: Option<S3Object> = None;
6504                let mut parent_idx: Option<usize> = None;
6505
6506                // Helper to recursively find object and its parent
6507                #[allow(clippy::too_many_arguments)]
6508                fn find_with_parent(
6509                    objects: &[S3Object],
6510                    visual_idx: &mut usize,
6511                    target_idx: usize,
6512                    expanded_prefixes: &std::collections::HashSet<String>,
6513                    prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6514                    found_obj: &mut Option<S3Object>,
6515                    parent_idx: &mut Option<usize>,
6516                    current_parent: Option<usize>,
6517                ) {
6518                    for obj in objects {
6519                        if *visual_idx == target_idx {
6520                            *found_obj = Some(obj.clone());
6521                            *parent_idx = current_parent;
6522                            return;
6523                        }
6524                        let obj_idx = *visual_idx;
6525                        *visual_idx += 1;
6526
6527                        // Check nested items if expanded
6528                        if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6529                            if let Some(preview) = prefix_preview.get(&obj.key) {
6530                                find_with_parent(
6531                                    preview,
6532                                    visual_idx,
6533                                    target_idx,
6534                                    expanded_prefixes,
6535                                    prefix_preview,
6536                                    found_obj,
6537                                    parent_idx,
6538                                    Some(obj_idx),
6539                                );
6540                                if found_obj.is_some() {
6541                                    return;
6542                                }
6543                            }
6544                        }
6545                    }
6546                }
6547
6548                find_with_parent(
6549                    &self.s3_state.objects,
6550                    &mut visual_idx,
6551                    self.s3_state.selected_object,
6552                    &self.s3_state.expanded_prefixes,
6553                    &self.s3_state.prefix_preview,
6554                    &mut found_obj,
6555                    &mut parent_idx,
6556                    None,
6557                );
6558
6559                if let Some(obj) = found_obj {
6560                    if obj.is_prefix && self.s3_state.expanded_prefixes.contains(&obj.key) {
6561                        // Expanded: collapse it and jump to parent
6562                        self.s3_state.expanded_prefixes.remove(&obj.key);
6563                        if let Some(parent) = parent_idx {
6564                            self.s3_state.selected_object = parent;
6565                        }
6566                    } else if let Some(parent) = parent_idx {
6567                        // Already collapsed or not a prefix: jump to parent
6568                        self.s3_state.selected_object = parent;
6569                    }
6570                }
6571
6572                // Adjust scroll offset to keep selection visible
6573                let visible_rows = self.s3_state.object_visible_rows.get();
6574                if self.s3_state.selected_object < self.s3_state.object_scroll_offset {
6575                    self.s3_state.object_scroll_offset = self.s3_state.selected_object;
6576                } else if self.s3_state.selected_object
6577                    >= self.s3_state.object_scroll_offset + visible_rows
6578                {
6579                    self.s3_state.object_scroll_offset = self
6580                        .s3_state
6581                        .selected_object
6582                        .saturating_sub(visible_rows - 1);
6583                }
6584            } else {
6585                // In bucket list - find which bucket/prefix the selected row corresponds to
6586                let mut row_idx = 0;
6587                for bucket in &self.s3_state.buckets.items {
6588                    if row_idx == self.s3_state.selected_row {
6589                        // Selected row is a bucket - collapse it
6590                        self.s3_state.expanded_prefixes.remove(&bucket.name);
6591                        break;
6592                    }
6593                    row_idx += 1;
6594                    if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6595                        if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6596                            // Recursive function to check nested items at any depth
6597                            #[allow(clippy::too_many_arguments)]
6598                            fn check_nested_collapse(
6599                                objects: &[S3Object],
6600                                row_idx: &mut usize,
6601                                target_row: usize,
6602                                expanded_prefixes: &mut std::collections::HashSet<String>,
6603                                prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
6604                                found: &mut bool,
6605                                selected_row: &mut usize,
6606                                parent_row: usize,
6607                            ) {
6608                                for obj in objects {
6609                                    let current_row = *row_idx;
6610                                    if *row_idx == target_row {
6611                                        // Selected this item - collapse or jump to parent
6612                                        if obj.is_prefix {
6613                                            if expanded_prefixes.contains(&obj.key) {
6614                                                // Expanded: collapse it and move to parent
6615                                                expanded_prefixes.remove(&obj.key);
6616                                                *selected_row = parent_row;
6617                                            } else {
6618                                                // Already collapsed: jump to parent
6619                                                *selected_row = parent_row;
6620                                            }
6621                                        } else {
6622                                            // Not a prefix: jump to parent
6623                                            *selected_row = parent_row;
6624                                        }
6625                                        *found = true;
6626                                        return;
6627                                    }
6628                                    *row_idx += 1;
6629
6630                                    // Recursively check nested items if expanded
6631                                    if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6632                                        if let Some(nested) = prefix_preview.get(&obj.key) {
6633                                            check_nested_collapse(
6634                                                nested,
6635                                                row_idx,
6636                                                target_row,
6637                                                expanded_prefixes,
6638                                                prefix_preview,
6639                                                found,
6640                                                selected_row,
6641                                                current_row,
6642                                            );
6643                                            if *found {
6644                                                return;
6645                                            }
6646                                        } else {
6647                                            *row_idx += 1; // Loading row
6648                                        }
6649                                    }
6650                                }
6651                            }
6652
6653                            let mut found = false;
6654                            let parent_row = row_idx - 1; // Parent is the bucket
6655                            check_nested_collapse(
6656                                preview,
6657                                &mut row_idx,
6658                                self.s3_state.selected_row,
6659                                &mut self.s3_state.expanded_prefixes,
6660                                &self.s3_state.prefix_preview,
6661                                &mut found,
6662                                &mut self.s3_state.selected_row,
6663                                parent_row,
6664                            );
6665                            if found {
6666                                // Adjust scroll offset to keep selection visible
6667                                let visible_rows = self.s3_state.bucket_visible_rows.get();
6668                                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6669                                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6670                                } else if self.s3_state.selected_row
6671                                    >= self.s3_state.bucket_scroll_offset + visible_rows
6672                                {
6673                                    self.s3_state.bucket_scroll_offset =
6674                                        self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6675                                }
6676                                return;
6677                            }
6678                        } else {
6679                            row_idx += 1;
6680                        }
6681                    }
6682                }
6683
6684                // Adjust scroll offset to keep selection visible after collapse
6685                let visible_rows = self.s3_state.bucket_visible_rows.get();
6686                if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6687                    self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6688                } else if self.s3_state.selected_row
6689                    >= self.s3_state.bucket_scroll_offset + visible_rows
6690                {
6691                    self.s3_state.bucket_scroll_offset =
6692                        self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6693                }
6694            }
6695        } else if self.view_mode == ViewMode::InsightsResults {
6696            // Left arrow scrolls horizontally by 1 column
6697            self.insights_state.insights.results_horizontal_scroll = self
6698                .insights_state
6699                .insights
6700                .results_horizontal_scroll
6701                .saturating_sub(1);
6702        } else if self.current_service == Service::CloudWatchLogGroups
6703            && self.view_mode == ViewMode::List
6704        {
6705            // Collapse expanded log group
6706            if self.log_groups_state.log_groups.has_expanded_item() {
6707                self.log_groups_state.log_groups.collapse();
6708            }
6709        } else if self.current_service == Service::CloudWatchLogGroups
6710            && self.view_mode == ViewMode::Detail
6711        {
6712            // Collapse expanded log stream
6713            if self.log_groups_state.expanded_stream.is_some() {
6714                self.log_groups_state.expanded_stream = None;
6715            }
6716        } else if self.view_mode == ViewMode::Events {
6717            // Collapse expanded event
6718            if self.log_groups_state.expanded_event.is_some() {
6719                self.log_groups_state.expanded_event = None;
6720            }
6721        } else if self.current_service == Service::CloudWatchAlarms {
6722            // Collapse expanded alarm
6723            self.alarms_state.table.collapse();
6724        } else if self.current_service == Service::Ec2Instances {
6725            self.ec2_state.table.collapse();
6726        } else if self.current_service == Service::ApiGatewayApis {
6727            self.apig_state.apis.collapse();
6728        } else if self.current_service == Service::EcrRepositories {
6729            crate::ecr::actions::prev_pane(self);
6730        } else if self.current_service == Service::SqsQueues {
6731            if self.sqs_state.current_queue.is_some()
6732                && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
6733            {
6734                self.sqs_state.triggers.collapse();
6735            } else if self.sqs_state.current_queue.is_some()
6736                && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
6737            {
6738                self.sqs_state.pipes.collapse();
6739            } else if self.sqs_state.current_queue.is_some()
6740                && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
6741            {
6742                self.sqs_state.tags.collapse();
6743            } else if self.sqs_state.current_queue.is_some()
6744                && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
6745            {
6746                self.sqs_state.subscriptions.collapse();
6747            } else {
6748                self.sqs_state.queues.collapse();
6749            }
6750        } else if self.current_service == Service::LambdaFunctions {
6751            crate::lambda::functions::prev_pane(self);
6752        } else if self.current_service == Service::LambdaApplications {
6753            crate::lambda::applications::prev_pane(self);
6754        } else if self.current_service == Service::CloudFormationStacks
6755            && self.cfn_state.current_stack.is_none()
6756        {
6757            self.cfn_state.table.collapse();
6758        } else if self.current_service == Service::CloudFormationStacks
6759            && self.cfn_state.current_stack.is_some()
6760            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
6761        {
6762            self.cfn_state.parameters.collapse();
6763        } else if self.current_service == Service::CloudFormationStacks
6764            && self.cfn_state.current_stack.is_some()
6765            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
6766        {
6767            self.cfn_state.outputs.collapse();
6768        } else if self.current_service == Service::CloudFormationStacks
6769            && self.cfn_state.current_stack.is_some()
6770            && self.cfn_state.detail_tab == CfnDetailTab::Resources
6771        {
6772            self.cfn_state.resources.collapse();
6773        } else if self.current_service == Service::IamUsers {
6774            if self.iam_state.users.has_expanded_item() {
6775                self.iam_state.users.collapse();
6776            }
6777        } else if self.current_service == Service::IamRoles {
6778            if self.view_mode == ViewMode::PolicyView {
6779                // Go back from policy view to role detail
6780                self.view_mode = ViewMode::Detail;
6781                self.iam_state.current_policy = None;
6782                self.iam_state.policy_document.clear();
6783                self.iam_state.policy_scroll = 0;
6784            } else if self.iam_state.current_role.is_some() {
6785                if self.iam_state.role_tab == RoleTab::Tags
6786                    && self.iam_state.tags.has_expanded_item()
6787                {
6788                    self.iam_state.tags.collapse();
6789                } else if self.iam_state.role_tab == RoleTab::LastAccessed
6790                    && self
6791                        .iam_state
6792                        .last_accessed_services
6793                        .expanded_item
6794                        .is_some()
6795                {
6796                    self.iam_state.last_accessed_services.collapse();
6797                } else if self.iam_state.policies.has_expanded_item() {
6798                    self.iam_state.policies.collapse();
6799                }
6800            } else if self.iam_state.roles.has_expanded_item() {
6801                self.iam_state.roles.collapse();
6802            }
6803        } else if self.current_service == Service::IamUserGroups {
6804            if self.iam_state.current_group.is_some() {
6805                if self.iam_state.group_tab == GroupTab::Users
6806                    && self.iam_state.group_users.has_expanded_item()
6807                {
6808                    self.iam_state.group_users.collapse();
6809                } else if self.iam_state.group_tab == GroupTab::Permissions
6810                    && self.iam_state.policies.has_expanded_item()
6811                {
6812                    self.iam_state.policies.collapse();
6813                } else if self.iam_state.group_tab == GroupTab::AccessAdvisor
6814                    && self
6815                        .iam_state
6816                        .last_accessed_services
6817                        .expanded_item
6818                        .is_some()
6819                {
6820                    self.iam_state.last_accessed_services.collapse();
6821                }
6822            } else if self.iam_state.groups.has_expanded_item() {
6823                self.iam_state.groups.collapse();
6824            }
6825        }
6826    }
6827
6828    fn collapse_row(&mut self) {
6829        match self.current_service {
6830            Service::S3Buckets => {
6831                if self.s3_state.current_bucket.is_none() {
6832                    // Filter buckets first
6833                    let filtered_buckets: Vec<_> = self
6834                        .s3_state
6835                        .buckets
6836                        .items
6837                        .iter()
6838                        .filter(|b| {
6839                            if self.s3_state.buckets.filter.is_empty() {
6840                                true
6841                            } else {
6842                                b.name
6843                                    .to_lowercase()
6844                                    .contains(&self.s3_state.buckets.filter.to_lowercase())
6845                            }
6846                        })
6847                        .collect();
6848
6849                    // In bucket list - collapse bucket or nested prefix
6850                    let mut row_idx = 0;
6851
6852                    for bucket in filtered_buckets {
6853                        if row_idx == self.s3_state.selected_row {
6854                            // Selected row is a bucket - collapse it
6855                            self.s3_state.expanded_prefixes.remove(&bucket.name);
6856                            // Don't return - let scroll adjustment happen below
6857                            break;
6858                        }
6859                        row_idx += 1;
6860                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
6861                            // Check if bucket has error - don't count error rows as they're not selectable
6862                            if self.s3_state.bucket_errors.contains_key(&bucket.name) {
6863                                // Bucket has error, no child rows to navigate
6864                                continue;
6865                            }
6866                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
6867                                // Recursive function to check nested items at any depth
6868                                #[allow(clippy::too_many_arguments)]
6869                                fn check_nested_collapse(
6870                                    objects: &[S3Object],
6871                                    row_idx: &mut usize,
6872                                    target_row: usize,
6873                                    expanded_prefixes: &mut std::collections::HashSet<String>,
6874                                    prefix_preview: &std::collections::HashMap<
6875                                        String,
6876                                        Vec<S3Object>,
6877                                    >,
6878                                    found: &mut bool,
6879                                    selected_row: &mut usize,
6880                                    parent_row: usize,
6881                                ) {
6882                                    for obj in objects {
6883                                        let current_row = *row_idx;
6884                                        if *row_idx == target_row {
6885                                            // Selected this item - collapse or jump to parent
6886                                            if obj.is_prefix {
6887                                                if expanded_prefixes.contains(&obj.key) {
6888                                                    // Expanded: collapse it and move to parent
6889                                                    expanded_prefixes.remove(&obj.key);
6890                                                    *selected_row = parent_row;
6891                                                } else {
6892                                                    // Already collapsed: jump to parent
6893                                                    *selected_row = parent_row;
6894                                                }
6895                                            } else {
6896                                                // Not a prefix: jump to parent
6897                                                *selected_row = parent_row;
6898                                            }
6899                                            *found = true;
6900                                            return;
6901                                        }
6902                                        *row_idx += 1;
6903
6904                                        // Recursively check nested items if expanded
6905                                        if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
6906                                            if let Some(nested) = prefix_preview.get(&obj.key) {
6907                                                check_nested_collapse(
6908                                                    nested,
6909                                                    row_idx,
6910                                                    target_row,
6911                                                    expanded_prefixes,
6912                                                    prefix_preview,
6913                                                    found,
6914                                                    selected_row,
6915                                                    current_row,
6916                                                );
6917                                                if *found {
6918                                                    return;
6919                                                }
6920                                            } else {
6921                                                *row_idx += 1; // Loading row
6922                                            }
6923                                        }
6924                                    }
6925                                }
6926
6927                                let mut found = false;
6928                                let parent_row = row_idx - 1; // Parent is the bucket
6929                                check_nested_collapse(
6930                                    preview,
6931                                    &mut row_idx,
6932                                    self.s3_state.selected_row,
6933                                    &mut self.s3_state.expanded_prefixes,
6934                                    &self.s3_state.prefix_preview,
6935                                    &mut found,
6936                                    &mut self.s3_state.selected_row,
6937                                    parent_row,
6938                                );
6939                                if found {
6940                                    // Adjust scroll offset to keep selection visible
6941                                    let visible_rows = self.s3_state.bucket_visible_rows.get();
6942                                    if self.s3_state.selected_row
6943                                        < self.s3_state.bucket_scroll_offset
6944                                    {
6945                                        self.s3_state.bucket_scroll_offset =
6946                                            self.s3_state.selected_row;
6947                                    } else if self.s3_state.selected_row
6948                                        >= self.s3_state.bucket_scroll_offset + visible_rows
6949                                    {
6950                                        self.s3_state.bucket_scroll_offset = self
6951                                            .s3_state
6952                                            .selected_row
6953                                            .saturating_sub(visible_rows - 1);
6954                                    }
6955                                    return;
6956                                }
6957                            } else {
6958                                row_idx += 1;
6959                            }
6960                        }
6961                    }
6962
6963                    // Adjust scroll offset to keep selection visible after collapse
6964                    let visible_rows = self.s3_state.bucket_visible_rows.get();
6965                    if self.s3_state.selected_row < self.s3_state.bucket_scroll_offset {
6966                        self.s3_state.bucket_scroll_offset = self.s3_state.selected_row;
6967                    } else if self.s3_state.selected_row
6968                        >= self.s3_state.bucket_scroll_offset + visible_rows
6969                    {
6970                        self.s3_state.bucket_scroll_offset =
6971                            self.s3_state.selected_row.saturating_sub(visible_rows - 1);
6972                    }
6973                }
6974            }
6975            Service::CloudWatchLogGroups => {
6976                if self.view_mode == ViewMode::Events {
6977                    if let Some(idx) = self.log_groups_state.expanded_event {
6978                        self.log_groups_state.expanded_event = None;
6979                        self.log_groups_state.selected_event = idx;
6980                    }
6981                } else if self.view_mode == ViewMode::Detail {
6982                    if let Some(idx) = self.log_groups_state.expanded_stream {
6983                        self.log_groups_state.expanded_stream = None;
6984                        self.log_groups_state.selected_stream = idx;
6985                    }
6986                } else {
6987                    self.log_groups_state.log_groups.collapse();
6988                }
6989            }
6990            Service::CloudWatchAlarms => self.alarms_state.table.collapse(),
6991            Service::Ec2Instances => {
6992                if self.ec2_state.current_instance.is_some()
6993                    && self.ec2_state.detail_tab == Ec2DetailTab::Tags
6994                {
6995                    self.ec2_state.tags.collapse();
6996                } else {
6997                    self.ec2_state.table.collapse();
6998                }
6999            }
7000            Service::EcrRepositories => {
7001                crate::ecr::actions::collapse_row(self);
7002            }
7003            Service::LambdaFunctions => crate::lambda::functions::collapse_row(self),
7004            Service::LambdaApplications => crate::lambda::applications::prev_pane(self),
7005            Service::SqsQueues => self.sqs_state.queues.collapse(),
7006            Service::CloudFormationStacks => {
7007                if self.cfn_state.current_stack.is_some() {
7008                    match self.cfn_state.detail_tab {
7009                        crate::ui::cfn::DetailTab::Resources => {
7010                            self.cfn_state.resources.collapse();
7011                        }
7012                        crate::ui::cfn::DetailTab::Parameters => {
7013                            self.cfn_state.parameters.collapse();
7014                        }
7015                        crate::ui::cfn::DetailTab::Outputs => {
7016                            self.cfn_state.outputs.collapse();
7017                        }
7018                        _ => {}
7019                    }
7020                } else {
7021                    self.cfn_state.table.collapse();
7022                }
7023            }
7024            Service::IamUsers => {
7025                if self.iam_state.current_user.is_some() {
7026                    match self.iam_state.user_tab {
7027                        crate::ui::iam::UserTab::Permissions => {
7028                            self.iam_state.policies.collapse();
7029                        }
7030                        crate::ui::iam::UserTab::Groups => {
7031                            self.iam_state.user_group_memberships.collapse();
7032                        }
7033                        crate::ui::iam::UserTab::Tags => {
7034                            self.iam_state.user_tags.collapse();
7035                        }
7036                        _ => {}
7037                    }
7038                } else {
7039                    self.iam_state.users.collapse();
7040                }
7041            }
7042            Service::IamRoles => {
7043                if self.iam_state.current_role.is_some() {
7044                    match self.iam_state.role_tab {
7045                        crate::ui::iam::RoleTab::Permissions => {
7046                            self.iam_state.policies.collapse();
7047                        }
7048                        crate::ui::iam::RoleTab::Tags => {
7049                            self.iam_state.tags.collapse();
7050                        }
7051                        _ => {}
7052                    }
7053                } else {
7054                    self.iam_state.roles.collapse();
7055                }
7056            }
7057            Service::IamUserGroups => self.iam_state.groups.collapse(),
7058            Service::ApiGatewayApis => {
7059                if let Some(api) = &self.apig_state.current_api {
7060                    if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
7061                        let protocol = api.protocol_type.to_uppercase();
7062                        if protocol == "REST" {
7063                            // Handle resources tree - use filtered items
7064                            let (filtered_items, _filtered_children) =
7065                                crate::ui::apig::filter_tree_items(
7066                                    &self.apig_state.resources.items,
7067                                    &self.apig_state.resource_children,
7068                                    &self.apig_state.route_filter,
7069                                );
7070
7071                            let selected_row = self.apig_state.resources.selected;
7072                            let mut current_row = 0;
7073                            if let Some(resource_id) = self.find_resource_at_row(
7074                                &filtered_items,
7075                                selected_row,
7076                                &mut current_row,
7077                            ) {
7078                                if self.apig_state.expanded_resources.contains(&resource_id) {
7079                                    self.apig_state.expanded_resources.remove(&resource_id);
7080                                } else if let Some(parent_row) =
7081                                    self.find_resource_parent_row(&filtered_items, &resource_id)
7082                                {
7083                                    self.apig_state.resources.selected = parent_row;
7084                                }
7085                            }
7086                        } else {
7087                            // Handle routes tree - use filtered items
7088                            let (filtered_items, _filtered_children) =
7089                                crate::ui::apig::filter_tree_items(
7090                                    &self.apig_state.routes.items,
7091                                    &self.apig_state.route_children,
7092                                    &self.apig_state.route_filter,
7093                                );
7094
7095                            let selected_row = self.apig_state.routes.selected;
7096                            let mut current_row = 0;
7097                            if let Some(route_key) = self.find_route_at_row(
7098                                &filtered_items,
7099                                selected_row,
7100                                &mut current_row,
7101                            ) {
7102                                if self.apig_state.expanded_routes.contains(&route_key) {
7103                                    self.apig_state.expanded_routes.remove(&route_key);
7104                                } else if let Some(parent_row) =
7105                                    self.find_parent_row(&filtered_items, &route_key)
7106                                {
7107                                    self.apig_state.routes.selected = parent_row;
7108                                }
7109                            }
7110                        }
7111                    }
7112                } else {
7113                    self.apig_state.apis.collapse();
7114                }
7115            }
7116            Service::CloudTrailEvents => {
7117                if self.cloudtrail_state.current_event.is_some()
7118                    && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
7119                {
7120                    // Collapse resources table column
7121                    self.cloudtrail_state.resources_expanded_index = None;
7122                } else {
7123                    self.cloudtrail_state.table.collapse();
7124                }
7125            }
7126            _ => {}
7127        }
7128    }
7129
7130    fn expand_row(&mut self) {
7131        match self.current_service {
7132            Service::S3Buckets => {
7133                if self.s3_state.current_bucket.is_none() {
7134                    // Filter buckets first
7135                    let filtered_buckets: Vec<_> = self
7136                        .s3_state
7137                        .buckets
7138                        .items
7139                        .iter()
7140                        .filter(|b| {
7141                            if self.s3_state.buckets.filter.is_empty() {
7142                                true
7143                            } else {
7144                                b.name
7145                                    .to_lowercase()
7146                                    .contains(&self.s3_state.buckets.filter.to_lowercase())
7147                            }
7148                        })
7149                        .collect();
7150
7151                    // Recursive helper to check nested items
7152                    fn check_nested_expand(
7153                        objects: &[S3Object],
7154                        row_idx: &mut usize,
7155                        target_row: usize,
7156                        expanded_prefixes: &mut std::collections::HashSet<String>,
7157                        prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7158                    ) -> Option<(bool, usize)> {
7159                        for obj in objects {
7160                            if *row_idx == target_row {
7161                                if obj.is_prefix {
7162                                    // Toggle expansion
7163                                    if expanded_prefixes.contains(&obj.key) {
7164                                        expanded_prefixes.remove(&obj.key);
7165                                        return Some((true, *row_idx));
7166                                    } else {
7167                                        expanded_prefixes.insert(obj.key.clone());
7168                                        return Some((true, *row_idx + 1)); // Move to first child
7169                                    }
7170                                }
7171                                return Some((false, *row_idx));
7172                            }
7173                            *row_idx += 1;
7174
7175                            // Check nested items if expanded
7176                            if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7177                                if let Some(nested) = prefix_preview.get(&obj.key) {
7178                                    if let Some(result) = check_nested_expand(
7179                                        nested,
7180                                        row_idx,
7181                                        target_row,
7182                                        expanded_prefixes,
7183                                        prefix_preview,
7184                                    ) {
7185                                        return Some(result);
7186                                    }
7187                                }
7188                            }
7189                        }
7190                        None
7191                    }
7192
7193                    let mut row_idx = 0;
7194                    for bucket in filtered_buckets {
7195                        if row_idx == self.s3_state.selected_row {
7196                            if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7197                                // Already expanded: if preview loaded, enter first child
7198                                if let Some(preview) =
7199                                    self.s3_state.bucket_preview.get(&bucket.name)
7200                                {
7201                                    if !preview.is_empty() {
7202                                        self.s3_state.selected_row = row_idx + 1;
7203                                        let visible = self.s3_state.bucket_visible_rows.get();
7204                                        if self.s3_state.selected_row
7205                                            >= self.s3_state.bucket_scroll_offset + visible
7206                                        {
7207                                            self.s3_state.bucket_scroll_offset = self
7208                                                .s3_state
7209                                                .selected_row
7210                                                .saturating_sub(visible - 1);
7211                                        }
7212                                    }
7213                                    // If preview empty, nothing to enter — stay
7214                                } else {
7215                                    // Preview still loading — stay on bucket
7216                                }
7217                            } else {
7218                                self.s3_state.expanded_prefixes.insert(bucket.name.clone());
7219                                self.s3_state.buckets.loading = true;
7220                                // Move to first child only if preview already loaded
7221                                if let Some(preview) =
7222                                    self.s3_state.bucket_preview.get(&bucket.name)
7223                                {
7224                                    if !preview.is_empty() {
7225                                        self.s3_state.selected_row = row_idx + 1;
7226                                        let visible = self.s3_state.bucket_visible_rows.get();
7227                                        if self.s3_state.selected_row
7228                                            >= self.s3_state.bucket_scroll_offset + visible
7229                                        {
7230                                            self.s3_state.bucket_scroll_offset = self
7231                                                .s3_state
7232                                                .selected_row
7233                                                .saturating_sub(visible - 1);
7234                                        }
7235                                    }
7236                                }
7237                            }
7238                            return;
7239                        }
7240                        row_idx += 1;
7241
7242                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7243                            if self.s3_state.bucket_errors.contains_key(&bucket.name) {
7244                                continue;
7245                            }
7246                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7247                                if let Some((loading, new_row)) = check_nested_expand(
7248                                    preview,
7249                                    &mut row_idx,
7250                                    self.s3_state.selected_row,
7251                                    &mut self.s3_state.expanded_prefixes,
7252                                    &self.s3_state.prefix_preview,
7253                                ) {
7254                                    self.s3_state.selected_row = new_row;
7255                                    if loading {
7256                                        self.s3_state.buckets.loading = true;
7257                                    }
7258                                    // Adjust scroll so new selection stays visible
7259                                    let visible = self.s3_state.bucket_visible_rows.get();
7260                                    if self.s3_state.selected_row
7261                                        >= self.s3_state.bucket_scroll_offset + visible
7262                                    {
7263                                        self.s3_state.bucket_scroll_offset =
7264                                            self.s3_state.selected_row.saturating_sub(visible - 1);
7265                                    } else if self.s3_state.selected_row
7266                                        < self.s3_state.bucket_scroll_offset
7267                                    {
7268                                        self.s3_state.bucket_scroll_offset =
7269                                            self.s3_state.selected_row;
7270                                    }
7271                                    return;
7272                                }
7273                            }
7274                        }
7275                    }
7276                } else {
7277                    // Inside a bucket - expand objects
7278                    fn check_object_expand(
7279                        objects: &[S3Object],
7280                        row_idx: &mut usize,
7281                        target_row: usize,
7282                        expanded_prefixes: &mut std::collections::HashSet<String>,
7283                        prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7284                    ) -> Option<(bool, usize)> {
7285                        for obj in objects {
7286                            if *row_idx == target_row {
7287                                if obj.is_prefix {
7288                                    if expanded_prefixes.contains(&obj.key) {
7289                                        expanded_prefixes.remove(&obj.key);
7290                                        return Some((true, *row_idx));
7291                                    } else {
7292                                        expanded_prefixes.insert(obj.key.clone());
7293                                        return Some((true, *row_idx + 1));
7294                                    }
7295                                }
7296                                return Some((false, *row_idx));
7297                            }
7298                            *row_idx += 1;
7299
7300                            if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7301                                if let Some(nested) = prefix_preview.get(&obj.key) {
7302                                    if let Some(result) = check_object_expand(
7303                                        nested,
7304                                        row_idx,
7305                                        target_row,
7306                                        expanded_prefixes,
7307                                        prefix_preview,
7308                                    ) {
7309                                        return Some(result);
7310                                    }
7311                                }
7312                            }
7313                        }
7314                        None
7315                    }
7316
7317                    let mut row_idx = 0;
7318                    if let Some((loading, new_row)) = check_object_expand(
7319                        &self.s3_state.objects,
7320                        &mut row_idx,
7321                        self.s3_state.selected_object,
7322                        &mut self.s3_state.expanded_prefixes,
7323                        &self.s3_state.prefix_preview,
7324                    ) {
7325                        self.s3_state.selected_object = new_row;
7326                        if loading {
7327                            self.s3_state.buckets.loading = true;
7328                        }
7329                    }
7330                }
7331            }
7332            Service::ApiGatewayApis => {
7333                if let Some(api) = &self.apig_state.current_api {
7334                    if self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes {
7335                        let protocol = api.protocol_type.to_uppercase();
7336                        if protocol == "REST" {
7337                            // Handle resources tree - use filtered items
7338                            let (filtered_items, filtered_children) =
7339                                crate::ui::apig::filter_tree_items(
7340                                    &self.apig_state.resources.items,
7341                                    &self.apig_state.resource_children,
7342                                    &self.apig_state.route_filter,
7343                                );
7344
7345                            let selected_row = self.apig_state.resources.selected;
7346                            let mut current_row = 0;
7347                            if let Some(resource_id) = self.find_resource_at_row(
7348                                &filtered_items,
7349                                selected_row,
7350                                &mut current_row,
7351                            ) {
7352                                if self.apig_state.expanded_resources.contains(&resource_id) {
7353                                    let total_rows =
7354                                        crate::ui::tree::TreeRenderer::count_visible_rows(
7355                                            &filtered_items,
7356                                            &self.apig_state.expanded_resources,
7357                                            &filtered_children,
7358                                        );
7359                                    if selected_row + 1 < total_rows {
7360                                        self.apig_state.resources.selected = selected_row + 1;
7361                                    }
7362                                } else {
7363                                    self.apig_state.expanded_resources.insert(resource_id);
7364                                }
7365                            }
7366                        } else {
7367                            // Handle routes tree - use filtered items
7368                            let (filtered_items, filtered_children) =
7369                                crate::ui::apig::filter_tree_items(
7370                                    &self.apig_state.routes.items,
7371                                    &self.apig_state.route_children,
7372                                    &self.apig_state.route_filter,
7373                                );
7374
7375                            let selected_row = self.apig_state.routes.selected;
7376                            let mut current_row = 0;
7377                            if let Some(route_key) = self.find_route_at_row(
7378                                &filtered_items,
7379                                selected_row,
7380                                &mut current_row,
7381                            ) {
7382                                if self.apig_state.expanded_routes.contains(&route_key) {
7383                                    let total_rows =
7384                                        crate::ui::tree::TreeRenderer::count_visible_rows(
7385                                            &filtered_items,
7386                                            &self.apig_state.expanded_routes,
7387                                            &filtered_children,
7388                                        );
7389                                    if selected_row + 1 < total_rows {
7390                                        self.apig_state.routes.selected = selected_row + 1;
7391                                    }
7392                                } else {
7393                                    self.apig_state.expanded_routes.insert(route_key);
7394                                }
7395                            }
7396                        }
7397                    }
7398                } else {
7399                    self.apig_state.apis.expand();
7400                }
7401            }
7402            Service::CloudTrailEvents => {
7403                if self.cloudtrail_state.current_event.is_some()
7404                    && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
7405                {
7406                    // Expand resources table column
7407                    self.cloudtrail_state.resources_expanded_index = Some(0);
7408                } else {
7409                    self.cloudtrail_state.table.expand();
7410                }
7411            }
7412            _ => {
7413                // For other services, Right arrow switches panes
7414                self.next_pane();
7415            }
7416        }
7417    }
7418
7419    fn find_route_at_row(
7420        &self,
7421        routes: &[Route],
7422        target_row: usize,
7423        current_row: &mut usize,
7424    ) -> Option<String> {
7425        for route in routes {
7426            if *current_row == target_row {
7427                return Some(route.route_key.clone());
7428            }
7429            *current_row += 1;
7430
7431            // Check children if expanded
7432            if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
7433                if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
7434                    if let Some(key) = self.find_route_at_row(children, target_row, current_row) {
7435                        return Some(key);
7436                    }
7437                }
7438            }
7439        }
7440        None
7441    }
7442
7443    fn find_route_id_at_row_with_children(
7444        &self,
7445        routes: &[Route],
7446        children_map: &HashMap<String, Vec<Route>>,
7447        target_row: usize,
7448        current_row: &mut usize,
7449    ) -> Option<String> {
7450        for route in routes {
7451            if *current_row == target_row {
7452                // Only return route_id if this is a leaf (has target/integration)
7453                if !route.target.is_empty() {
7454                    return Some(route.route_id.clone());
7455                } else {
7456                    return None;
7457                }
7458            }
7459            *current_row += 1;
7460
7461            // Check children if expanded
7462            if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
7463                if let Some(children) = children_map.get(&route.route_key) {
7464                    if let Some(id) = self.find_route_id_at_row_with_children(
7465                        children,
7466                        children_map,
7467                        target_row,
7468                        current_row,
7469                    ) {
7470                        return Some(id);
7471                    }
7472                }
7473            }
7474        }
7475        None
7476    }
7477
7478    fn find_parent_row(&self, routes: &[Route], child_key: &str) -> Option<usize> {
7479        let mut current_row = 0;
7480        self.find_parent_row_recursive(routes, child_key, &mut current_row)
7481    }
7482
7483    fn find_parent_row_recursive(
7484        &self,
7485        routes: &[Route],
7486        child_key: &str,
7487        current_row: &mut usize,
7488    ) -> Option<usize> {
7489        for route in routes {
7490            let parent_row = *current_row;
7491            *current_row += 1;
7492
7493            // Check if this route's children contain the target
7494            if route.is_expandable() && self.apig_state.expanded_routes.contains(&route.route_key) {
7495                if let Some(children) = self.apig_state.route_children.get(&route.route_key) {
7496                    // Check direct children
7497                    for child in children {
7498                        if child.route_key == child_key {
7499                            return Some(parent_row);
7500                        }
7501                    }
7502
7503                    // Check nested children
7504                    if let Some(row) =
7505                        self.find_parent_row_recursive(children, child_key, current_row)
7506                    {
7507                        return Some(row);
7508                    }
7509                }
7510            }
7511        }
7512        None
7513    }
7514
7515    fn find_resource_at_row(
7516        &self,
7517        resources: &[ApigResource],
7518        target_row: usize,
7519        current_row: &mut usize,
7520    ) -> Option<String> {
7521        for resource in resources {
7522            if *current_row == target_row {
7523                return Some(resource.id.clone());
7524            }
7525            *current_row += 1;
7526
7527            if self.apig_state.expanded_resources.contains(&resource.id) {
7528                if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
7529                    if let Some(id) = self.find_resource_at_row(children, target_row, current_row) {
7530                        return Some(id);
7531                    }
7532                }
7533            }
7534        }
7535        None
7536    }
7537
7538    fn find_resource_parent_row(
7539        &self,
7540        resources: &[ApigResource],
7541        child_id: &str,
7542    ) -> Option<usize> {
7543        let mut current_row = 0;
7544        self.find_resource_parent_row_recursive(resources, child_id, &mut current_row)
7545    }
7546
7547    fn find_resource_parent_row_recursive(
7548        &self,
7549        resources: &[ApigResource],
7550        child_id: &str,
7551        current_row: &mut usize,
7552    ) -> Option<usize> {
7553        for resource in resources {
7554            let parent_row = *current_row;
7555            *current_row += 1;
7556
7557            if self.apig_state.expanded_resources.contains(&resource.id) {
7558                if let Some(children) = self.apig_state.resource_children.get(&resource.id) {
7559                    for child in children {
7560                        if child.id == child_id {
7561                            return Some(parent_row);
7562                        }
7563                    }
7564
7565                    if let Some(row) =
7566                        self.find_resource_parent_row_recursive(children, child_id, current_row)
7567                    {
7568                        return Some(row);
7569                    }
7570                }
7571            }
7572        }
7573        None
7574    }
7575
7576    fn select_item(&mut self) {
7577        if self.mode == Mode::RegionPicker {
7578            let filtered = self.get_filtered_regions();
7579            if let Some(region) = filtered.get(self.region_picker_selected) {
7580                // Save current session before changing region
7581                if !self.tabs.is_empty() {
7582                    let mut session = Session::new(
7583                        self.profile.clone(),
7584                        self.region.clone(),
7585                        self.config.account_id.clone(),
7586                        self.config.role_arn.clone(),
7587                    );
7588
7589                    for tab in &self.tabs {
7590                        session.tabs.push(SessionTab {
7591                            service: format!("{:?}", tab.service),
7592                            title: tab.title.clone(),
7593                            breadcrumb: tab.breadcrumb.clone(),
7594                            filter: None,
7595                            selected_item: None,
7596                        });
7597                    }
7598
7599                    let _ = session.save();
7600                }
7601
7602                self.region = region.code.to_string();
7603                self.config.region = region.code.to_string();
7604
7605                // Close all tabs - region change invalidates all data
7606                self.tabs.clear();
7607                self.current_tab = 0;
7608                self.service_selected = false;
7609
7610                self.mode = Mode::Normal;
7611            }
7612        } else if self.mode == Mode::ProfilePicker {
7613            let filtered = self.get_filtered_profiles();
7614            if let Some(profile) = filtered.get(self.profile_picker_selected) {
7615                let profile_name = profile.name.clone();
7616                let profile_region = profile.region.clone();
7617
7618                self.profile = profile_name.clone();
7619                std::env::set_var("AWS_PROFILE", &profile_name);
7620
7621                // Use profile's region if available
7622                if let Some(region) = profile_region {
7623                    self.region = region;
7624                }
7625
7626                self.mode = Mode::Normal;
7627                // Note: Changing profile requires reconnecting to AWS
7628            }
7629        } else if self.mode == Mode::ServicePicker {
7630            // If filter is active (INSERT mode), exit filter mode instead of selecting
7631            if self.service_picker.filter_active {
7632                self.service_picker.filter_active = false;
7633                return;
7634            }
7635
7636            let filtered = self.filtered_services();
7637            if let Some(&service) = filtered.get(self.service_picker.selected) {
7638                let new_service = match service {
7639                    "API Gateway › APIs" => Service::ApiGatewayApis,
7640                    "CloudWatch › Log Groups" => Service::CloudWatchLogGroups,
7641                    "CloudWatch › Logs Insights" => Service::CloudWatchInsights,
7642                    "CloudWatch › Alarms" => Service::CloudWatchAlarms,
7643                    "CloudTrail › Event History" => Service::CloudTrailEvents,
7644                    "CloudFormation › Stacks" => Service::CloudFormationStacks,
7645                    "EC2 › Instances" => Service::Ec2Instances,
7646                    "ECR › Repositories" => Service::EcrRepositories,
7647                    "IAM › Users" => Service::IamUsers,
7648                    "IAM › Roles" => Service::IamRoles,
7649                    "IAM › User Groups" => Service::IamUserGroups,
7650                    "Lambda › Functions" => Service::LambdaFunctions,
7651                    "Lambda › Applications" => Service::LambdaApplications,
7652                    "S3 › Buckets" => Service::S3Buckets,
7653                    "SQS › Queues" => Service::SqsQueues,
7654                    _ => return,
7655                };
7656
7657                // Create new tab
7658                self.tabs.push(Tab {
7659                    service: new_service,
7660                    title: service.to_string(),
7661                    breadcrumb: service.to_string(),
7662                });
7663                self.current_tab = self.tabs.len() - 1;
7664                self.current_service = new_service;
7665                self.view_mode = ViewMode::List;
7666                self.service_selected = true;
7667                self.mode = Mode::Normal;
7668            }
7669        } else if self.mode == Mode::TabPicker {
7670            let filtered = self.get_filtered_tabs();
7671            if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
7672                self.current_tab = idx;
7673                self.current_service = self.tabs[idx].service;
7674                self.mode = Mode::Normal;
7675                self.tab_filter.clear();
7676            }
7677        } else if self.mode == Mode::SessionPicker {
7678            let filtered = self.get_filtered_sessions();
7679            if let Some(&session) = filtered.get(self.session_picker_selected) {
7680                let session = session.clone();
7681
7682                // Load the selected session
7683                self.current_session = Some(session.clone());
7684                self.profile = session.profile.clone();
7685                self.region = session.region.clone();
7686                self.config.region = session.region.clone();
7687                self.config.account_id = session.account_id.clone();
7688                self.config.role_arn = session.role_arn.clone();
7689
7690                // Restore tabs
7691                self.tabs = session
7692                    .tabs
7693                    .iter()
7694                    .map(|st| Tab {
7695                        service: match st.service.as_str() {
7696                            "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
7697                            "CloudWatchInsights" => Service::CloudWatchInsights,
7698                            "CloudWatchAlarms" => Service::CloudWatchAlarms,
7699                            "S3Buckets" => Service::S3Buckets,
7700                            "CloudTrailEvents" => Service::CloudTrailEvents,
7701                            "SqsQueues" => Service::SqsQueues,
7702                            _ => Service::CloudWatchLogGroups,
7703                        },
7704                        title: st.title.clone(),
7705                        breadcrumb: st.breadcrumb.clone(),
7706                    })
7707                    .collect();
7708
7709                if !self.tabs.is_empty() {
7710                    self.current_tab = 0;
7711                    self.current_service = self.tabs[0].service;
7712                    self.service_selected = true;
7713                }
7714
7715                self.mode = Mode::Normal;
7716            }
7717        } else if self.mode == Mode::InsightsInput {
7718            // In InsightsInput mode, behavior depends on focus
7719            use crate::app::InsightsFocus;
7720            match self.insights_state.insights.insights_focus {
7721                InsightsFocus::Query => {
7722                    // Add newline to query
7723                    self.insights_state.insights.query_text.push('\n');
7724                    self.insights_state.insights.query_cursor_line += 1;
7725                    self.insights_state.insights.query_cursor_col = 0;
7726                }
7727                InsightsFocus::LogGroupSearch => {
7728                    // Toggle dropdown
7729                    self.insights_state.insights.show_dropdown =
7730                        !self.insights_state.insights.show_dropdown;
7731                }
7732                _ => {}
7733            }
7734        } else if self.mode == Mode::Normal {
7735            // If no service selected, select from service picker
7736            if !self.service_selected {
7737                let filtered = self.filtered_services();
7738                if let Some(&service) = filtered.get(self.service_picker.selected) {
7739                    match service {
7740                        "CloudWatch › Log Groups" => {
7741                            self.current_service = Service::CloudWatchLogGroups;
7742                            self.view_mode = ViewMode::List;
7743                            self.service_selected = true;
7744                        }
7745                        "CloudWatch › Logs Insights" => {
7746                            self.current_service = Service::CloudWatchInsights;
7747                            self.view_mode = ViewMode::InsightsResults;
7748                            self.service_selected = true;
7749                        }
7750                        "CloudWatch › Alarms" => {
7751                            self.current_service = Service::CloudWatchAlarms;
7752                            self.view_mode = ViewMode::List;
7753                            self.service_selected = true;
7754                        }
7755                        "S3 › Buckets" => {
7756                            self.current_service = Service::S3Buckets;
7757                            self.view_mode = ViewMode::List;
7758                            self.service_selected = true;
7759                        }
7760                        "EC2 › Instances" => {
7761                            self.current_service = Service::Ec2Instances;
7762                            self.view_mode = ViewMode::List;
7763                            self.service_selected = true;
7764                        }
7765                        "ECR › Repositories" => {
7766                            self.current_service = Service::EcrRepositories;
7767                            self.view_mode = ViewMode::List;
7768                            self.service_selected = true;
7769                        }
7770                        "Lambda › Functions" => {
7771                            self.current_service = Service::LambdaFunctions;
7772                            self.view_mode = ViewMode::List;
7773                            self.service_selected = true;
7774                        }
7775                        "Lambda › Applications" => {
7776                            self.current_service = Service::LambdaApplications;
7777                            self.view_mode = ViewMode::List;
7778                            self.service_selected = true;
7779                        }
7780                        _ => {}
7781                    }
7782                }
7783                return;
7784            }
7785
7786            // Select in content area
7787            if self.view_mode == ViewMode::InsightsResults {
7788                // Toggle expand for selected result
7789                if self.insights_state.insights.expanded_result
7790                    == Some(self.insights_state.insights.results_selected)
7791                {
7792                    self.insights_state.insights.expanded_result = None;
7793                } else {
7794                    self.insights_state.insights.expanded_result =
7795                        Some(self.insights_state.insights.results_selected);
7796                }
7797            } else if self.current_service == Service::S3Buckets {
7798                if self.s3_state.current_bucket.is_none() {
7799                    // Filter buckets first
7800                    let filtered_buckets: Vec<_> = self
7801                        .s3_state
7802                        .buckets
7803                        .items
7804                        .iter()
7805                        .filter(|b| {
7806                            if self.s3_state.buckets.filter.is_empty() {
7807                                true
7808                            } else {
7809                                b.name
7810                                    .to_lowercase()
7811                                    .contains(&self.s3_state.buckets.filter.to_lowercase())
7812                            }
7813                        })
7814                        .collect();
7815
7816                    // Find which bucket/prefix the selected row corresponds to
7817                    let mut row_idx = 0;
7818                    for bucket in filtered_buckets {
7819                        if row_idx == self.s3_state.selected_row {
7820                            // Selected a bucket - drill into it
7821                            self.s3_state.current_bucket = Some(bucket.name.clone());
7822                            self.s3_state.prefix_stack.clear();
7823                            self.s3_state.buckets.loading = true;
7824                            return;
7825                        }
7826                        row_idx += 1;
7827
7828                        // Skip error rows - they're not selectable
7829                        if self.s3_state.bucket_errors.contains_key(&bucket.name)
7830                            && self.s3_state.expanded_prefixes.contains(&bucket.name)
7831                        {
7832                            continue;
7833                        }
7834
7835                        if self.s3_state.expanded_prefixes.contains(&bucket.name) {
7836                            if let Some(preview) = self.s3_state.bucket_preview.get(&bucket.name) {
7837                                for obj in preview {
7838                                    if row_idx == self.s3_state.selected_row {
7839                                        // Selected a prefix - drill into bucket with this prefix
7840                                        if obj.is_prefix {
7841                                            self.s3_state.current_bucket =
7842                                                Some(bucket.name.clone());
7843                                            self.s3_state.prefix_stack = vec![obj.key.clone()];
7844                                            self.s3_state.buckets.loading = true;
7845                                        }
7846                                        return;
7847                                    }
7848                                    row_idx += 1;
7849
7850                                    // Check nested preview rows
7851                                    if obj.is_prefix
7852                                        && self.s3_state.expanded_prefixes.contains(&obj.key)
7853                                    {
7854                                        if let Some(nested) =
7855                                            self.s3_state.prefix_preview.get(&obj.key)
7856                                        {
7857                                            for nested_obj in nested {
7858                                                if row_idx == self.s3_state.selected_row {
7859                                                    // Selected a nested prefix - drill into bucket with this prefix
7860                                                    if nested_obj.is_prefix {
7861                                                        self.s3_state.current_bucket =
7862                                                            Some(bucket.name.clone());
7863                                                        // Build proper prefix stack: parent, then child
7864                                                        self.s3_state.prefix_stack = vec![
7865                                                            obj.key.clone(),
7866                                                            nested_obj.key.clone(),
7867                                                        ];
7868                                                        self.s3_state.buckets.loading = true;
7869                                                    }
7870                                                    return;
7871                                                }
7872                                                row_idx += 1;
7873                                            }
7874                                        } else {
7875                                            row_idx += 1;
7876                                        }
7877                                    }
7878                                }
7879                            } else {
7880                                row_idx += 1;
7881                            }
7882                        }
7883                    }
7884                } else {
7885                    // In objects view - map visual index to actual object (including nested items)
7886                    let mut visual_idx = 0;
7887                    let mut found_obj: Option<S3Object> = None;
7888
7889                    // Helper to recursively check nested items
7890                    fn check_nested_select(
7891                        obj: &S3Object,
7892                        visual_idx: &mut usize,
7893                        target_idx: usize,
7894                        expanded_prefixes: &std::collections::HashSet<String>,
7895                        prefix_preview: &std::collections::HashMap<String, Vec<S3Object>>,
7896                        found_obj: &mut Option<S3Object>,
7897                    ) {
7898                        if obj.is_prefix && expanded_prefixes.contains(&obj.key) {
7899                            if let Some(preview) = prefix_preview.get(&obj.key) {
7900                                for nested_obj in preview {
7901                                    if *visual_idx == target_idx {
7902                                        *found_obj = Some(nested_obj.clone());
7903                                        return;
7904                                    }
7905                                    *visual_idx += 1;
7906
7907                                    // Recursively check deeper levels
7908                                    check_nested_select(
7909                                        nested_obj,
7910                                        visual_idx,
7911                                        target_idx,
7912                                        expanded_prefixes,
7913                                        prefix_preview,
7914                                        found_obj,
7915                                    );
7916                                    if found_obj.is_some() {
7917                                        return;
7918                                    }
7919                                }
7920                            } else {
7921                                // Loading row
7922                                *visual_idx += 1;
7923                            }
7924                        }
7925                    }
7926
7927                    for obj in &self.s3_state.objects {
7928                        if visual_idx == self.s3_state.selected_object {
7929                            found_obj = Some(obj.clone());
7930                            break;
7931                        }
7932                        visual_idx += 1;
7933
7934                        // Check nested items recursively
7935                        check_nested_select(
7936                            obj,
7937                            &mut visual_idx,
7938                            self.s3_state.selected_object,
7939                            &self.s3_state.expanded_prefixes,
7940                            &self.s3_state.prefix_preview,
7941                            &mut found_obj,
7942                        );
7943                        if found_obj.is_some() {
7944                            break;
7945                        }
7946                    }
7947
7948                    if let Some(obj) = found_obj {
7949                        if obj.is_prefix {
7950                            // Drill into prefix
7951                            self.s3_state.prefix_stack.push(obj.key.clone());
7952                            self.s3_state.buckets.loading = true;
7953                        }
7954                    }
7955                }
7956            } else if self.current_service == Service::ApiGatewayApis {
7957                if self.apig_state.current_api.is_none() {
7958                    // In APIs view - drill into selected API
7959                    let filtered_apis = crate::ui::apig::filtered_apis(self);
7960                    if let Some(api) = self.apig_state.apis.get_selected(&filtered_apis) {
7961                        let protocol = api.protocol_type.to_uppercase();
7962                        self.apig_state.current_api = Some((*api).clone());
7963                        if protocol == "REST" {
7964                            self.apig_state.resources.loading = true;
7965                        } else {
7966                            self.apig_state.routes.loading = true;
7967                        }
7968                        self.update_current_tab_breadcrumb();
7969                    }
7970                }
7971            } else if self.current_service == Service::CloudFormationStacks {
7972                if self.cfn_state.current_stack.is_none() {
7973                    // Drill into stack detail view
7974                    let filtered_stacks = filtered_cloudformation_stacks(self);
7975                    if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
7976                        let stack_name = stack.name.clone();
7977                        let mut tags = stack.tags.clone();
7978                        tags.sort_by(|a, b| a.0.cmp(&b.0));
7979
7980                        self.cfn_state.current_stack = Some(stack_name);
7981                        self.cfn_state.tags.items = tags;
7982                        self.cfn_state.tags.reset();
7983                        self.cfn_state.table.loading = true;
7984                        self.update_current_tab_breadcrumb();
7985                    }
7986                }
7987            } else if self.current_service == Service::CloudWatchAlarms {
7988                if self.alarms_state.current_alarm.is_none() {
7989                    let filtered_alarms: Vec<_> = self.alarms_state.table.items.iter().collect();
7990                    if let Some(alarm) = self.alarms_state.table.get_selected(&filtered_alarms) {
7991                        self.alarms_state.current_alarm = Some(alarm.name.clone());
7992                        self.alarms_state.metrics_loading = true;
7993                        self.view_mode = ViewMode::Detail;
7994                        self.update_current_tab_breadcrumb();
7995                    }
7996                }
7997            } else if self.current_service == Service::CloudTrailEvents {
7998                if self.cloudtrail_state.current_event.is_none() {
7999                    let filtered_events: Vec<_> =
8000                        self.cloudtrail_state.table.items.iter().collect();
8001                    if let Some(event) = self.cloudtrail_state.table.get_selected(&filtered_events)
8002                    {
8003                        self.cloudtrail_state.current_event = Some((*event).clone());
8004                        self.cloudtrail_state.event_json_scroll = 0;
8005                        self.update_current_tab_breadcrumb();
8006                    }
8007                }
8008            } else if self.current_service == Service::EcrRepositories {
8009                crate::ecr::actions::select_item(self);
8010            } else if self.current_service == Service::Ec2Instances {
8011                if self.ec2_state.current_instance.is_none() {
8012                    let filtered_instances = filtered_ec2_instances(self);
8013                    if let Some(instance) = self.ec2_state.table.get_selected(&filtered_instances) {
8014                        self.ec2_state.current_instance = Some(instance.instance_id.clone());
8015                        self.view_mode = ViewMode::Detail;
8016                        self.update_current_tab_breadcrumb();
8017                    }
8018                }
8019            } else if self.current_service == Service::SqsQueues {
8020                if self.sqs_state.current_queue.is_none() {
8021                    let filtered_queues = filtered_queues(
8022                        &self.sqs_state.queues.items,
8023                        &self.sqs_state.queues.filter,
8024                    );
8025                    if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
8026                        self.sqs_state.current_queue = Some(queue.url.clone());
8027
8028                        if self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring {
8029                            self.sqs_state.metrics_loading = true;
8030                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers {
8031                            self.sqs_state.triggers.loading = true;
8032                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes {
8033                            self.sqs_state.pipes.loading = true;
8034                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging {
8035                            self.sqs_state.tags.loading = true;
8036                        } else if self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions {
8037                            self.sqs_state.subscriptions.loading = true;
8038                        }
8039                    }
8040                }
8041            } else if self.current_service == Service::IamUsers {
8042                if self.iam_state.current_user.is_some() {
8043                    // Open policy view only when in Permissions tab
8044                    if self.iam_state.user_tab == UserTab::Permissions {
8045                        let filtered = filtered_iam_policies(self);
8046                        if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
8047                            self.iam_state.current_policy = Some(policy.policy_name.clone());
8048                            self.iam_state.policy_scroll = 0;
8049                            self.view_mode = ViewMode::PolicyView;
8050                            self.iam_state.policies.loading = true;
8051                            self.update_current_tab_breadcrumb();
8052                        }
8053                    }
8054                } else if self.iam_state.current_user.is_none() {
8055                    let filtered_users = filtered_iam_users(self);
8056                    if let Some(user) = self.iam_state.users.get_selected(&filtered_users) {
8057                        self.iam_state.current_user = Some(user.user_name.clone());
8058                        self.iam_state.user_tab = UserTab::Permissions;
8059                        self.iam_state.policies.reset();
8060                        self.update_current_tab_breadcrumb();
8061                    }
8062                }
8063            } else if self.current_service == Service::IamRoles {
8064                if self.iam_state.current_role.is_some() {
8065                    // Open policy view only when in Permissions tab
8066                    if self.iam_state.role_tab == RoleTab::Permissions {
8067                        let filtered = filtered_iam_policies(self);
8068                        if let Some(policy) = self.iam_state.policies.get_selected(&filtered) {
8069                            self.iam_state.current_policy = Some(policy.policy_name.clone());
8070                            self.iam_state.policy_scroll = 0;
8071                            self.view_mode = ViewMode::PolicyView;
8072                            self.iam_state.policies.loading = true;
8073                            self.update_current_tab_breadcrumb();
8074                        }
8075                    }
8076                } else if self.iam_state.current_role.is_none() {
8077                    let filtered_roles = filtered_iam_roles(self);
8078                    if let Some(role) = self.iam_state.roles.get_selected(&filtered_roles) {
8079                        self.iam_state.current_role = Some(role.role_name.clone());
8080                        self.iam_state.role_tab = RoleTab::Permissions;
8081                        self.iam_state.policies.reset();
8082                        self.update_current_tab_breadcrumb();
8083                    }
8084                }
8085            } else if self.current_service == Service::IamUserGroups {
8086                if self.iam_state.current_group.is_none() {
8087                    let filtered_groups: Vec<_> = self
8088                        .iam_state
8089                        .groups
8090                        .items
8091                        .iter()
8092                        .filter(|g| {
8093                            if self.iam_state.groups.filter.is_empty() {
8094                                true
8095                            } else {
8096                                g.group_name
8097                                    .to_lowercase()
8098                                    .contains(&self.iam_state.groups.filter.to_lowercase())
8099                            }
8100                        })
8101                        .collect();
8102                    if let Some(group) = self.iam_state.groups.get_selected(&filtered_groups) {
8103                        self.iam_state.current_group = Some(group.group_name.clone());
8104                        self.update_current_tab_breadcrumb();
8105                    }
8106                }
8107            } else if self.current_service == Service::LambdaFunctions {
8108                crate::lambda::functions::select_item(self);
8109            } else if self.current_service == Service::LambdaApplications {
8110                crate::lambda::applications::select_item(self);
8111            } else if self.current_service == Service::CloudWatchLogGroups {
8112                if self.view_mode == ViewMode::List {
8113                    // Map filtered selection to actual group index
8114                    let filtered_groups = filtered_log_groups(self);
8115                    if let Some(selected_group) =
8116                        filtered_groups.get(self.log_groups_state.log_groups.selected)
8117                    {
8118                        if let Some(actual_idx) = self
8119                            .log_groups_state
8120                            .log_groups
8121                            .items
8122                            .iter()
8123                            .position(|g| g.name == selected_group.name)
8124                        {
8125                            self.log_groups_state.log_groups.selected = actual_idx;
8126                        }
8127                    }
8128                    self.view_mode = ViewMode::Detail;
8129                    self.log_groups_state.log_streams.clear();
8130                    self.log_groups_state.tags.items.clear();
8131                    self.log_groups_state.tags.reset();
8132                    self.log_groups_state.selected_stream = 0;
8133                    self.log_groups_state.loading = true;
8134                    self.column_selector_index = 0;
8135                    self.update_current_tab_breadcrumb();
8136                } else if self.view_mode == ViewMode::Detail {
8137                    // Map filtered stream selection to actual stream index
8138                    let filtered_streams = filtered_log_streams(self);
8139                    if let Some(selected_stream) =
8140                        filtered_streams.get(self.log_groups_state.selected_stream)
8141                    {
8142                        if let Some(actual_idx) = self
8143                            .log_groups_state
8144                            .log_streams
8145                            .iter()
8146                            .position(|s| s.name == selected_stream.name)
8147                        {
8148                            self.log_groups_state.selected_stream = actual_idx;
8149                        }
8150                    }
8151                    self.view_mode = ViewMode::Events;
8152                    self.update_current_tab_breadcrumb();
8153                    self.log_groups_state.log_events.clear();
8154                    self.log_groups_state.event_scroll_offset = 0;
8155                    self.log_groups_state.next_backward_token = None;
8156                    self.log_groups_state.loading = true;
8157                } else if self.view_mode == ViewMode::Events {
8158                    // Toggle expand for selected event
8159                    if self.log_groups_state.expanded_event
8160                        == Some(self.log_groups_state.event_scroll_offset)
8161                    {
8162                        self.log_groups_state.expanded_event = None;
8163                    } else {
8164                        self.log_groups_state.expanded_event =
8165                            Some(self.log_groups_state.event_scroll_offset);
8166                    }
8167                }
8168            } else if self.current_service == Service::CloudWatchAlarms
8169                && self.view_mode != ViewMode::Detail
8170            {
8171                // Toggle expand for selected alarm in list view
8172                self.alarms_state.table.toggle_expand();
8173            } else if self.current_service == Service::CloudWatchInsights {
8174                // In Normal mode, Enter always executes query
8175                if !self.insights_state.insights.selected_log_groups.is_empty() {
8176                    self.log_groups_state.loading = true;
8177                    self.insights_state.insights.query_completed = true;
8178                }
8179            }
8180        }
8181    }
8182
8183    pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
8184        self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
8185        Ok(())
8186    }
8187
8188    pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
8189        let alarms = self.alarms_client.list_alarms().await?;
8190        self.alarms_state.table.items = alarms
8191            .into_iter()
8192            .map(
8193                |(
8194                    name,
8195                    state,
8196                    state_updated,
8197                    description,
8198                    metric_name,
8199                    namespace,
8200                    statistic,
8201                    period,
8202                    comparison,
8203                    threshold,
8204                    actions_enabled,
8205                    state_reason,
8206                    resource,
8207                    dimensions,
8208                    expression,
8209                    alarm_type,
8210                    cross_account,
8211                )| Alarm {
8212                    name,
8213                    state,
8214                    state_updated_timestamp: state_updated,
8215                    description,
8216                    metric_name,
8217                    namespace,
8218                    statistic,
8219                    period,
8220                    comparison_operator: comparison,
8221                    threshold,
8222                    actions_enabled,
8223                    state_reason,
8224                    resource,
8225                    dimensions,
8226                    expression,
8227                    alarm_type,
8228                    cross_account,
8229                },
8230            )
8231            .collect();
8232        Ok(())
8233    }
8234
8235    pub async fn load_cloudtrail_events(&mut self) -> anyhow::Result<()> {
8236        let (events, next_token) = self.cloudtrail_client.lookup_events(None, None).await?;
8237        self.cloudtrail_state.table.items = events
8238            .into_iter()
8239            .map(
8240                |(
8241                    event_name,
8242                    event_time,
8243                    username,
8244                    event_source,
8245                    resource_type,
8246                    resource_name,
8247                    read_only,
8248                    aws_region,
8249                    event_id,
8250                    access_key_id,
8251                    source_ip_address,
8252                    error_code,
8253                    request_id,
8254                    event_type,
8255                    cloud_trail_event_json,
8256                )| CloudTrailEvent {
8257                    event_name,
8258                    event_time,
8259                    username,
8260                    event_source,
8261                    resource_type,
8262                    resource_name,
8263                    read_only,
8264                    aws_region,
8265                    event_id,
8266                    access_key_id,
8267                    source_ip_address,
8268                    error_code,
8269                    request_id,
8270                    event_type,
8271                    cloud_trail_event_json,
8272                },
8273            )
8274            .collect();
8275        self.cloudtrail_state.table.next_token = next_token;
8276        Ok(())
8277    }
8278
8279    pub async fn load_more_cloudtrail_events(&mut self) -> anyhow::Result<()> {
8280        if let Some(token) = self.cloudtrail_state.table.next_token.clone() {
8281            // Just load the next batch of events
8282            let (events, next_token) = self
8283                .cloudtrail_client
8284                .lookup_events(None, Some(token))
8285                .await?;
8286            self.cloudtrail_state
8287                .table
8288                .items
8289                .extend(events.into_iter().map(
8290                    |(
8291                        event_name,
8292                        event_time,
8293                        username,
8294                        event_source,
8295                        resource_type,
8296                        resource_name,
8297                        read_only,
8298                        aws_region,
8299                        event_id,
8300                        access_key_id,
8301                        source_ip_address,
8302                        error_code,
8303                        request_id,
8304                        event_type,
8305                        cloud_trail_event_json,
8306                    )| CloudTrailEvent {
8307                        event_name,
8308                        event_time,
8309                        username,
8310                        event_source,
8311                        resource_type,
8312                        resource_name,
8313                        read_only,
8314                        aws_region,
8315                        event_id,
8316                        access_key_id,
8317                        source_ip_address,
8318                        error_code,
8319                        request_id,
8320                        event_type,
8321                        cloud_trail_event_json,
8322                    },
8323                ));
8324            self.cloudtrail_state.table.next_token = next_token;
8325        }
8326        Ok(())
8327    }
8328
8329    pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
8330        if let Some(bucket_name) = &self.s3_state.current_bucket {
8331            // Get or fetch bucket region
8332            let bucket_region = if let Some(bucket) = self
8333                .s3_state
8334                .buckets
8335                .items
8336                .iter_mut()
8337                .find(|b| &b.name == bucket_name)
8338            {
8339                if bucket.region.is_empty() {
8340                    // Fetch the actual region
8341                    let region = self.s3_client.get_bucket_location(bucket_name).await?;
8342                    bucket.region = region.clone();
8343                    region
8344                } else {
8345                    bucket.region.clone()
8346                }
8347            } else {
8348                self.config.region.clone()
8349            };
8350
8351            let prefix = self
8352                .s3_state
8353                .prefix_stack
8354                .last()
8355                .cloned()
8356                .unwrap_or_default();
8357            let objects = self
8358                .s3_client
8359                .list_objects(bucket_name, &bucket_region, &prefix)
8360                .await?;
8361            self.s3_state.objects = objects
8362                .into_iter()
8363                .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
8364                    key,
8365                    size,
8366                    last_modified: modified,
8367                    is_prefix,
8368                    storage_class,
8369                })
8370                .collect();
8371            self.s3_state.selected_object = 0;
8372        }
8373        Ok(())
8374    }
8375
8376    pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
8377        let stored_region = self
8378            .s3_state
8379            .buckets
8380            .items
8381            .iter()
8382            .find(|b| b.name == bucket_name)
8383            .and_then(|b| {
8384                if b.region.is_empty() {
8385                    None
8386                } else {
8387                    Some(b.region.clone())
8388                }
8389            });
8390
8391        let bucket_region = match stored_region {
8392            Some(r) => r,
8393            None => {
8394                let r = self.s3_client.get_bucket_location(&bucket_name).await?;
8395                // Cache the discovered region back into the bucket
8396                if let Some(b) = self
8397                    .s3_state
8398                    .buckets
8399                    .items
8400                    .iter_mut()
8401                    .find(|b| b.name == bucket_name)
8402                {
8403                    b.region = r.clone();
8404                }
8405                r
8406            }
8407        };
8408        let objects = self
8409            .s3_client
8410            .list_objects(&bucket_name, &bucket_region, "")
8411            .await?;
8412        let preview: Vec<S3Object> = objects
8413            .into_iter()
8414            .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
8415                key,
8416                size,
8417                last_modified: modified,
8418                is_prefix,
8419                storage_class,
8420            })
8421            .collect();
8422        self.s3_state
8423            .bucket_preview
8424            .insert(bucket_name.clone(), preview);
8425        self.after_bucket_preview_loaded(&bucket_name);
8426        Ok(())
8427    }
8428
8429    /// Called after a bucket's preview is loaded. If the bucket is the currently
8430    /// selected bucket (selection is on the bucket row itself), advance selection
8431    /// to the first child row so the user lands inside the bucket automatically.
8432    pub fn after_bucket_preview_loaded(&mut self, bucket_name: &str) {
8433        if self.s3_state.current_bucket.is_some() {
8434            return; // inside a bucket view, not the bucket list
8435        }
8436        // Find the row index of this bucket in the current view
8437        let mut row_idx = 0usize;
8438        for b in &self.s3_state.buckets.items {
8439            if b.name == bucket_name {
8440                // Only advance if selection is sitting on this exact bucket row
8441                if self.s3_state.selected_row == row_idx {
8442                    if let Some(preview) = self.s3_state.bucket_preview.get(bucket_name) {
8443                        if !preview.is_empty() {
8444                            self.s3_state.selected_row = row_idx + 1;
8445                            let visible = self.s3_state.bucket_visible_rows.get();
8446                            if self.s3_state.selected_row
8447                                >= self.s3_state.bucket_scroll_offset + visible
8448                            {
8449                                self.s3_state.bucket_scroll_offset =
8450                                    self.s3_state.selected_row.saturating_sub(visible - 1);
8451                            }
8452                        }
8453                    }
8454                }
8455                return;
8456            }
8457            row_idx += 1;
8458            // Account for this bucket's loaded children
8459            if self.s3_state.expanded_prefixes.contains(&b.name) {
8460                if let Some(p) = self.s3_state.bucket_preview.get(&b.name) {
8461                    row_idx += p.len();
8462                }
8463            }
8464        }
8465    }
8466
8467    pub async fn load_prefix_preview(
8468        &mut self,
8469        bucket_name: String,
8470        prefix: String,
8471    ) -> anyhow::Result<()> {
8472        let stored_region = self
8473            .s3_state
8474            .buckets
8475            .items
8476            .iter()
8477            .find(|b| b.name == bucket_name)
8478            .and_then(|b| {
8479                if b.region.is_empty() {
8480                    None
8481                } else {
8482                    Some(b.region.clone())
8483                }
8484            });
8485
8486        let bucket_region = match stored_region {
8487            Some(r) => r,
8488            None => {
8489                let r = self.s3_client.get_bucket_location(&bucket_name).await?;
8490                if let Some(b) = self
8491                    .s3_state
8492                    .buckets
8493                    .items
8494                    .iter_mut()
8495                    .find(|b| b.name == bucket_name)
8496                {
8497                    b.region = r.clone();
8498                }
8499                r
8500            }
8501        };
8502        let objects = self
8503            .s3_client
8504            .list_objects(&bucket_name, &bucket_region, &prefix)
8505            .await?;
8506        let preview: Vec<S3Object> = objects
8507            .into_iter()
8508            .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
8509                key,
8510                size,
8511                last_modified: modified,
8512                is_prefix,
8513                storage_class,
8514            })
8515            .collect();
8516        self.s3_state.prefix_preview.insert(prefix, preview);
8517        Ok(())
8518    }
8519
8520    pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
8521        let repos = match self.ecr_state.tab {
8522            EcrTab::Private => self.ecr_client.list_private_repositories().await?,
8523            EcrTab::Public => self.ecr_client.list_public_repositories().await?,
8524        };
8525
8526        self.ecr_state.repositories.items = repos
8527            .into_iter()
8528            .map(|r| EcrRepository {
8529                name: r.name,
8530                uri: r.uri,
8531                created_at: r.created_at,
8532                tag_immutability: r.tag_immutability,
8533                encryption_type: r.encryption_type,
8534            })
8535            .collect();
8536
8537        self.ecr_state
8538            .repositories
8539            .items
8540            .sort_by(|a, b| a.name.cmp(&b.name));
8541        Ok(())
8542    }
8543
8544    pub async fn load_apis(&mut self) -> anyhow::Result<()> {
8545        let apis = self.apig_client.list_rest_apis().await?;
8546
8547        self.apig_state.apis.items = apis
8548            .into_iter()
8549            .map(|a| crate::apig::api::RestApi {
8550                id: a.id,
8551                name: a.name,
8552                description: a.description,
8553                created_date: a.created_date,
8554                api_key_source: a.api_key_source,
8555                endpoint_configuration: a.endpoint_configuration,
8556                protocol_type: a.protocol_type,
8557                disable_execute_api_endpoint: a.disable_execute_api_endpoint,
8558                status: a.status,
8559            })
8560            .collect();
8561
8562        self.apig_state
8563            .apis
8564            .items
8565            .sort_by(|a, b| a.name.cmp(&b.name));
8566        Ok(())
8567    }
8568
8569    pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
8570        let instances = self.ec2_client.list_instances().await?;
8571
8572        self.ec2_state.table.items = instances
8573            .into_iter()
8574            .map(|i| Ec2Instance {
8575                instance_id: i.instance_id,
8576                name: i.name,
8577                state: i.state,
8578                instance_type: i.instance_type,
8579                availability_zone: i.availability_zone,
8580                public_ipv4_dns: i.public_ipv4_dns,
8581                public_ipv4_address: i.public_ipv4_address,
8582                elastic_ip: i.elastic_ip,
8583                ipv6_ips: i.ipv6_ips,
8584                monitoring: i.monitoring,
8585                security_groups: i.security_groups,
8586                key_name: i.key_name,
8587                launch_time: i.launch_time,
8588                platform_details: i.platform_details,
8589                status_checks: i.status_checks,
8590                alarm_status: i.alarm_status,
8591                private_dns_name: String::new(),
8592                private_ip_address: String::new(),
8593                security_group_ids: String::new(),
8594                owner_id: String::new(),
8595                volume_id: String::new(),
8596                root_device_name: String::new(),
8597                root_device_type: String::new(),
8598                ebs_optimized: String::new(),
8599                image_id: String::new(),
8600                kernel_id: String::new(),
8601                ramdisk_id: String::new(),
8602                ami_launch_index: String::new(),
8603                reservation_id: String::new(),
8604                vpc_id: String::new(),
8605                subnet_ids: String::new(),
8606                instance_lifecycle: String::new(),
8607                architecture: String::new(),
8608                virtualization_type: String::new(),
8609                platform: String::new(),
8610                iam_instance_profile_arn: String::new(),
8611                tenancy: String::new(),
8612                affinity: String::new(),
8613                host_id: String::new(),
8614                placement_group: String::new(),
8615                partition_number: String::new(),
8616                capacity_reservation_id: String::new(),
8617                state_transition_reason_code: String::new(),
8618                state_transition_reason_message: String::new(),
8619                stop_hibernation_behavior: String::new(),
8620                outpost_arn: String::new(),
8621                product_codes: String::new(),
8622                availability_zone_id: String::new(),
8623                imdsv2: String::new(),
8624                usage_operation: String::new(),
8625                managed: String::new(),
8626                operator: String::new(),
8627            })
8628            .collect();
8629
8630        // Sort by launch time descending by default
8631        self.ec2_state
8632            .table
8633            .items
8634            .sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
8635        Ok(())
8636    }
8637
8638    pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
8639        if let Some(repo_name) = &self.ecr_state.current_repository {
8640            if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
8641                let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
8642
8643                self.ecr_state.images.items = images
8644                    .into_iter()
8645                    .map(|i| EcrImage {
8646                        tag: i.tag,
8647                        artifact_type: i.artifact_type,
8648                        pushed_at: i.pushed_at,
8649                        size_bytes: i.size_bytes,
8650                        uri: i.uri,
8651                        digest: i.digest,
8652                        last_pull_time: i.last_pull_time,
8653                    })
8654                    .collect();
8655
8656                self.ecr_state
8657                    .images
8658                    .items
8659                    .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
8660            }
8661        }
8662        Ok(())
8663    }
8664
8665    pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
8666        let stacks = self
8667            .cloudformation_client
8668            .list_stacks(self.cfn_state.view_nested)
8669            .await?;
8670
8671        let mut stacks: Vec<CfnStack> = stacks
8672            .into_iter()
8673            .map(|s| CfnStack {
8674                name: s.name,
8675                stack_id: s.stack_id,
8676                status: s.status,
8677                created_time: s.created_time,
8678                updated_time: s.updated_time,
8679                deleted_time: s.deleted_time,
8680                drift_status: s.drift_status,
8681                last_drift_check_time: s.last_drift_check_time,
8682                status_reason: s.status_reason,
8683                description: s.description,
8684                detailed_status: String::new(),
8685                root_stack: String::new(),
8686                parent_stack: String::new(),
8687                termination_protection: false,
8688                iam_role: String::new(),
8689                tags: Vec::new(),
8690                stack_policy: String::new(),
8691                rollback_monitoring_time: String::new(),
8692                rollback_alarms: Vec::new(),
8693                notification_arns: Vec::new(),
8694            })
8695            .collect();
8696
8697        // Sort by created_time DESC
8698        stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
8699
8700        self.cfn_state.table.items = stacks;
8701
8702        Ok(())
8703    }
8704
8705    pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
8706        let template = self.cloudformation_client.get_template(stack_name).await?;
8707        self.cfn_state.template_body = template;
8708        self.cfn_state.template_scroll = 0;
8709        Ok(())
8710    }
8711
8712    pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
8713        let mut parameters = self
8714            .cloudformation_client
8715            .get_stack_parameters(stack_name)
8716            .await?;
8717        parameters.sort_by(|a, b| a.key.cmp(&b.key));
8718        self.cfn_state.parameters.items = parameters;
8719        self.cfn_state.parameters.reset();
8720        Ok(())
8721    }
8722
8723    pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
8724        let outputs = self
8725            .cloudformation_client
8726            .get_stack_outputs(stack_name)
8727            .await?;
8728        self.cfn_state.outputs.items = outputs;
8729        self.cfn_state.outputs.reset();
8730        Ok(())
8731    }
8732
8733    pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
8734        let resources = self
8735            .cloudformation_client
8736            .get_stack_resources(stack_name)
8737            .await?;
8738        self.cfn_state.resources.items = resources;
8739        self.cfn_state.resources.reset();
8740        Ok(())
8741    }
8742
8743    pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
8744        // Load attached (managed) policies
8745        let attached_policies = self
8746            .iam_client
8747            .list_attached_role_policies(role_name)
8748            .await
8749            .map_err(|e| anyhow::anyhow!(e))?;
8750
8751        let mut policies: Vec<IamPolicy> = attached_policies
8752            .into_iter()
8753            .map(|p| IamPolicy {
8754                policy_name: p.policy_name().unwrap_or("").to_string(),
8755                policy_type: "Managed".to_string(),
8756                attached_via: "Direct".to_string(),
8757                attached_entities: "-".to_string(),
8758                description: "-".to_string(),
8759                creation_time: "-".to_string(),
8760                edited_time: "-".to_string(),
8761                policy_arn: p.policy_arn().map(|s| s.to_string()),
8762            })
8763            .collect();
8764
8765        // Load inline policies
8766        let inline_policy_names = self
8767            .iam_client
8768            .list_role_policies(role_name)
8769            .await
8770            .map_err(|e| anyhow::anyhow!(e))?;
8771
8772        for policy_name in inline_policy_names {
8773            policies.push(IamPolicy {
8774                policy_name,
8775                policy_type: "Inline".to_string(),
8776                attached_via: "Direct".to_string(),
8777                attached_entities: "-".to_string(),
8778                description: "-".to_string(),
8779                creation_time: "-".to_string(),
8780                edited_time: "-".to_string(),
8781                policy_arn: None,
8782            });
8783        }
8784
8785        self.iam_state.policies.items = policies;
8786
8787        Ok(())
8788    }
8789
8790    pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
8791        let attached_policies = self
8792            .iam_client
8793            .list_attached_group_policies(group_name)
8794            .await
8795            .map_err(|e| anyhow::anyhow!(e))?;
8796
8797        let mut policies: Vec<IamPolicy> = attached_policies
8798            .into_iter()
8799            .map(|p| IamPolicy {
8800                policy_name: p.policy_name().unwrap_or("").to_string(),
8801                policy_type: "AWS managed".to_string(),
8802                attached_via: "Direct".to_string(),
8803                attached_entities: "-".to_string(),
8804                description: "-".to_string(),
8805                creation_time: "-".to_string(),
8806                edited_time: "-".to_string(),
8807                policy_arn: p.policy_arn().map(|s| s.to_string()),
8808            })
8809            .collect();
8810
8811        let inline_policy_names = self
8812            .iam_client
8813            .list_group_policies(group_name)
8814            .await
8815            .map_err(|e| anyhow::anyhow!(e))?;
8816
8817        for policy_name in inline_policy_names {
8818            policies.push(IamPolicy {
8819                policy_name,
8820                policy_type: "Inline".to_string(),
8821                attached_via: "Direct".to_string(),
8822                attached_entities: "-".to_string(),
8823                description: "-".to_string(),
8824                creation_time: "-".to_string(),
8825                edited_time: "-".to_string(),
8826                policy_arn: None,
8827            });
8828        }
8829
8830        self.iam_state.policies.items = policies;
8831
8832        Ok(())
8833    }
8834
8835    pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
8836        let users = self
8837            .iam_client
8838            .get_group_users(group_name)
8839            .await
8840            .map_err(|e| anyhow::anyhow!(e))?;
8841
8842        let group_users: Vec<IamGroupUser> = users
8843            .into_iter()
8844            .map(|u| {
8845                let creation_time = {
8846                    let dt = u.create_date();
8847                    let timestamp = dt.secs();
8848                    let datetime =
8849                        chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
8850                    datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
8851                };
8852
8853                IamGroupUser {
8854                    user_name: u.user_name().to_string(),
8855                    groups: String::new(),
8856                    last_activity: String::new(),
8857                    creation_time,
8858                }
8859            })
8860            .collect();
8861
8862        self.iam_state.group_users.items = group_users;
8863
8864        Ok(())
8865    }
8866
8867    pub async fn load_policy_document(
8868        &mut self,
8869        role_name: &str,
8870        policy_name: &str,
8871    ) -> anyhow::Result<()> {
8872        // Find the policy to get its ARN and type
8873        let policy = self
8874            .iam_state
8875            .policies
8876            .items
8877            .iter()
8878            .find(|p| p.policy_name == policy_name)
8879            .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
8880
8881        let document = if let Some(policy_arn) = &policy.policy_arn {
8882            // Managed policy - use get_policy_version
8883            self.iam_client
8884                .get_policy_version(policy_arn)
8885                .await
8886                .map_err(|e| anyhow::anyhow!(e))?
8887        } else {
8888            // Inline policy - use get_role_policy
8889            self.iam_client
8890                .get_role_policy(role_name, policy_name)
8891                .await
8892                .map_err(|e| anyhow::anyhow!(e))?
8893        };
8894
8895        self.iam_state.policy_document = document;
8896
8897        Ok(())
8898    }
8899
8900    pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
8901        let document = self
8902            .iam_client
8903            .get_role(role_name)
8904            .await
8905            .map_err(|e| anyhow::anyhow!(e))?;
8906
8907        self.iam_state.trust_policy_document = document;
8908
8909        Ok(())
8910    }
8911
8912    pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
8913        // TODO: Implement real AWS API call to get service last accessed details
8914        self.iam_state.last_accessed_services.items = vec![];
8915        self.iam_state.last_accessed_services.selected = 0;
8916
8917        Ok(())
8918    }
8919
8920    pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
8921        let tags = self
8922            .iam_client
8923            .list_role_tags(role_name)
8924            .await
8925            .map_err(|e| anyhow::anyhow!(e))?;
8926        self.iam_state.tags.items = tags
8927            .into_iter()
8928            .map(|(k, v)| IamRoleTag { key: k, value: v })
8929            .collect();
8930        self.iam_state.tags.reset();
8931        Ok(())
8932    }
8933
8934    pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
8935        let tags = self
8936            .iam_client
8937            .list_user_tags(user_name)
8938            .await
8939            .map_err(|e| anyhow::anyhow!(e))?;
8940        self.iam_state.user_tags.items = tags
8941            .into_iter()
8942            .map(|(k, v)| IamUserTag { key: k, value: v })
8943            .collect();
8944        self.iam_state.user_tags.reset();
8945        Ok(())
8946    }
8947
8948    pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
8949        if let Some(group) = self
8950            .log_groups_state
8951            .log_groups
8952            .items
8953            .get(self.log_groups_state.log_groups.selected)
8954        {
8955            self.log_groups_state.log_streams =
8956                self.cloudwatch_client.list_log_streams(&group.name).await?;
8957            self.log_groups_state.selected_stream = 0;
8958        }
8959        Ok(())
8960    }
8961
8962    pub async fn load_log_group_tags(&mut self) -> anyhow::Result<()> {
8963        if let Some(group) = self
8964            .log_groups_state
8965            .log_groups
8966            .items
8967            .get(self.log_groups_state.log_groups.selected)
8968        {
8969            // Use log_group_arn if available, otherwise construct from name
8970            let arn = if let Some(arn) = &group.log_group_arn {
8971                arn.clone()
8972            } else if let Some(arn) = &group.arn {
8973                arn.clone()
8974            } else {
8975                // Construct ARN from log group name
8976                let account_id = if self.config.account_id.is_empty() {
8977                    "*"
8978                } else {
8979                    &self.config.account_id
8980                };
8981                format!(
8982                    "arn:aws:logs:{}:{}:log-group:{}",
8983                    self.config.region, account_id, group.name
8984                )
8985            };
8986
8987            let tags = self.cloudwatch_client.list_tags_for_log_group(&arn).await?;
8988            self.log_groups_state.tags.items = tags;
8989            self.log_groups_state.tags.selected = 0;
8990            self.log_groups_state.tags.scroll_offset = 0;
8991        }
8992        Ok(())
8993    }
8994
8995    pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
8996        if let Some(group) = self
8997            .log_groups_state
8998            .log_groups
8999            .items
9000            .get(self.log_groups_state.log_groups.selected)
9001        {
9002            if let Some(stream) = self
9003                .log_groups_state
9004                .log_streams
9005                .get(self.log_groups_state.selected_stream)
9006            {
9007                // Calculate time range from relative date picker
9008                let (start_time, end_time) =
9009                    if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
9010                        let now = chrono::Utc::now().timestamp_millis();
9011                        let duration_ms = match self.log_groups_state.relative_unit {
9012                            TimeUnit::Minutes => amount * 60 * 1000,
9013                            TimeUnit::Hours => amount * 60 * 60 * 1000,
9014                            TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
9015                            TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
9016                        };
9017                        (Some(now - duration_ms), Some(now))
9018                    } else {
9019                        (None, None)
9020                    };
9021
9022                let (mut events, has_more, token) = self
9023                    .cloudwatch_client
9024                    .get_log_events(
9025                        &group.name,
9026                        &stream.name,
9027                        self.log_groups_state.next_backward_token.clone(),
9028                        start_time,
9029                        end_time,
9030                    )
9031                    .await?;
9032
9033                if self.log_groups_state.next_backward_token.is_some() {
9034                    // Prepend older events - keep selection at top
9035                    events.append(&mut self.log_groups_state.log_events);
9036                    self.log_groups_state.event_scroll_offset = 0;
9037                } else {
9038                    // Initial load - start at first event
9039                    self.log_groups_state.event_scroll_offset = 0;
9040                }
9041
9042                self.log_groups_state.log_events = events;
9043                self.log_groups_state.has_older_events =
9044                    has_more && self.log_groups_state.log_events.len() >= 25;
9045                self.log_groups_state.next_backward_token = token;
9046                self.log_groups_state.selected_event = 0;
9047            }
9048        }
9049        Ok(())
9050    }
9051
9052    pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
9053        if self.insights_state.insights.selected_log_groups.is_empty() {
9054            return Err(anyhow::anyhow!(
9055                "No log groups selected. Please select at least one log group."
9056            ));
9057        }
9058
9059        let now = chrono::Utc::now().timestamp_millis();
9060        let amount = self
9061            .insights_state
9062            .insights
9063            .insights_relative_amount
9064            .parse::<i64>()
9065            .unwrap_or(1);
9066        let duration_ms = match self.insights_state.insights.insights_relative_unit {
9067            TimeUnit::Minutes => amount * 60 * 1000,
9068            TimeUnit::Hours => amount * 60 * 60 * 1000,
9069            TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
9070            TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
9071        };
9072        let start_time = now - duration_ms;
9073
9074        let query_id = self
9075            .cloudwatch_client
9076            .start_query(
9077                self.insights_state.insights.selected_log_groups.clone(),
9078                self.insights_state.insights.query_text.trim().to_string(),
9079                start_time,
9080                now,
9081            )
9082            .await?;
9083
9084        // Poll for results
9085        for _ in 0..60 {
9086            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
9087            let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
9088
9089            if status == "Complete" {
9090                self.insights_state.insights.query_results = results;
9091                self.insights_state.insights.query_completed = true;
9092                self.insights_state.insights.results_selected = 0;
9093                self.insights_state.insights.expanded_result = None;
9094                self.view_mode = ViewMode::InsightsResults;
9095                return Ok(());
9096            } else if status == "Failed" || status == "Cancelled" {
9097                return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
9098            }
9099        }
9100
9101        Err(anyhow::anyhow!("Query timeout"))
9102    }
9103
9104    /// Toggle column visibility, ensuring at least one column remains visible
9105    pub(crate) fn toggle_column_visibility<T: PartialEq + Copy>(
9106        visible_columns: &mut Vec<T>,
9107        _all_columns: &[T],
9108        column_to_toggle: T,
9109    ) {
9110        if let Some(pos) = visible_columns.iter().position(|c| c == &column_to_toggle) {
9111            // Only remove if more than one column is visible
9112            if visible_columns.len() > 1 {
9113                visible_columns.remove(pos);
9114            }
9115        } else {
9116            visible_columns.push(column_to_toggle);
9117        }
9118    }
9119}
9120
9121impl CloudWatchInsightsState {
9122    fn new() -> Self {
9123        Self {
9124            insights: InsightsState::default(),
9125            loading: false,
9126        }
9127    }
9128}
9129
9130impl CloudWatchAlarmsState {
9131    fn new() -> Self {
9132        Self {
9133            table: TableState::new(),
9134            current_alarm: None,
9135            alarm_tab: AlarmTab::AllAlarms,
9136            view_as: AlarmViewMode::Table,
9137            wrap_lines: false,
9138            sort_column: "Last state update".to_string(),
9139            sort_direction: SortDirection::Asc,
9140            input_focus: InputFocus::Filter,
9141            metric_data: Vec::new(),
9142            metrics_loading: false,
9143        }
9144    }
9145}
9146
9147impl ServicePickerState {
9148    fn new() -> Self {
9149        Self {
9150            filter: String::new(),
9151            filter_active: false,
9152            selected: 0,
9153            services: vec![
9154                "API Gateway › APIs",
9155                "CloudWatch › Log Groups",
9156                "CloudWatch › Logs Insights",
9157                "CloudWatch › Alarms",
9158                "CloudTrail › Event History",
9159                "CloudFormation › Stacks",
9160                "EC2 › Instances",
9161                "ECR › Repositories",
9162                "IAM › Users",
9163                "IAM › Roles",
9164                "IAM › User Groups",
9165                "Lambda › Functions",
9166                "Lambda › Applications",
9167                "S3 › Buckets",
9168                "SQS › Queues",
9169            ],
9170        }
9171    }
9172}
9173
9174#[cfg(test)]
9175mod test_helpers {
9176    use super::*;
9177
9178    // Test helper functions to reduce boilerplate
9179    pub fn test_app() -> App {
9180        App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
9181    }
9182
9183    pub fn test_app_no_region() -> App {
9184        App::new_without_client("test".to_string(), None)
9185    }
9186}
9187
9188#[cfg(test)]
9189mod tests {
9190    use super::*;
9191    use crate::keymap::Action;
9192    use test_helpers::*;
9193
9194    #[test]
9195    fn test_next_tab_cycles_forward() {
9196        let mut app = test_app();
9197        app.tabs = vec![
9198            Tab {
9199                service: Service::CloudWatchLogGroups,
9200                title: "CloudWatch › Log Groups".to_string(),
9201                breadcrumb: "CloudWatch › Log Groups".to_string(),
9202            },
9203            Tab {
9204                service: Service::CloudWatchInsights,
9205                title: "CloudWatch › Logs Insights".to_string(),
9206                breadcrumb: "CloudWatch › Logs Insights".to_string(),
9207            },
9208            Tab {
9209                service: Service::CloudWatchAlarms,
9210                title: "CloudWatch › Alarms".to_string(),
9211                breadcrumb: "CloudWatch › Alarms".to_string(),
9212            },
9213        ];
9214        app.current_tab = 0;
9215
9216        app.handle_action(Action::NextTab);
9217        assert_eq!(app.current_tab, 1);
9218        assert_eq!(app.current_service, Service::CloudWatchInsights);
9219
9220        app.handle_action(Action::NextTab);
9221        assert_eq!(app.current_tab, 2);
9222        assert_eq!(app.current_service, Service::CloudWatchAlarms);
9223
9224        // Should wrap around
9225        app.handle_action(Action::NextTab);
9226        assert_eq!(app.current_tab, 0);
9227        assert_eq!(app.current_service, Service::CloudWatchLogGroups);
9228    }
9229
9230    #[test]
9231    fn test_prev_tab_cycles_backward() {
9232        let mut app = test_app();
9233        app.tabs = vec![
9234            Tab {
9235                service: Service::CloudWatchLogGroups,
9236                title: "CloudWatch › Log Groups".to_string(),
9237                breadcrumb: "CloudWatch › Log Groups".to_string(),
9238            },
9239            Tab {
9240                service: Service::CloudWatchInsights,
9241                title: "CloudWatch › Logs Insights".to_string(),
9242                breadcrumb: "CloudWatch › Logs Insights".to_string(),
9243            },
9244            Tab {
9245                service: Service::CloudWatchAlarms,
9246                title: "CloudWatch › Alarms".to_string(),
9247                breadcrumb: "CloudWatch › Alarms".to_string(),
9248            },
9249        ];
9250        app.current_tab = 2;
9251
9252        app.handle_action(Action::PrevTab);
9253        assert_eq!(app.current_tab, 1);
9254        assert_eq!(app.current_service, Service::CloudWatchInsights);
9255
9256        app.handle_action(Action::PrevTab);
9257        assert_eq!(app.current_tab, 0);
9258        assert_eq!(app.current_service, Service::CloudWatchLogGroups);
9259
9260        // Should wrap around
9261        app.handle_action(Action::PrevTab);
9262        assert_eq!(app.current_tab, 2);
9263        assert_eq!(app.current_service, Service::CloudWatchAlarms);
9264    }
9265
9266    #[test]
9267    fn test_close_tab_removes_current() {
9268        let mut app = test_app();
9269        app.tabs = vec![
9270            Tab {
9271                service: Service::CloudWatchLogGroups,
9272                title: "CloudWatch › Log Groups".to_string(),
9273                breadcrumb: "CloudWatch › Log Groups".to_string(),
9274            },
9275            Tab {
9276                service: Service::CloudWatchInsights,
9277                title: "CloudWatch › Logs Insights".to_string(),
9278                breadcrumb: "CloudWatch › Logs Insights".to_string(),
9279            },
9280            Tab {
9281                service: Service::CloudWatchAlarms,
9282                title: "CloudWatch › Alarms".to_string(),
9283                breadcrumb: "CloudWatch › Alarms".to_string(),
9284            },
9285        ];
9286        app.current_tab = 1;
9287        app.service_selected = true;
9288
9289        app.handle_action(Action::CloseTab);
9290        assert_eq!(app.tabs.len(), 2);
9291        assert_eq!(app.current_tab, 1);
9292        assert_eq!(app.current_service, Service::CloudWatchAlarms);
9293    }
9294
9295    #[test]
9296    fn test_close_last_tab_exits_service() {
9297        let mut app = test_app();
9298        app.tabs = vec![Tab {
9299            service: Service::CloudWatchLogGroups,
9300            title: "CloudWatch › Log Groups".to_string(),
9301            breadcrumb: "CloudWatch › Log Groups".to_string(),
9302        }];
9303        app.current_tab = 0;
9304        app.service_selected = true;
9305
9306        app.handle_action(Action::CloseTab);
9307        assert_eq!(app.tabs.len(), 0);
9308        assert!(!app.service_selected);
9309        assert_eq!(app.current_tab, 0);
9310    }
9311
9312    #[test]
9313    fn test_close_service_removes_current_tab() {
9314        let mut app = test_app();
9315        app.tabs = vec![
9316            Tab {
9317                service: Service::CloudWatchLogGroups,
9318                title: "CloudWatch › Log Groups".to_string(),
9319                breadcrumb: "CloudWatch › Log Groups".to_string(),
9320            },
9321            Tab {
9322                service: Service::CloudWatchInsights,
9323                title: "CloudWatch › Logs Insights".to_string(),
9324                breadcrumb: "CloudWatch › Logs Insights".to_string(),
9325            },
9326            Tab {
9327                service: Service::CloudWatchAlarms,
9328                title: "CloudWatch › Alarms".to_string(),
9329                breadcrumb: "CloudWatch › Alarms".to_string(),
9330            },
9331        ];
9332        app.current_tab = 1;
9333        app.service_selected = true;
9334
9335        app.handle_action(Action::CloseService);
9336
9337        // Tab should be removed
9338        assert_eq!(app.tabs.len(), 2);
9339        // Should switch to next tab (Alarms at index 1)
9340        assert_eq!(app.current_tab, 1);
9341        assert_eq!(app.current_service, Service::CloudWatchAlarms);
9342        // Should stay in service mode, NOT show service picker
9343        assert!(app.service_selected);
9344        assert_eq!(app.mode, Mode::Normal);
9345    }
9346
9347    #[test]
9348    fn test_close_service_last_tab_shows_picker() {
9349        let mut app = test_app();
9350        app.tabs = vec![Tab {
9351            service: Service::CloudWatchLogGroups,
9352            title: "CloudWatch › Log Groups".to_string(),
9353            breadcrumb: "CloudWatch › Log Groups".to_string(),
9354        }];
9355        app.current_tab = 0;
9356        app.service_selected = true;
9357
9358        app.handle_action(Action::CloseService);
9359
9360        // Tab should be removed
9361        assert_eq!(app.tabs.len(), 0);
9362        // Should show service picker
9363        assert!(!app.service_selected);
9364        assert_eq!(app.mode, Mode::ServicePicker);
9365    }
9366
9367    #[test]
9368    fn test_open_tab_picker_with_tabs() {
9369        let mut app = test_app();
9370        app.tabs = vec![
9371            Tab {
9372                service: Service::CloudWatchLogGroups,
9373                title: "CloudWatch › Log Groups".to_string(),
9374                breadcrumb: "CloudWatch › Log Groups".to_string(),
9375            },
9376            Tab {
9377                service: Service::CloudWatchInsights,
9378                title: "CloudWatch › Logs Insights".to_string(),
9379                breadcrumb: "CloudWatch › Logs Insights".to_string(),
9380            },
9381        ];
9382        app.current_tab = 1;
9383
9384        app.handle_action(Action::OpenTabPicker);
9385        assert_eq!(app.mode, Mode::TabPicker);
9386        assert_eq!(app.tab_picker_selected, 1);
9387    }
9388
9389    #[test]
9390    fn test_open_tab_picker_without_tabs() {
9391        let mut app = test_app();
9392        app.tabs = vec![];
9393
9394        app.handle_action(Action::OpenTabPicker);
9395        assert_eq!(app.mode, Mode::Normal);
9396    }
9397
9398    #[test]
9399    fn test_pending_key_state() {
9400        let mut app = test_app();
9401        assert_eq!(app.pending_key, None);
9402
9403        app.pending_key = Some('g');
9404        assert_eq!(app.pending_key, Some('g'));
9405    }
9406
9407    #[test]
9408    fn test_tab_breadcrumb_updates() {
9409        let mut app = test_app();
9410        app.tabs = vec![Tab {
9411            service: Service::CloudWatchLogGroups,
9412            title: "CloudWatch › Log Groups".to_string(),
9413            breadcrumb: "CloudWatch > Log groups".to_string(),
9414        }];
9415        app.current_tab = 0;
9416        app.service_selected = true;
9417        app.current_service = Service::CloudWatchLogGroups;
9418
9419        // Initial breadcrumb
9420        assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
9421
9422        // Add a log group and update breadcrumb
9423        app.log_groups_state
9424            .log_groups
9425            .items
9426            .push(rusticity_core::LogGroup {
9427                name: "/aws/lambda/test".to_string(),
9428                creation_time: None,
9429                stored_bytes: Some(1024),
9430                retention_days: None,
9431                log_class: None,
9432                arn: None,
9433                log_group_arn: None,
9434                deletion_protection_enabled: None,
9435            });
9436        app.log_groups_state.log_groups.reset();
9437        app.view_mode = ViewMode::Detail;
9438        app.update_current_tab_breadcrumb();
9439
9440        // Breadcrumb should now include log group
9441        assert_eq!(
9442            app.tabs[0].breadcrumb,
9443            "CloudWatch > Log groups > /aws/lambda/test"
9444        );
9445    }
9446
9447    #[test]
9448    fn test_s3_bucket_column_selector_navigation() {
9449        let mut app = test_app();
9450        app.current_service = Service::S3Buckets;
9451        app.mode = Mode::ColumnSelector;
9452        app.column_selector_index = 0;
9453
9454        // Should navigate through 3 S3 bucket columns (0, 1, 2)
9455        app.handle_action(Action::NextItem);
9456        assert_eq!(app.column_selector_index, 1);
9457
9458        app.handle_action(Action::NextItem);
9459        assert_eq!(app.column_selector_index, 2);
9460
9461        app.handle_action(Action::NextItem);
9462        assert_eq!(app.column_selector_index, 3);
9463
9464        // Should not go beyond max (now includes page size options: 3 columns + 6 = 9)
9465        for _ in 0..10 {
9466            app.handle_action(Action::NextItem);
9467        }
9468        assert_eq!(app.column_selector_index, 9);
9469
9470        // Navigate back
9471        app.handle_action(Action::PrevItem);
9472        assert_eq!(app.column_selector_index, 8);
9473
9474        app.handle_action(Action::PrevItem);
9475        assert_eq!(app.column_selector_index, 7);
9476
9477        // Should not go below 0
9478        for _ in 0..10 {
9479            app.handle_action(Action::PrevItem);
9480        }
9481        assert_eq!(app.column_selector_index, 0);
9482    }
9483
9484    #[test]
9485    fn test_cloudwatch_alarms_state_initialized() {
9486        let app = test_app();
9487
9488        // Alarms state should be initialized
9489        assert_eq!(app.alarms_state.table.items.len(), 0);
9490        assert_eq!(app.alarms_state.table.selected, 0);
9491        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
9492        assert!(!app.alarms_state.table.loading);
9493        assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
9494        assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
9495    }
9496
9497    #[test]
9498    fn test_cloudwatch_alarms_service_selection() {
9499        let mut app = test_app();
9500
9501        // Switch to alarms service
9502        app.current_service = Service::CloudWatchAlarms;
9503        app.service_selected = true;
9504
9505        assert_eq!(app.current_service, Service::CloudWatchAlarms);
9506        assert!(app.service_selected);
9507    }
9508
9509    #[test]
9510    fn test_cloudwatch_alarms_column_preferences() {
9511        let app = test_app();
9512
9513        // Should have alarm columns defined
9514        assert!(!app.cw_alarm_column_ids.is_empty());
9515        assert!(!app.cw_alarm_visible_column_ids.is_empty());
9516
9517        // Default visible columns
9518        assert!(app
9519            .cw_alarm_visible_column_ids
9520            .contains(&AlarmColumn::Name.id()));
9521        assert!(app
9522            .cw_alarm_visible_column_ids
9523            .contains(&AlarmColumn::State.id()));
9524    }
9525
9526    #[test]
9527    fn test_s3_bucket_navigation_without_expansion() {
9528        let mut app = test_app();
9529        app.current_service = Service::S3Buckets;
9530        app.service_selected = true;
9531        app.mode = Mode::Normal;
9532
9533        // Add 3 buckets
9534        app.s3_state.buckets.items = vec![
9535            S3Bucket {
9536                name: "bucket1".to_string(),
9537                region: "us-east-1".to_string(),
9538                creation_date: "2024-01-01T00:00:00Z".to_string(),
9539            },
9540            S3Bucket {
9541                name: "bucket2".to_string(),
9542                region: "us-east-1".to_string(),
9543                creation_date: "2024-01-02T00:00:00Z".to_string(),
9544            },
9545            S3Bucket {
9546                name: "bucket3".to_string(),
9547                region: "us-east-1".to_string(),
9548                creation_date: "2024-01-03T00:00:00Z".to_string(),
9549            },
9550        ];
9551        app.s3_state.selected_row = 0;
9552
9553        // Navigate down
9554        app.handle_action(Action::NextItem);
9555        assert_eq!(app.s3_state.selected_row, 1);
9556
9557        app.handle_action(Action::NextItem);
9558        assert_eq!(app.s3_state.selected_row, 2);
9559
9560        // Should not go beyond last bucket
9561        app.handle_action(Action::NextItem);
9562        assert_eq!(app.s3_state.selected_row, 2);
9563
9564        // Navigate up
9565        app.handle_action(Action::PrevItem);
9566        assert_eq!(app.s3_state.selected_row, 1);
9567
9568        app.handle_action(Action::PrevItem);
9569        assert_eq!(app.s3_state.selected_row, 0);
9570
9571        // Should not go below 0
9572        app.handle_action(Action::PrevItem);
9573        assert_eq!(app.s3_state.selected_row, 0);
9574    }
9575
9576    #[test]
9577    fn test_s3_bucket_navigation_with_expansion() {
9578        let mut app = test_app();
9579        app.current_service = Service::S3Buckets;
9580        app.service_selected = true;
9581        app.mode = Mode::Normal;
9582
9583        // Add 2 buckets
9584        app.s3_state.buckets.items = vec![
9585            S3Bucket {
9586                name: "bucket1".to_string(),
9587                region: "us-east-1".to_string(),
9588                creation_date: "2024-01-01T00:00:00Z".to_string(),
9589            },
9590            S3Bucket {
9591                name: "bucket2".to_string(),
9592                region: "us-east-1".to_string(),
9593                creation_date: "2024-01-02T00:00:00Z".to_string(),
9594            },
9595        ];
9596
9597        // Expand bucket1 with 2 objects
9598        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
9599        app.s3_state.bucket_preview.insert(
9600            "bucket1".to_string(),
9601            vec![
9602                S3Object {
9603                    key: "file1.txt".to_string(),
9604                    size: 100,
9605                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9606                    is_prefix: false,
9607                    storage_class: "STANDARD".to_string(),
9608                },
9609                S3Object {
9610                    key: "folder/".to_string(),
9611                    size: 0,
9612                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9613                    is_prefix: true,
9614                    storage_class: String::new(),
9615                },
9616            ],
9617        );
9618
9619        app.s3_state.selected_row = 0;
9620
9621        // Total rows: bucket1 (row 0) + file1.txt (row 1) + folder/ (row 2) + bucket2 (row 3) = 4 rows
9622        // Navigate through all rows
9623        app.handle_action(Action::NextItem);
9624        assert_eq!(app.s3_state.selected_row, 1); // file1.txt
9625
9626        app.handle_action(Action::NextItem);
9627        assert_eq!(app.s3_state.selected_row, 2); // folder/
9628
9629        app.handle_action(Action::NextItem);
9630        assert_eq!(app.s3_state.selected_row, 3); // bucket2
9631
9632        // Should not go beyond last row
9633        app.handle_action(Action::NextItem);
9634        assert_eq!(app.s3_state.selected_row, 3);
9635    }
9636
9637    #[test]
9638    fn test_s3_bucket_navigation_with_nested_expansion() {
9639        let mut app = test_app();
9640        app.current_service = Service::S3Buckets;
9641        app.service_selected = true;
9642        app.mode = Mode::Normal;
9643
9644        // Add 1 bucket
9645        app.s3_state.buckets.items = vec![S3Bucket {
9646            name: "bucket1".to_string(),
9647            region: "us-east-1".to_string(),
9648            creation_date: "2024-01-01T00:00:00Z".to_string(),
9649        }];
9650
9651        // Expand bucket1 with a folder
9652        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
9653        app.s3_state.bucket_preview.insert(
9654            "bucket1".to_string(),
9655            vec![S3Object {
9656                key: "folder/".to_string(),
9657                size: 0,
9658                last_modified: "2024-01-01T00:00:00Z".to_string(),
9659                is_prefix: true,
9660                storage_class: String::new(),
9661            }],
9662        );
9663
9664        // Expand the folder with 2 nested objects
9665        app.s3_state.expanded_prefixes.insert("folder/".to_string());
9666        app.s3_state.prefix_preview.insert(
9667            "folder/".to_string(),
9668            vec![
9669                S3Object {
9670                    key: "folder/file1.txt".to_string(),
9671                    size: 100,
9672                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9673                    is_prefix: false,
9674                    storage_class: "STANDARD".to_string(),
9675                },
9676                S3Object {
9677                    key: "folder/file2.txt".to_string(),
9678                    size: 200,
9679                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9680                    is_prefix: false,
9681                    storage_class: "STANDARD".to_string(),
9682                },
9683            ],
9684        );
9685
9686        app.s3_state.selected_row = 0;
9687
9688        // Total rows: bucket1 (0) + folder/ (1) + file1.txt (2) + file2.txt (3) = 4 rows
9689        app.handle_action(Action::NextItem);
9690        assert_eq!(app.s3_state.selected_row, 1); // folder/
9691
9692        app.handle_action(Action::NextItem);
9693        assert_eq!(app.s3_state.selected_row, 2); // folder/file1.txt
9694
9695        app.handle_action(Action::NextItem);
9696        assert_eq!(app.s3_state.selected_row, 3); // folder/file2.txt
9697
9698        // Should not go beyond last row
9699        app.handle_action(Action::NextItem);
9700        assert_eq!(app.s3_state.selected_row, 3);
9701    }
9702
9703    #[test]
9704    fn test_calculate_total_bucket_rows() {
9705        let mut app = test_app();
9706
9707        // No buckets
9708        assert_eq!(app.calculate_total_bucket_rows(), 0);
9709
9710        // 2 buckets, no expansion
9711        app.s3_state.buckets.items = vec![
9712            S3Bucket {
9713                name: "bucket1".to_string(),
9714                region: "us-east-1".to_string(),
9715                creation_date: "2024-01-01T00:00:00Z".to_string(),
9716            },
9717            S3Bucket {
9718                name: "bucket2".to_string(),
9719                region: "us-east-1".to_string(),
9720                creation_date: "2024-01-02T00:00:00Z".to_string(),
9721            },
9722        ];
9723        assert_eq!(app.calculate_total_bucket_rows(), 2);
9724
9725        // Expand bucket1 with 3 objects
9726        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
9727        app.s3_state.bucket_preview.insert(
9728            "bucket1".to_string(),
9729            vec![
9730                S3Object {
9731                    key: "file1.txt".to_string(),
9732                    size: 100,
9733                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9734                    is_prefix: false,
9735                    storage_class: "STANDARD".to_string(),
9736                },
9737                S3Object {
9738                    key: "file2.txt".to_string(),
9739                    size: 200,
9740                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9741                    is_prefix: false,
9742                    storage_class: "STANDARD".to_string(),
9743                },
9744                S3Object {
9745                    key: "folder/".to_string(),
9746                    size: 0,
9747                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9748                    is_prefix: true,
9749                    storage_class: String::new(),
9750                },
9751            ],
9752        );
9753        assert_eq!(app.calculate_total_bucket_rows(), 5); // 2 buckets + 3 objects
9754
9755        // Expand folder/ with 2 nested objects
9756        app.s3_state.expanded_prefixes.insert("folder/".to_string());
9757        app.s3_state.prefix_preview.insert(
9758            "folder/".to_string(),
9759            vec![
9760                S3Object {
9761                    key: "folder/nested1.txt".to_string(),
9762                    size: 50,
9763                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9764                    is_prefix: false,
9765                    storage_class: "STANDARD".to_string(),
9766                },
9767                S3Object {
9768                    key: "folder/nested2.txt".to_string(),
9769                    size: 75,
9770                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9771                    is_prefix: false,
9772                    storage_class: "STANDARD".to_string(),
9773                },
9774            ],
9775        );
9776        assert_eq!(app.calculate_total_bucket_rows(), 7); // 2 buckets + 3 objects + 2 nested
9777    }
9778
9779    #[test]
9780    fn test_calculate_total_object_rows() {
9781        let mut app = test_app();
9782        app.s3_state.current_bucket = Some("test-bucket".to_string());
9783
9784        // No objects
9785        assert_eq!(app.calculate_total_object_rows(), 0);
9786
9787        // 2 objects, no expansion
9788        app.s3_state.objects = vec![
9789            S3Object {
9790                key: "file1.txt".to_string(),
9791                size: 100,
9792                last_modified: "2024-01-01T00:00:00Z".to_string(),
9793                is_prefix: false,
9794                storage_class: "STANDARD".to_string(),
9795            },
9796            S3Object {
9797                key: "folder/".to_string(),
9798                size: 0,
9799                last_modified: "2024-01-01T00:00:00Z".to_string(),
9800                is_prefix: true,
9801                storage_class: String::new(),
9802            },
9803        ];
9804        assert_eq!(app.calculate_total_object_rows(), 2);
9805
9806        // Expand folder/ with 2 items
9807        app.s3_state.expanded_prefixes.insert("folder/".to_string());
9808        app.s3_state.prefix_preview.insert(
9809            "folder/".to_string(),
9810            vec![
9811                S3Object {
9812                    key: "folder/file2.txt".to_string(),
9813                    size: 200,
9814                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9815                    is_prefix: false,
9816                    storage_class: "STANDARD".to_string(),
9817                },
9818                S3Object {
9819                    key: "folder/subfolder/".to_string(),
9820                    size: 0,
9821                    last_modified: "2024-01-01T00:00:00Z".to_string(),
9822                    is_prefix: true,
9823                    storage_class: String::new(),
9824                },
9825            ],
9826        );
9827        assert_eq!(app.calculate_total_object_rows(), 4); // 2 + 2 nested
9828
9829        // Expand subfolder/ with 1 item (3rd level)
9830        app.s3_state
9831            .expanded_prefixes
9832            .insert("folder/subfolder/".to_string());
9833        app.s3_state.prefix_preview.insert(
9834            "folder/subfolder/".to_string(),
9835            vec![S3Object {
9836                key: "folder/subfolder/deep.txt".to_string(),
9837                size: 50,
9838                last_modified: "2024-01-01T00:00:00Z".to_string(),
9839                is_prefix: false,
9840                storage_class: "STANDARD".to_string(),
9841            }],
9842        );
9843        assert_eq!(app.calculate_total_object_rows(), 5); // 2 + 2 nested + 1 deep
9844    }
9845
9846    #[test]
9847    fn test_s3_object_navigation_with_deep_nesting() {
9848        let mut app = test_app();
9849        app.current_service = Service::S3Buckets;
9850        app.service_selected = true;
9851        app.mode = Mode::Normal;
9852        app.s3_state.current_bucket = Some("test-bucket".to_string());
9853
9854        // Add folder structure: folder1/ -> folder2/ -> file.txt
9855        app.s3_state.objects = vec![S3Object {
9856            key: "folder1/".to_string(),
9857            size: 0,
9858            last_modified: "2024-01-01T00:00:00Z".to_string(),
9859            is_prefix: true,
9860            storage_class: String::new(),
9861        }];
9862
9863        // Expand folder1/
9864        app.s3_state
9865            .expanded_prefixes
9866            .insert("folder1/".to_string());
9867        app.s3_state.prefix_preview.insert(
9868            "folder1/".to_string(),
9869            vec![S3Object {
9870                key: "folder1/folder2/".to_string(),
9871                size: 0,
9872                last_modified: "2024-01-01T00:00:00Z".to_string(),
9873                is_prefix: true,
9874                storage_class: String::new(),
9875            }],
9876        );
9877
9878        // Expand folder2/
9879        app.s3_state
9880            .expanded_prefixes
9881            .insert("folder1/folder2/".to_string());
9882        app.s3_state.prefix_preview.insert(
9883            "folder1/folder2/".to_string(),
9884            vec![S3Object {
9885                key: "folder1/folder2/file.txt".to_string(),
9886                size: 100,
9887                last_modified: "2024-01-01T00:00:00Z".to_string(),
9888                is_prefix: false,
9889                storage_class: "STANDARD".to_string(),
9890            }],
9891        );
9892
9893        app.s3_state.selected_object = 0;
9894
9895        // Total: folder1/ (0) + folder2/ (1) + file.txt (2) = 3 rows
9896        app.handle_action(Action::NextItem);
9897        assert_eq!(app.s3_state.selected_object, 1); // folder2/
9898
9899        app.handle_action(Action::NextItem);
9900        assert_eq!(app.s3_state.selected_object, 2); // file.txt
9901
9902        // Should not go beyond
9903        app.handle_action(Action::NextItem);
9904        assert_eq!(app.s3_state.selected_object, 2);
9905    }
9906
9907    #[test]
9908    fn test_s3_expand_nested_folder_in_objects_view() {
9909        let mut app = test_app();
9910        app.current_service = Service::S3Buckets;
9911        app.service_selected = true;
9912        app.mode = Mode::Normal;
9913        app.s3_state.current_bucket = Some("test-bucket".to_string());
9914
9915        // Add parent folder
9916        app.s3_state.objects = vec![S3Object {
9917            key: "parent/".to_string(),
9918            size: 0,
9919            last_modified: "2024-01-01T00:00:00Z".to_string(),
9920            is_prefix: true,
9921            storage_class: String::new(),
9922        }];
9923
9924        // Expand parent
9925        app.s3_state.expanded_prefixes.insert("parent/".to_string());
9926        app.s3_state.prefix_preview.insert(
9927            "parent/".to_string(),
9928            vec![S3Object {
9929                key: "parent/child/".to_string(),
9930                size: 0,
9931                last_modified: "2024-01-01T00:00:00Z".to_string(),
9932                is_prefix: true,
9933                storage_class: String::new(),
9934            }],
9935        );
9936
9937        // Select the nested folder (index 1)
9938        app.s3_state.selected_object = 1;
9939
9940        // Expand it (simulate pressing Enter/Right)
9941        app.handle_action(Action::NextPane);
9942
9943        // Should be expanded now
9944        assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
9945        assert!(app.s3_state.buckets.loading); // Should trigger load
9946    }
9947
9948    #[test]
9949    fn test_s3_drill_into_nested_folder() {
9950        let mut app = test_app();
9951        app.current_service = Service::S3Buckets;
9952        app.service_selected = true;
9953        app.mode = Mode::Normal;
9954        app.s3_state.current_bucket = Some("test-bucket".to_string());
9955
9956        // Add parent folder
9957        app.s3_state.objects = vec![S3Object {
9958            key: "parent/".to_string(),
9959            size: 0,
9960            last_modified: "2024-01-01T00:00:00Z".to_string(),
9961            is_prefix: true,
9962            storage_class: String::new(),
9963        }];
9964
9965        // Expand parent
9966        app.s3_state.expanded_prefixes.insert("parent/".to_string());
9967        app.s3_state.prefix_preview.insert(
9968            "parent/".to_string(),
9969            vec![S3Object {
9970                key: "parent/child/".to_string(),
9971                size: 0,
9972                last_modified: "2024-01-01T00:00:00Z".to_string(),
9973                is_prefix: true,
9974                storage_class: String::new(),
9975            }],
9976        );
9977
9978        // Select the nested folder (index 1)
9979        app.s3_state.selected_object = 1;
9980
9981        // Drill into it (simulate pressing Enter)
9982        app.handle_action(Action::Select);
9983
9984        // Should navigate into the folder
9985        assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
9986        assert!(app.s3_state.buckets.loading); // Should trigger load
9987    }
9988
9989    #[test]
9990    fn test_s3_esc_pops_navigation_stack() {
9991        let mut app = test_app();
9992        app.current_service = Service::S3Buckets;
9993        app.s3_state.current_bucket = Some("test-bucket".to_string());
9994        app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
9995
9996        // Press Esc - should pop from stack
9997        app.handle_action(Action::GoBack);
9998        assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
9999        assert!(app.s3_state.buckets.loading);
10000
10001        // Press Esc again - should pop to bucket root
10002        app.s3_state.buckets.loading = false;
10003        app.handle_action(Action::GoBack);
10004        assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
10005        assert!(app.s3_state.buckets.loading);
10006
10007        // Press Esc again - should exit bucket
10008        app.s3_state.buckets.loading = false;
10009        app.handle_action(Action::GoBack);
10010        assert_eq!(app.s3_state.current_bucket, None);
10011    }
10012
10013    #[test]
10014    fn test_s3_esc_from_bucket_root_exits() {
10015        let mut app = test_app();
10016        app.current_service = Service::S3Buckets;
10017        app.s3_state.current_bucket = Some("test-bucket".to_string());
10018        app.s3_state.prefix_stack = vec![];
10019
10020        // Press Esc from bucket root - should exit bucket
10021        app.handle_action(Action::GoBack);
10022        assert_eq!(app.s3_state.current_bucket, None);
10023        assert_eq!(app.s3_state.objects.len(), 0);
10024    }
10025
10026    #[test]
10027    fn test_s3_drill_into_nested_prefix_from_bucket_list() {
10028        let mut app = test_app();
10029        app.current_service = Service::S3Buckets;
10030        app.service_selected = true;
10031        app.mode = Mode::Normal;
10032
10033        // Setup bucket with nested preview
10034        app.s3_state.buckets.items = vec![S3Bucket {
10035            name: "test-bucket".to_string(),
10036            region: "us-east-1".to_string(),
10037            creation_date: "2024-01-01".to_string(),
10038        }];
10039
10040        // Expand bucket to show first-level prefix
10041        app.s3_state
10042            .expanded_prefixes
10043            .insert("test-bucket".to_string());
10044        app.s3_state.bucket_preview.insert(
10045            "test-bucket".to_string(),
10046            vec![S3Object {
10047                key: "parent/".to_string(),
10048                size: 0,
10049                last_modified: "2024-01-01".to_string(),
10050                is_prefix: true,
10051                storage_class: String::new(),
10052            }],
10053        );
10054
10055        // Expand parent to show nested prefix
10056        app.s3_state.expanded_prefixes.insert("parent/".to_string());
10057        app.s3_state.prefix_preview.insert(
10058            "parent/".to_string(),
10059            vec![S3Object {
10060                key: "parent/child/".to_string(),
10061                size: 0,
10062                last_modified: "2024-01-01".to_string(),
10063                is_prefix: true,
10064                storage_class: String::new(),
10065            }],
10066        );
10067
10068        // Select nested prefix (row 2: bucket, parent, nested)
10069        app.s3_state.selected_row = 2;
10070
10071        // Drill into nested prefix
10072        app.handle_action(Action::Select);
10073
10074        // Should have both parent and child in stack
10075        assert_eq!(
10076            app.s3_state.prefix_stack,
10077            vec!["parent/".to_string(), "parent/child/".to_string()]
10078        );
10079        assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
10080        assert!(app.s3_state.buckets.loading);
10081
10082        // Now press Esc - should go back to parent
10083        app.s3_state.buckets.loading = false;
10084        app.handle_action(Action::GoBack);
10085        assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
10086        assert!(app.s3_state.buckets.loading);
10087
10088        // Press Esc again - should go to bucket root
10089        app.s3_state.buckets.loading = false;
10090        app.handle_action(Action::GoBack);
10091        assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
10092        assert!(app.s3_state.buckets.loading);
10093
10094        // Press Esc again - should exit bucket
10095        app.s3_state.buckets.loading = false;
10096        app.handle_action(Action::GoBack);
10097        assert_eq!(app.s3_state.current_bucket, None);
10098    }
10099
10100    #[test]
10101    fn test_region_picker_fuzzy_filter() {
10102        let mut app = test_app();
10103        app.region_latencies.insert("us-east-1".to_string(), 10);
10104        app.region_filter = "vir".to_string();
10105        let filtered = app.get_filtered_regions();
10106        assert!(filtered.iter().any(|r| r.code == "us-east-1"));
10107    }
10108
10109    #[test]
10110    fn test_profile_picker_loads_profiles() {
10111        let profiles = App::load_aws_profiles();
10112        // Should at least have default profile or be empty if no config
10113        assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
10114    }
10115
10116    #[test]
10117    fn test_profile_with_region_uses_it() {
10118        let mut app = test_app_no_region();
10119        app.available_profiles = vec![AwsProfile {
10120            name: "test-profile".to_string(),
10121            region: Some("eu-west-1".to_string()),
10122            account: Some("123456789".to_string()),
10123            role_arn: None,
10124            source_profile: None,
10125        }];
10126        app.profile_picker_selected = 0;
10127        app.mode = Mode::ProfilePicker;
10128
10129        // Simulate selecting the profile
10130        let filtered = app.get_filtered_profiles();
10131        if let Some(profile) = filtered.first() {
10132            let profile_name = profile.name.clone();
10133            let profile_region = profile.region.clone();
10134
10135            app.profile = profile_name;
10136            if let Some(region) = profile_region {
10137                app.region = region;
10138            }
10139        }
10140
10141        assert_eq!(app.profile, "test-profile");
10142        assert_eq!(app.region, "eu-west-1");
10143    }
10144
10145    #[test]
10146    fn test_profile_without_region_keeps_unknown() {
10147        let mut app = test_app_no_region();
10148        let initial_region = app.region.clone();
10149
10150        app.available_profiles = vec![AwsProfile {
10151            name: "test-profile".to_string(),
10152            region: None,
10153            account: None,
10154            role_arn: None,
10155            source_profile: None,
10156        }];
10157        app.profile_picker_selected = 0;
10158        app.mode = Mode::ProfilePicker;
10159
10160        let filtered = app.get_filtered_profiles();
10161        if let Some(profile) = filtered.first() {
10162            let profile_name = profile.name.clone();
10163            let profile_region = profile.region.clone();
10164
10165            app.profile = profile_name;
10166            if let Some(region) = profile_region {
10167                app.region = region;
10168            }
10169        }
10170
10171        assert_eq!(app.profile, "test-profile");
10172        assert_eq!(app.region, initial_region); // Should keep initial region
10173    }
10174
10175    #[test]
10176    fn test_region_selection_closes_all_tabs() {
10177        let mut app = test_app();
10178
10179        // Add some tabs
10180        app.tabs.push(Tab {
10181            service: Service::CloudWatchLogGroups,
10182            title: "CloudWatch".to_string(),
10183            breadcrumb: "CloudWatch".to_string(),
10184        });
10185        app.tabs.push(Tab {
10186            service: Service::S3Buckets,
10187            title: "S3".to_string(),
10188            breadcrumb: "S3".to_string(),
10189        });
10190        app.service_selected = true;
10191        app.current_tab = 1;
10192
10193        // Add latency for region
10194        app.region_latencies.insert("eu-west-1".to_string(), 50);
10195
10196        // Simulate selecting a different region
10197        app.mode = Mode::RegionPicker;
10198        app.region_picker_selected = 0;
10199
10200        let filtered = app.get_filtered_regions();
10201        if let Some(region) = filtered.first() {
10202            app.region = region.code.to_string();
10203            app.tabs.clear();
10204            app.current_tab = 0;
10205            app.service_selected = false;
10206            app.mode = Mode::Normal;
10207        }
10208
10209        assert_eq!(app.tabs.len(), 0);
10210        assert_eq!(app.current_tab, 0);
10211        assert!(!app.service_selected);
10212        assert_eq!(app.region, "eu-west-1");
10213    }
10214
10215    #[test]
10216    fn test_region_picker_can_be_closed_without_selection() {
10217        let mut app = test_app();
10218        let initial_region = app.region.clone();
10219
10220        app.mode = Mode::RegionPicker;
10221
10222        // Close without selecting (Esc)
10223        app.mode = Mode::Normal;
10224
10225        // Region should not change
10226        assert_eq!(app.region, initial_region);
10227    }
10228
10229    #[test]
10230    fn test_session_filter_works() {
10231        let mut app = test_app();
10232
10233        app.sessions = vec![
10234            Session {
10235                id: "1".to_string(),
10236                timestamp: "2024-01-01".to_string(),
10237                profile: "prod-profile".to_string(),
10238                region: "us-east-1".to_string(),
10239                account_id: "123456789".to_string(),
10240                role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
10241                tabs: vec![],
10242            },
10243            Session {
10244                id: "2".to_string(),
10245                timestamp: "2024-01-02".to_string(),
10246                profile: "dev-profile".to_string(),
10247                region: "eu-west-1".to_string(),
10248                account_id: "987654321".to_string(),
10249                role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
10250                tabs: vec![],
10251            },
10252        ];
10253
10254        // Filter by profile
10255        app.session_filter = "prod".to_string();
10256        let filtered = app.get_filtered_sessions();
10257        assert_eq!(filtered.len(), 1);
10258        assert_eq!(filtered[0].profile, "prod-profile");
10259
10260        // Filter by region
10261        app.session_filter = "eu".to_string();
10262        let filtered = app.get_filtered_sessions();
10263        assert_eq!(filtered.len(), 1);
10264        assert_eq!(filtered[0].region, "eu-west-1");
10265
10266        // No filter
10267        app.session_filter.clear();
10268        let filtered = app.get_filtered_sessions();
10269        assert_eq!(filtered.len(), 2);
10270    }
10271
10272    #[test]
10273    fn test_profile_picker_shows_account() {
10274        let mut app = test_app_no_region();
10275        app.available_profiles = vec![AwsProfile {
10276            name: "test-profile".to_string(),
10277            region: Some("us-east-1".to_string()),
10278            account: Some("123456789".to_string()),
10279            role_arn: None,
10280            source_profile: None,
10281        }];
10282
10283        let filtered = app.get_filtered_profiles();
10284        assert_eq!(filtered.len(), 1);
10285        assert_eq!(filtered[0].account, Some("123456789".to_string()));
10286    }
10287
10288    #[test]
10289    fn test_profile_without_account() {
10290        let mut app = test_app_no_region();
10291        app.available_profiles = vec![AwsProfile {
10292            name: "test-profile".to_string(),
10293            region: Some("us-east-1".to_string()),
10294            account: None,
10295            role_arn: None,
10296            source_profile: None,
10297        }];
10298
10299        let filtered = app.get_filtered_profiles();
10300        assert_eq!(filtered.len(), 1);
10301        assert_eq!(filtered[0].account, None);
10302    }
10303
10304    #[test]
10305    fn test_profile_with_all_fields() {
10306        let mut app = test_app_no_region();
10307        app.available_profiles = vec![AwsProfile {
10308            name: "prod-profile".to_string(),
10309            region: Some("us-west-2".to_string()),
10310            account: Some("123456789".to_string()),
10311            role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
10312            source_profile: Some("base-profile".to_string()),
10313        }];
10314
10315        let filtered = app.get_filtered_profiles();
10316        assert_eq!(filtered.len(), 1);
10317        assert_eq!(filtered[0].name, "prod-profile");
10318        assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
10319        assert_eq!(filtered[0].account, Some("123456789".to_string()));
10320        assert_eq!(
10321            filtered[0].role_arn,
10322            Some("arn:aws:iam::123456789:role/AdminRole".to_string())
10323        );
10324        assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
10325    }
10326
10327    #[test]
10328    fn test_profile_filter_by_source_profile() {
10329        let mut app = test_app_no_region();
10330        app.available_profiles = vec![
10331            AwsProfile {
10332                name: "profile1".to_string(),
10333                region: None,
10334                account: None,
10335                role_arn: None,
10336                source_profile: Some("base".to_string()),
10337            },
10338            AwsProfile {
10339                name: "profile2".to_string(),
10340                region: None,
10341                account: None,
10342                role_arn: None,
10343                source_profile: Some("other".to_string()),
10344            },
10345        ];
10346
10347        app.profile_filter = "base".to_string();
10348        let filtered = app.get_filtered_profiles();
10349        assert_eq!(filtered.len(), 1);
10350        assert_eq!(filtered[0].name, "profile1");
10351    }
10352
10353    #[test]
10354    fn test_profile_filter_by_role() {
10355        let mut app = test_app_no_region();
10356        app.available_profiles = vec![
10357            AwsProfile {
10358                name: "admin-profile".to_string(),
10359                region: None,
10360                account: None,
10361                role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
10362                source_profile: None,
10363            },
10364            AwsProfile {
10365                name: "dev-profile".to_string(),
10366                region: None,
10367                account: None,
10368                role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
10369                source_profile: None,
10370            },
10371        ];
10372
10373        app.profile_filter = "Admin".to_string();
10374        let filtered = app.get_filtered_profiles();
10375        assert_eq!(filtered.len(), 1);
10376        assert_eq!(filtered[0].name, "admin-profile");
10377    }
10378
10379    #[test]
10380    fn test_profiles_sorted_by_name() {
10381        let mut app = test_app_no_region();
10382        app.available_profiles = vec![
10383            AwsProfile {
10384                name: "zebra-profile".to_string(),
10385                region: None,
10386                account: None,
10387                role_arn: None,
10388                source_profile: None,
10389            },
10390            AwsProfile {
10391                name: "alpha-profile".to_string(),
10392                region: None,
10393                account: None,
10394                role_arn: None,
10395                source_profile: None,
10396            },
10397            AwsProfile {
10398                name: "beta-profile".to_string(),
10399                region: None,
10400                account: None,
10401                role_arn: None,
10402                source_profile: None,
10403            },
10404        ];
10405
10406        let filtered = app.get_filtered_profiles();
10407        assert_eq!(filtered.len(), 3);
10408        assert_eq!(filtered[0].name, "alpha-profile");
10409        assert_eq!(filtered[1].name, "beta-profile");
10410        assert_eq!(filtered[2].name, "zebra-profile");
10411    }
10412
10413    #[test]
10414    fn test_profile_with_role_arn() {
10415        let mut app = test_app_no_region();
10416        app.available_profiles = vec![AwsProfile {
10417            name: "role-profile".to_string(),
10418            region: Some("us-east-1".to_string()),
10419            account: Some("123456789".to_string()),
10420            role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
10421            source_profile: None,
10422        }];
10423
10424        let filtered = app.get_filtered_profiles();
10425        assert_eq!(filtered.len(), 1);
10426        assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
10427    }
10428
10429    #[test]
10430    fn test_profile_with_user_arn() {
10431        let mut app = test_app_no_region();
10432        app.available_profiles = vec![AwsProfile {
10433            name: "user-profile".to_string(),
10434            region: Some("us-east-1".to_string()),
10435            account: Some("123456789".to_string()),
10436            role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
10437            source_profile: None,
10438        }];
10439
10440        let filtered = app.get_filtered_profiles();
10441        assert_eq!(filtered.len(), 1);
10442        assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
10443    }
10444
10445    #[test]
10446    fn test_filtered_profiles_also_sorted() {
10447        let mut app = test_app_no_region();
10448        app.available_profiles = vec![
10449            AwsProfile {
10450                name: "prod-zebra".to_string(),
10451                region: Some("us-east-1".to_string()),
10452                account: None,
10453                role_arn: None,
10454                source_profile: None,
10455            },
10456            AwsProfile {
10457                name: "prod-alpha".to_string(),
10458                region: Some("us-east-1".to_string()),
10459                account: None,
10460                role_arn: None,
10461                source_profile: None,
10462            },
10463            AwsProfile {
10464                name: "dev-profile".to_string(),
10465                region: Some("us-west-2".to_string()),
10466                account: None,
10467                role_arn: None,
10468                source_profile: None,
10469            },
10470        ];
10471
10472        app.profile_filter = "prod".to_string();
10473        let filtered = app.get_filtered_profiles();
10474        assert_eq!(filtered.len(), 2);
10475        assert_eq!(filtered[0].name, "prod-alpha");
10476        assert_eq!(filtered[1].name, "prod-zebra");
10477    }
10478
10479    #[test]
10480    fn test_profile_picker_has_all_columns() {
10481        let mut app = test_app_no_region();
10482        app.available_profiles = vec![AwsProfile {
10483            name: "test".to_string(),
10484            region: Some("us-east-1".to_string()),
10485            account: Some("123456789".to_string()),
10486            role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
10487            source_profile: Some("base".to_string()),
10488        }];
10489
10490        let filtered = app.get_filtered_profiles();
10491        assert_eq!(filtered.len(), 1);
10492        assert!(filtered[0].name == "test");
10493        assert!(filtered[0].region.is_some());
10494        assert!(filtered[0].account.is_some());
10495        assert!(filtered[0].role_arn.is_some());
10496        assert!(filtered[0].source_profile.is_some());
10497    }
10498
10499    #[test]
10500    fn test_session_picker_shows_tab_count() {
10501        let mut app = test_app_no_region();
10502        app.sessions = vec![Session {
10503            id: "1".to_string(),
10504            timestamp: "2024-01-01".to_string(),
10505            profile: "test".to_string(),
10506            region: "us-east-1".to_string(),
10507            account_id: "123".to_string(),
10508            role_arn: String::new(),
10509            tabs: vec![
10510                SessionTab {
10511                    service: "CloudWatch".to_string(),
10512                    title: "Logs".to_string(),
10513                    breadcrumb: String::new(),
10514                    filter: None,
10515                    selected_item: None,
10516                },
10517                SessionTab {
10518                    service: "S3".to_string(),
10519                    title: "Buckets".to_string(),
10520                    breadcrumb: String::new(),
10521                    filter: None,
10522                    selected_item: None,
10523                },
10524            ],
10525        }];
10526
10527        let filtered = app.get_filtered_sessions();
10528        assert_eq!(filtered.len(), 1);
10529        assert_eq!(filtered[0].tabs.len(), 2);
10530    }
10531
10532    #[test]
10533    fn test_start_background_data_fetch_loads_profiles() {
10534        let mut app = test_app_no_region();
10535        assert!(app.available_profiles.is_empty());
10536
10537        // Load profiles synchronously
10538        app.available_profiles = App::load_aws_profiles();
10539
10540        // Profiles should be loaded
10541        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
10542    }
10543
10544    #[test]
10545    fn test_refresh_in_profile_picker() {
10546        let mut app = test_app_no_region();
10547        app.mode = Mode::ProfilePicker;
10548        app.available_profiles = vec![AwsProfile {
10549            name: "test".to_string(),
10550            region: None,
10551            account: None,
10552            role_arn: None,
10553            source_profile: None,
10554        }];
10555
10556        app.handle_action(Action::Refresh);
10557
10558        // Should set loading state
10559        assert!(app.log_groups_state.loading);
10560        assert_eq!(app.log_groups_state.loading_message, "Refreshing...");
10561    }
10562
10563    #[test]
10564    fn test_refresh_sets_loading_for_profile_picker() {
10565        let mut app = test_app_no_region();
10566        app.mode = Mode::ProfilePicker;
10567
10568        assert!(!app.log_groups_state.loading);
10569
10570        app.handle_action(Action::Refresh);
10571
10572        assert!(app.log_groups_state.loading);
10573    }
10574
10575    #[test]
10576    fn test_profiles_loaded_on_demand() {
10577        let mut app = test_app_no_region();
10578
10579        // Profiles not loaded by default
10580        assert!(app.available_profiles.is_empty());
10581
10582        // Load on demand
10583        app.available_profiles = App::load_aws_profiles();
10584
10585        // Now loaded
10586        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
10587    }
10588
10589    #[test]
10590    fn test_profile_accounts_not_fetched_automatically() {
10591        let mut app = test_app_no_region();
10592        app.available_profiles = App::load_aws_profiles();
10593
10594        // Accounts should not be populated automatically
10595        for profile in &app.available_profiles {
10596            // Account may or may not be set depending on what's in config
10597            // But we're not fetching them automatically
10598            assert!(profile.account.is_none() || profile.account.is_some());
10599        }
10600    }
10601
10602    #[test]
10603    fn test_ctrl_r_triggers_account_fetch() {
10604        let mut app = test_app_no_region();
10605        app.mode = Mode::ProfilePicker;
10606        app.available_profiles = vec![AwsProfile {
10607            name: "test".to_string(),
10608            region: Some("us-east-1".to_string()),
10609            account: None,
10610            role_arn: None,
10611            source_profile: None,
10612        }];
10613
10614        // Before refresh, account is None
10615        assert!(app.available_profiles[0].account.is_none());
10616
10617        // Trigger refresh
10618        app.handle_action(Action::Refresh);
10619
10620        // Should set loading state (actual fetch happens in main.rs event loop)
10621        assert!(app.log_groups_state.loading);
10622    }
10623
10624    #[test]
10625    fn test_refresh_in_region_picker() {
10626        let mut app = test_app_no_region();
10627        app.mode = Mode::RegionPicker;
10628
10629        let initial_latencies = app.region_latencies.len();
10630        app.handle_action(Action::Refresh);
10631
10632        // Latencies should be cleared and remeasured
10633        assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
10634    }
10635
10636    #[test]
10637    fn test_refresh_in_session_picker() {
10638        let mut app = test_app_no_region();
10639        app.mode = Mode::SessionPicker;
10640        app.sessions = vec![];
10641
10642        app.handle_action(Action::Refresh);
10643
10644        // Sessions should be reloaded (may be empty if no saved sessions)
10645        assert!(app.sessions.is_empty() || !app.sessions.is_empty());
10646    }
10647
10648    #[test]
10649    fn test_session_picker_selection() {
10650        let mut app = test_app();
10651
10652        app.sessions = vec![Session {
10653            id: "1".to_string(),
10654            timestamp: "2024-01-01".to_string(),
10655            profile: "prod-profile".to_string(),
10656            region: "us-west-2".to_string(),
10657            account_id: "123456789".to_string(),
10658            role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
10659            tabs: vec![SessionTab {
10660                service: "CloudWatchLogGroups".to_string(),
10661                title: "Log Groups".to_string(),
10662                breadcrumb: "CloudWatch › Log Groups".to_string(),
10663                filter: Some("test".to_string()),
10664                selected_item: None,
10665            }],
10666        }];
10667
10668        app.mode = Mode::SessionPicker;
10669        app.session_picker_selected = 0;
10670
10671        // Simulate selecting the session
10672        app.handle_action(Action::Select);
10673
10674        assert_eq!(app.mode, Mode::Normal);
10675        assert_eq!(app.profile, "prod-profile");
10676        assert_eq!(app.region, "us-west-2");
10677        assert_eq!(app.config.account_id, "123456789");
10678        assert_eq!(app.tabs.len(), 1);
10679        assert_eq!(app.tabs[0].title, "Log Groups");
10680    }
10681
10682    #[test]
10683    fn test_save_session_creates_session() {
10684        let mut app =
10685            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10686        app.config.account_id = "123456789".to_string();
10687        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10688
10689        app.tabs.push(Tab {
10690            service: Service::CloudWatchLogGroups,
10691            title: "Log Groups".to_string(),
10692            breadcrumb: "CloudWatch › Log Groups".to_string(),
10693        });
10694
10695        app.save_current_session();
10696
10697        assert!(app.current_session.is_some());
10698        let session = app.current_session.clone().unwrap();
10699        assert_eq!(session.profile, "test-profile");
10700        assert_eq!(session.region, "us-east-1");
10701        assert_eq!(session.account_id, "123456789");
10702        assert_eq!(session.tabs.len(), 1);
10703
10704        // Cleanup
10705        let _ = session.delete();
10706    }
10707
10708    #[test]
10709    fn test_save_session_updates_existing() {
10710        let mut app =
10711            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10712        app.config.account_id = "123456789".to_string();
10713        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10714
10715        app.current_session = Some(Session {
10716            id: "existing".to_string(),
10717            timestamp: "2024-01-01".to_string(),
10718            profile: "test-profile".to_string(),
10719            region: "us-east-1".to_string(),
10720            account_id: "123456789".to_string(),
10721            role_arn: "arn:aws:iam::123456789:role/test".to_string(),
10722            tabs: vec![],
10723        });
10724
10725        app.tabs.push(Tab {
10726            service: Service::CloudWatchLogGroups,
10727            title: "Log Groups".to_string(),
10728            breadcrumb: "CloudWatch › Log Groups".to_string(),
10729        });
10730
10731        app.save_current_session();
10732
10733        let session = app.current_session.clone().unwrap();
10734        assert_eq!(session.id, "existing");
10735        assert_eq!(session.tabs.len(), 1);
10736
10737        // Cleanup
10738        let _ = session.delete();
10739    }
10740
10741    #[test]
10742    fn test_save_session_skips_empty_tabs() {
10743        let mut app =
10744            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10745        app.config.account_id = "123456789".to_string();
10746
10747        app.save_current_session();
10748
10749        assert!(app.current_session.is_none());
10750    }
10751
10752    #[test]
10753    fn test_save_session_deletes_when_tabs_closed() {
10754        let mut app =
10755            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10756        app.config.account_id = "123456789".to_string();
10757        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10758
10759        // Create a session with tabs
10760        app.current_session = Some(Session {
10761            id: "test_delete".to_string(),
10762            timestamp: "2024-01-01 10:00:00 UTC".to_string(),
10763            profile: "test-profile".to_string(),
10764            region: "us-east-1".to_string(),
10765            account_id: "123456789".to_string(),
10766            role_arn: "arn:aws:iam::123456789:role/test".to_string(),
10767            tabs: vec![],
10768        });
10769
10770        // Save with no tabs should delete session
10771        app.save_current_session();
10772
10773        assert!(app.current_session.is_none());
10774    }
10775
10776    #[test]
10777    fn test_closing_all_tabs_deletes_session() {
10778        let mut app =
10779            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
10780        app.config.account_id = "123456789".to_string();
10781        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
10782
10783        // Add a tab
10784        app.tabs.push(Tab {
10785            service: Service::CloudWatchLogGroups,
10786            title: "Log Groups".to_string(),
10787            breadcrumb: "CloudWatch › Log Groups".to_string(),
10788        });
10789
10790        // Create session
10791        app.save_current_session();
10792        assert!(app.current_session.is_some());
10793        let session_id = app.current_session.as_ref().unwrap().id.clone();
10794
10795        // Close all tabs
10796        app.tabs.clear();
10797
10798        // Save should delete session
10799        app.save_current_session();
10800        assert!(app.current_session.is_none());
10801
10802        // Cleanup - ensure session file is deleted
10803        let _ = Session::load(&session_id).map(|s| s.delete());
10804    }
10805
10806    #[test]
10807    fn test_credential_error_opens_profile_picker() {
10808        // Simulate what main.rs does on credential error
10809        let mut app = App::new_without_client("default".to_string(), None);
10810        let error_str = "Unable to load credentials from any source";
10811
10812        if error_str.contains("credentials") {
10813            app.available_profiles = App::load_aws_profiles();
10814            app.mode = Mode::ProfilePicker;
10815        }
10816
10817        assert_eq!(app.mode, Mode::ProfilePicker);
10818        // Should have loaded profiles
10819        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
10820    }
10821
10822    #[test]
10823    fn test_non_credential_error_shows_error_modal() {
10824        let mut app = App::new_without_client("default".to_string(), None);
10825        let error_str = "Network timeout";
10826
10827        if !error_str.contains("credentials") {
10828            app.error_message = Some(error_str.to_string());
10829            app.mode = Mode::ErrorModal;
10830        }
10831
10832        assert_eq!(app.mode, Mode::ErrorModal);
10833        assert!(app.error_message.is_some());
10834    }
10835
10836    #[tokio::test]
10837    async fn test_profile_selection_loads_credentials() {
10838        // Set a valid AWS profile if available
10839        std::env::set_var("AWS_PROFILE", "default");
10840
10841        // Try to create app with profile
10842        let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
10843
10844        if let Ok(app) = result {
10845            // If credentials are available, verify they're loaded
10846            assert!(!app.config.account_id.is_empty());
10847            assert!(!app.config.role_arn.is_empty());
10848            assert_eq!(app.profile, "default");
10849            assert_eq!(app.config.region, "us-east-1");
10850        }
10851        // If no credentials, test passes (can't test without real AWS creds)
10852    }
10853
10854    #[test]
10855    fn test_new_app_shows_service_picker_with_no_tabs() {
10856        let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
10857
10858        // Should start with no service selected
10859        assert!(!app.service_selected);
10860        // Should be in ServicePicker mode (service picker)
10861        assert_eq!(app.mode, Mode::ServicePicker);
10862        // Should have no tabs
10863        assert!(app.tabs.is_empty());
10864    }
10865
10866    #[tokio::test]
10867    async fn test_aws_profile_env_var_read_before_config_load() {
10868        // This test verifies the bug: AWS_PROFILE should be read and used
10869        std::env::set_var("AWS_PROFILE", "test-profile");
10870
10871        // Simulate what happens in App::new
10872        let profile_name = None
10873            .or_else(|| std::env::var("AWS_PROFILE").ok())
10874            .unwrap_or_else(|| "default".to_string());
10875
10876        // Should have read test-profile from env
10877        assert_eq!(profile_name, "test-profile");
10878
10879        // Now set it (redundant but that's what the code does)
10880        std::env::set_var("AWS_PROFILE", &profile_name);
10881
10882        // Verify it's still set
10883        assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
10884
10885        std::env::remove_var("AWS_PROFILE");
10886    }
10887
10888    #[test]
10889    fn test_next_preferences_cloudformation() {
10890        let mut app = test_app();
10891        app.current_service = Service::CloudFormationStacks;
10892        app.mode = Mode::ColumnSelector;
10893        app.column_selector_index = 0;
10894
10895        // Should jump to PageSize section
10896        let page_size_idx = app.cfn_column_ids.len() + 2;
10897        app.handle_action(Action::NextPreferences);
10898        assert_eq!(app.column_selector_index, page_size_idx);
10899
10900        // Should wrap back to Columns
10901        app.handle_action(Action::NextPreferences);
10902        assert_eq!(app.column_selector_index, 0);
10903    }
10904
10905    #[test]
10906    fn test_s3_preferences_tab_cycling() {
10907        let mut app = test_app();
10908        app.current_service = Service::S3Buckets;
10909        app.mode = Mode::ColumnSelector;
10910        app.column_selector_index = 0;
10911
10912        let page_size_idx = app.s3_bucket_column_ids.len() + 2;
10913
10914        // Tab should jump to PageSize
10915        app.handle_action(Action::NextPreferences);
10916        assert_eq!(app.column_selector_index, page_size_idx);
10917
10918        // Tab should wrap back to Columns
10919        app.handle_action(Action::NextPreferences);
10920        assert_eq!(app.column_selector_index, 0);
10921
10922        // Shift+Tab should jump to PageSize
10923        app.handle_action(Action::PrevPreferences);
10924        assert_eq!(app.column_selector_index, page_size_idx);
10925
10926        // Shift+Tab should wrap back to Columns
10927        app.handle_action(Action::PrevPreferences);
10928        assert_eq!(app.column_selector_index, 0);
10929    }
10930
10931    #[test]
10932    fn test_s3_filter_resets_selection() {
10933        let mut app = test_app();
10934        app.current_service = Service::S3Buckets;
10935        app.service_selected = true;
10936
10937        // Add some buckets
10938        app.s3_state.buckets.items = vec![
10939            S3Bucket {
10940                name: "bucket-1".to_string(),
10941                region: "us-east-1".to_string(),
10942                creation_date: "2023-01-01".to_string(),
10943            },
10944            S3Bucket {
10945                name: "bucket-2".to_string(),
10946                region: "us-east-1".to_string(),
10947                creation_date: "2023-01-02".to_string(),
10948            },
10949            S3Bucket {
10950                name: "other-bucket".to_string(),
10951                region: "us-east-1".to_string(),
10952                creation_date: "2023-01-03".to_string(),
10953            },
10954        ];
10955
10956        // Navigate to second bucket
10957        app.s3_state.selected_row = 1;
10958        app.s3_state.bucket_scroll_offset = 1;
10959
10960        // Apply filter
10961        app.mode = Mode::FilterInput;
10962        app.apply_filter_operation(|f| f.push_str("bucket-"));
10963
10964        // Selection should be reset
10965        assert_eq!(app.s3_state.selected_row, 0);
10966        assert_eq!(app.s3_state.bucket_scroll_offset, 0);
10967        assert_eq!(app.s3_state.buckets.filter, "bucket-");
10968    }
10969
10970    #[test]
10971    fn test_s3_navigation_respects_filter() {
10972        let mut app = test_app();
10973        app.current_service = Service::S3Buckets;
10974        app.service_selected = true;
10975        app.mode = Mode::Normal;
10976
10977        // Add buckets
10978        app.s3_state.buckets.items = vec![
10979            S3Bucket {
10980                name: "prod-bucket".to_string(),
10981                region: "us-east-1".to_string(),
10982                creation_date: "2023-01-01".to_string(),
10983            },
10984            S3Bucket {
10985                name: "dev-bucket".to_string(),
10986                region: "us-east-1".to_string(),
10987                creation_date: "2023-01-02".to_string(),
10988            },
10989            S3Bucket {
10990                name: "prod-logs".to_string(),
10991                region: "us-east-1".to_string(),
10992                creation_date: "2023-01-03".to_string(),
10993            },
10994        ];
10995
10996        // Filter to only "prod" buckets (2 results)
10997        app.s3_state.buckets.filter = "prod".to_string();
10998
10999        // Should start at 0
11000        assert_eq!(app.s3_state.selected_row, 0);
11001
11002        // Navigate down - should go to row 1 (prod-logs)
11003        app.handle_action(Action::NextItem);
11004        assert_eq!(app.s3_state.selected_row, 1);
11005
11006        // Navigate down again - should stay at 1 (max for 2 filtered results)
11007        app.handle_action(Action::NextItem);
11008        assert_eq!(app.s3_state.selected_row, 1);
11009
11010        // Navigate up - should go back to 0
11011        app.handle_action(Action::PrevItem);
11012        assert_eq!(app.s3_state.selected_row, 0);
11013    }
11014
11015    #[test]
11016    fn test_next_preferences_lambda_functions() {
11017        let mut app = test_app();
11018        app.current_service = Service::LambdaFunctions;
11019        app.mode = Mode::ColumnSelector;
11020        app.column_selector_index = 0;
11021
11022        let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
11023        app.handle_action(Action::NextPreferences);
11024        assert_eq!(app.column_selector_index, page_size_idx);
11025
11026        app.handle_action(Action::NextPreferences);
11027        assert_eq!(app.column_selector_index, 0);
11028    }
11029
11030    #[test]
11031    fn test_next_preferences_lambda_applications() {
11032        let mut app = test_app();
11033        app.current_service = Service::LambdaApplications;
11034        app.mode = Mode::ColumnSelector;
11035        app.column_selector_index = 0;
11036
11037        let page_size_idx = app.lambda_application_column_ids.len() + 2;
11038        app.handle_action(Action::NextPreferences);
11039        assert_eq!(app.column_selector_index, page_size_idx);
11040
11041        app.handle_action(Action::NextPreferences);
11042        assert_eq!(app.column_selector_index, 0);
11043    }
11044
11045    #[test]
11046    fn test_next_preferences_ecr_images() {
11047        let mut app = test_app();
11048        app.current_service = Service::EcrRepositories;
11049        app.ecr_state.current_repository = Some("test-repo".to_string());
11050        app.mode = Mode::ColumnSelector;
11051        app.column_selector_index = 0;
11052
11053        let page_size_idx = app.ecr_image_column_ids.len() + 2;
11054        app.handle_action(Action::NextPreferences);
11055        assert_eq!(app.column_selector_index, page_size_idx);
11056
11057        app.handle_action(Action::NextPreferences);
11058        assert_eq!(app.column_selector_index, 0);
11059    }
11060
11061    #[test]
11062    fn test_next_preferences_cloudwatch_log_groups() {
11063        let mut app = test_app();
11064        app.current_service = Service::CloudWatchLogGroups;
11065        app.view_mode = ViewMode::List;
11066        app.mode = Mode::ColumnSelector;
11067        app.column_selector_index = 0;
11068
11069        // Tab from Columns to PageSize
11070        let page_size_idx = app.cw_log_group_column_ids.len() + 2;
11071        app.handle_action(Action::NextPreferences);
11072        assert_eq!(app.column_selector_index, page_size_idx);
11073
11074        // Tab from PageSize back to Columns
11075        app.handle_action(Action::NextPreferences);
11076        assert_eq!(app.column_selector_index, 0);
11077
11078        // Shift+Tab from Columns to PageSize
11079        app.handle_action(Action::PrevPreferences);
11080        assert_eq!(app.column_selector_index, page_size_idx);
11081
11082        // Shift+Tab from PageSize back to Columns
11083        app.handle_action(Action::PrevPreferences);
11084        assert_eq!(app.column_selector_index, 0);
11085    }
11086
11087    #[test]
11088    fn test_next_preferences_cloudwatch_log_streams() {
11089        let mut app = test_app();
11090        app.current_service = Service::CloudWatchLogGroups;
11091        app.view_mode = ViewMode::Detail;
11092        app.mode = Mode::ColumnSelector;
11093        app.column_selector_index = 0;
11094
11095        // Tab from Columns to PageSize
11096        let page_size_idx = app.cw_log_stream_column_ids.len() + 2;
11097        app.handle_action(Action::NextPreferences);
11098        assert_eq!(app.column_selector_index, page_size_idx);
11099
11100        // Tab from PageSize back to Columns
11101        app.handle_action(Action::NextPreferences);
11102        assert_eq!(app.column_selector_index, 0);
11103    }
11104
11105    #[test]
11106    fn test_cloudformation_next_item() {
11107        let mut app = test_app();
11108        app.current_service = Service::CloudFormationStacks;
11109        app.service_selected = true;
11110        app.mode = Mode::Normal;
11111        app.cfn_state.status_filter = CfnStatusFilter::Complete;
11112        app.cfn_state.table.items = vec![
11113            CfnStack {
11114                name: "stack1".to_string(),
11115                stack_id: "id1".to_string(),
11116                status: "CREATE_COMPLETE".to_string(),
11117                created_time: "2024-01-01".to_string(),
11118                updated_time: String::new(),
11119                deleted_time: String::new(),
11120                drift_status: String::new(),
11121                last_drift_check_time: String::new(),
11122                status_reason: String::new(),
11123                description: String::new(),
11124                detailed_status: String::new(),
11125                root_stack: String::new(),
11126                parent_stack: String::new(),
11127                termination_protection: false,
11128                iam_role: String::new(),
11129                tags: Vec::new(),
11130                stack_policy: String::new(),
11131                rollback_monitoring_time: String::new(),
11132                rollback_alarms: Vec::new(),
11133                notification_arns: Vec::new(),
11134            },
11135            CfnStack {
11136                name: "stack2".to_string(),
11137                stack_id: "id2".to_string(),
11138                status: "UPDATE_COMPLETE".to_string(),
11139                created_time: "2024-01-02".to_string(),
11140                updated_time: String::new(),
11141                deleted_time: String::new(),
11142                drift_status: String::new(),
11143                last_drift_check_time: String::new(),
11144                status_reason: String::new(),
11145                description: String::new(),
11146                detailed_status: String::new(),
11147                root_stack: String::new(),
11148                parent_stack: String::new(),
11149                termination_protection: false,
11150                iam_role: String::new(),
11151                tags: Vec::new(),
11152                stack_policy: String::new(),
11153                rollback_monitoring_time: String::new(),
11154                rollback_alarms: Vec::new(),
11155                notification_arns: Vec::new(),
11156            },
11157        ];
11158        app.cfn_state.table.reset();
11159
11160        app.handle_action(Action::NextItem);
11161        assert_eq!(app.cfn_state.table.selected, 1);
11162
11163        app.handle_action(Action::NextItem);
11164        assert_eq!(app.cfn_state.table.selected, 1); // At max
11165    }
11166
11167    #[test]
11168    fn test_cloudformation_prev_item() {
11169        let mut app = test_app();
11170        app.current_service = Service::CloudFormationStacks;
11171        app.service_selected = true;
11172        app.mode = Mode::Normal;
11173        app.cfn_state.status_filter = CfnStatusFilter::Complete;
11174        app.cfn_state.table.items = vec![
11175            CfnStack {
11176                name: "stack1".to_string(),
11177                stack_id: "id1".to_string(),
11178                status: "CREATE_COMPLETE".to_string(),
11179                created_time: "2024-01-01".to_string(),
11180                updated_time: String::new(),
11181                deleted_time: String::new(),
11182                drift_status: String::new(),
11183                last_drift_check_time: String::new(),
11184                status_reason: String::new(),
11185                description: String::new(),
11186                detailed_status: String::new(),
11187                root_stack: String::new(),
11188                parent_stack: String::new(),
11189                termination_protection: false,
11190                iam_role: String::new(),
11191                tags: Vec::new(),
11192                stack_policy: String::new(),
11193                rollback_monitoring_time: String::new(),
11194                rollback_alarms: Vec::new(),
11195                notification_arns: Vec::new(),
11196            },
11197            CfnStack {
11198                name: "stack2".to_string(),
11199                stack_id: "id2".to_string(),
11200                status: "UPDATE_COMPLETE".to_string(),
11201                created_time: "2024-01-02".to_string(),
11202                updated_time: String::new(),
11203                deleted_time: String::new(),
11204                drift_status: String::new(),
11205                last_drift_check_time: String::new(),
11206                status_reason: String::new(),
11207                description: String::new(),
11208                detailed_status: String::new(),
11209                root_stack: String::new(),
11210                parent_stack: String::new(),
11211                termination_protection: false,
11212                iam_role: String::new(),
11213                tags: Vec::new(),
11214                stack_policy: String::new(),
11215                rollback_monitoring_time: String::new(),
11216                rollback_alarms: Vec::new(),
11217                notification_arns: Vec::new(),
11218            },
11219        ];
11220        app.cfn_state.table.selected = 1;
11221
11222        app.handle_action(Action::PrevItem);
11223        assert_eq!(app.cfn_state.table.selected, 0);
11224
11225        app.handle_action(Action::PrevItem);
11226        assert_eq!(app.cfn_state.table.selected, 0); // At min
11227    }
11228
11229    #[test]
11230    fn test_cloudformation_page_down() {
11231        let mut app = test_app();
11232        app.current_service = Service::CloudFormationStacks;
11233        app.service_selected = true;
11234        app.mode = Mode::Normal;
11235        app.cfn_state.status_filter = CfnStatusFilter::Complete;
11236
11237        // Create 20 stacks
11238        for i in 0..20 {
11239            app.cfn_state.table.items.push(CfnStack {
11240                name: format!("stack{}", i),
11241                stack_id: format!("id{}", i),
11242                status: "CREATE_COMPLETE".to_string(),
11243                created_time: format!("2024-01-{:02}", i + 1),
11244                updated_time: String::new(),
11245                deleted_time: String::new(),
11246                drift_status: String::new(),
11247                last_drift_check_time: String::new(),
11248                status_reason: String::new(),
11249                description: String::new(),
11250                detailed_status: String::new(),
11251                root_stack: String::new(),
11252                parent_stack: String::new(),
11253                termination_protection: false,
11254                iam_role: String::new(),
11255                tags: Vec::new(),
11256                stack_policy: String::new(),
11257                rollback_monitoring_time: String::new(),
11258                rollback_alarms: Vec::new(),
11259                notification_arns: Vec::new(),
11260            });
11261        }
11262        app.cfn_state.table.reset();
11263
11264        app.handle_action(Action::PageDown);
11265        assert_eq!(app.cfn_state.table.selected, 10);
11266
11267        app.handle_action(Action::PageDown);
11268        assert_eq!(app.cfn_state.table.selected, 19); // Clamped to max
11269    }
11270
11271    #[test]
11272    fn test_cloudformation_page_up() {
11273        let mut app = test_app();
11274        app.current_service = Service::CloudFormationStacks;
11275        app.service_selected = true;
11276        app.mode = Mode::Normal;
11277        app.cfn_state.status_filter = CfnStatusFilter::Complete;
11278
11279        // Create 20 stacks
11280        for i in 0..20 {
11281            app.cfn_state.table.items.push(CfnStack {
11282                name: format!("stack{}", i),
11283                stack_id: format!("id{}", i),
11284                status: "CREATE_COMPLETE".to_string(),
11285                created_time: format!("2024-01-{:02}", i + 1),
11286                updated_time: String::new(),
11287                deleted_time: String::new(),
11288                drift_status: String::new(),
11289                last_drift_check_time: String::new(),
11290                status_reason: String::new(),
11291                description: String::new(),
11292                detailed_status: String::new(),
11293                root_stack: String::new(),
11294                parent_stack: String::new(),
11295                termination_protection: false,
11296                iam_role: String::new(),
11297                tags: Vec::new(),
11298                stack_policy: String::new(),
11299                rollback_monitoring_time: String::new(),
11300                rollback_alarms: Vec::new(),
11301                notification_arns: Vec::new(),
11302            });
11303        }
11304        app.cfn_state.table.selected = 15;
11305
11306        app.handle_action(Action::PageUp);
11307        assert_eq!(app.cfn_state.table.selected, 5);
11308
11309        app.handle_action(Action::PageUp);
11310        assert_eq!(app.cfn_state.table.selected, 0); // Clamped to min
11311    }
11312
11313    #[test]
11314    fn test_cloudformation_filter_input() {
11315        let mut app = test_app();
11316        app.current_service = Service::CloudFormationStacks;
11317        app.service_selected = true;
11318        app.mode = Mode::Normal;
11319
11320        app.handle_action(Action::StartFilter);
11321        assert_eq!(app.mode, Mode::FilterInput);
11322
11323        // Directly set filter (character input is handled in event loop, not actions)
11324        app.cfn_state.table.filter = "test".to_string();
11325        assert_eq!(app.cfn_state.table.filter, "test");
11326    }
11327
11328    #[test]
11329    fn test_cloudformation_filter_applies() {
11330        let mut app = test_app();
11331        app.current_service = Service::CloudFormationStacks;
11332        app.cfn_state.status_filter = CfnStatusFilter::Complete;
11333        app.cfn_state.table.items = vec![
11334            CfnStack {
11335                name: "prod-stack".to_string(),
11336                stack_id: "id1".to_string(),
11337                status: "CREATE_COMPLETE".to_string(),
11338                created_time: "2024-01-01".to_string(),
11339                updated_time: String::new(),
11340                deleted_time: String::new(),
11341                drift_status: String::new(),
11342                last_drift_check_time: String::new(),
11343                status_reason: String::new(),
11344                description: "Production stack".to_string(),
11345                detailed_status: String::new(),
11346                root_stack: String::new(),
11347                parent_stack: String::new(),
11348                termination_protection: false,
11349                iam_role: String::new(),
11350                tags: Vec::new(),
11351                stack_policy: String::new(),
11352                rollback_monitoring_time: String::new(),
11353                rollback_alarms: Vec::new(),
11354                notification_arns: Vec::new(),
11355            },
11356            CfnStack {
11357                name: "dev-stack".to_string(),
11358                stack_id: "id2".to_string(),
11359                status: "UPDATE_COMPLETE".to_string(),
11360                created_time: "2024-01-02".to_string(),
11361                updated_time: String::new(),
11362                deleted_time: String::new(),
11363                drift_status: String::new(),
11364                last_drift_check_time: String::new(),
11365                status_reason: String::new(),
11366                description: "Development stack".to_string(),
11367                detailed_status: String::new(),
11368                root_stack: String::new(),
11369                parent_stack: String::new(),
11370                termination_protection: false,
11371                iam_role: String::new(),
11372                tags: Vec::new(),
11373                stack_policy: String::new(),
11374                rollback_monitoring_time: String::new(),
11375                rollback_alarms: Vec::new(),
11376                notification_arns: Vec::new(),
11377            },
11378        ];
11379        app.cfn_state.table.filter = "prod".to_string();
11380
11381        let filtered = filtered_cloudformation_stacks(&app);
11382        assert_eq!(filtered.len(), 1);
11383        assert_eq!(filtered[0].name, "prod-stack");
11384    }
11385
11386    #[test]
11387    fn test_cloudformation_right_arrow_expands() {
11388        let mut app = test_app();
11389        app.current_service = Service::CloudFormationStacks;
11390        app.service_selected = true;
11391        app.cfn_state.status_filter = CfnStatusFilter::Complete;
11392        app.cfn_state.table.items = vec![CfnStack {
11393            name: "test-stack".to_string(),
11394            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11395                .to_string(),
11396            status: "CREATE_COMPLETE".to_string(),
11397            created_time: "2024-01-01".to_string(),
11398            updated_time: String::new(),
11399            deleted_time: String::new(),
11400            drift_status: String::new(),
11401            last_drift_check_time: String::new(),
11402            status_reason: String::new(),
11403            description: "Test stack".to_string(),
11404            detailed_status: String::new(),
11405            root_stack: String::new(),
11406            parent_stack: String::new(),
11407            termination_protection: false,
11408            iam_role: String::new(),
11409            tags: Vec::new(),
11410            stack_policy: String::new(),
11411            rollback_monitoring_time: String::new(),
11412            rollback_alarms: Vec::new(),
11413            notification_arns: Vec::new(),
11414        }];
11415        app.cfn_state.table.reset();
11416
11417        assert_eq!(app.cfn_state.table.expanded_item, None);
11418
11419        app.handle_action(Action::NextPane);
11420        assert_eq!(app.cfn_state.table.expanded_item, Some(0));
11421    }
11422
11423    #[test]
11424    fn test_cloudformation_left_arrow_collapses() {
11425        let mut app = test_app();
11426        app.current_service = Service::CloudFormationStacks;
11427        app.service_selected = true;
11428        app.cfn_state.status_filter = CfnStatusFilter::Complete;
11429        app.cfn_state.table.items = vec![CfnStack {
11430            name: "test-stack".to_string(),
11431            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11432                .to_string(),
11433            status: "CREATE_COMPLETE".to_string(),
11434            created_time: "2024-01-01".to_string(),
11435            updated_time: String::new(),
11436            deleted_time: String::new(),
11437            drift_status: String::new(),
11438            last_drift_check_time: String::new(),
11439            status_reason: String::new(),
11440            description: "Test stack".to_string(),
11441            detailed_status: String::new(),
11442            root_stack: String::new(),
11443            parent_stack: String::new(),
11444            termination_protection: false,
11445            iam_role: String::new(),
11446            tags: Vec::new(),
11447            stack_policy: String::new(),
11448            rollback_monitoring_time: String::new(),
11449            rollback_alarms: Vec::new(),
11450            notification_arns: Vec::new(),
11451        }];
11452        app.cfn_state.table.reset();
11453        app.cfn_state.table.expanded_item = Some(0);
11454
11455        app.handle_action(Action::PrevPane);
11456        assert_eq!(app.cfn_state.table.expanded_item, None);
11457    }
11458
11459    #[test]
11460    fn test_cloudformation_enter_drills_into_stack() {
11461        let mut app = test_app();
11462        app.current_service = Service::CloudFormationStacks;
11463        app.service_selected = true;
11464        app.mode = Mode::Normal;
11465        app.tabs = vec![Tab {
11466            service: Service::CloudFormationStacks,
11467            title: "CloudFormation › Stacks".to_string(),
11468            breadcrumb: "CloudFormation › Stacks".to_string(),
11469        }];
11470        app.current_tab = 0;
11471        app.cfn_state.status_filter = CfnStatusFilter::Complete;
11472        app.cfn_state.table.items = vec![CfnStack {
11473            name: "test-stack".to_string(),
11474            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11475                .to_string(),
11476            status: "CREATE_COMPLETE".to_string(),
11477            created_time: "2024-01-01".to_string(),
11478            updated_time: String::new(),
11479            deleted_time: String::new(),
11480            drift_status: String::new(),
11481            last_drift_check_time: String::new(),
11482            status_reason: String::new(),
11483            description: "Test stack".to_string(),
11484            detailed_status: String::new(),
11485            root_stack: String::new(),
11486            parent_stack: String::new(),
11487            termination_protection: false,
11488            iam_role: String::new(),
11489            tags: Vec::new(),
11490            stack_policy: String::new(),
11491            rollback_monitoring_time: String::new(),
11492            rollback_alarms: Vec::new(),
11493            notification_arns: Vec::new(),
11494        }];
11495        app.cfn_state.table.reset();
11496
11497        // Verify filtering works
11498        let filtered = filtered_cloudformation_stacks(&app);
11499        assert_eq!(filtered.len(), 1);
11500        assert_eq!(filtered[0].name, "test-stack");
11501
11502        assert_eq!(app.cfn_state.current_stack, None);
11503
11504        // Enter drills into stack detail view
11505        app.handle_action(Action::Select);
11506        assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
11507    }
11508
11509    #[test]
11510    fn test_cloudformation_copy_to_clipboard() {
11511        let mut app = test_app();
11512        app.current_service = Service::CloudFormationStacks;
11513        app.service_selected = true;
11514        app.mode = Mode::Normal;
11515        app.cfn_state.status_filter = CfnStatusFilter::Complete;
11516        app.cfn_state.table.items = vec![
11517            CfnStack {
11518                name: "stack1".to_string(),
11519                stack_id: "id1".to_string(),
11520                status: "CREATE_COMPLETE".to_string(),
11521                created_time: "2024-01-01".to_string(),
11522                updated_time: String::new(),
11523                deleted_time: String::new(),
11524                drift_status: String::new(),
11525                last_drift_check_time: String::new(),
11526                status_reason: String::new(),
11527                description: String::new(),
11528                detailed_status: String::new(),
11529                root_stack: String::new(),
11530                parent_stack: String::new(),
11531                termination_protection: false,
11532                iam_role: String::new(),
11533                tags: Vec::new(),
11534                stack_policy: String::new(),
11535                rollback_monitoring_time: String::new(),
11536                rollback_alarms: Vec::new(),
11537                notification_arns: Vec::new(),
11538            },
11539            CfnStack {
11540                name: "stack2".to_string(),
11541                stack_id: "id2".to_string(),
11542                status: "UPDATE_COMPLETE".to_string(),
11543                created_time: "2024-01-02".to_string(),
11544                updated_time: String::new(),
11545                deleted_time: String::new(),
11546                drift_status: String::new(),
11547                last_drift_check_time: String::new(),
11548                status_reason: String::new(),
11549                description: String::new(),
11550                detailed_status: String::new(),
11551                root_stack: String::new(),
11552                parent_stack: String::new(),
11553                termination_protection: false,
11554                iam_role: String::new(),
11555                tags: Vec::new(),
11556                stack_policy: String::new(),
11557                rollback_monitoring_time: String::new(),
11558                rollback_alarms: Vec::new(),
11559                notification_arns: Vec::new(),
11560            },
11561        ];
11562
11563        assert!(!app.snapshot_requested);
11564        app.handle_action(Action::CopyToClipboard);
11565
11566        // Should set snapshot_requested flag
11567        assert!(app.snapshot_requested);
11568    }
11569
11570    #[test]
11571    fn test_cloudformation_expansion_shows_all_visible_columns() {
11572        let mut app = test_app();
11573        app.current_service = Service::CloudFormationStacks;
11574        app.cfn_state.status_filter = CfnStatusFilter::Complete;
11575        app.cfn_state.table.items = vec![CfnStack {
11576            name: "test-stack".to_string(),
11577            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
11578                .to_string(),
11579            status: "CREATE_COMPLETE".to_string(),
11580            created_time: "2024-01-01".to_string(),
11581            updated_time: "2024-01-02".to_string(),
11582            deleted_time: String::new(),
11583            drift_status: "IN_SYNC".to_string(),
11584            last_drift_check_time: "2024-01-03".to_string(),
11585            status_reason: String::new(),
11586            description: "Test description".to_string(),
11587            detailed_status: String::new(),
11588            root_stack: String::new(),
11589            parent_stack: String::new(),
11590            termination_protection: false,
11591            iam_role: String::new(),
11592            tags: Vec::new(),
11593            stack_policy: String::new(),
11594            rollback_monitoring_time: String::new(),
11595            rollback_alarms: Vec::new(),
11596            notification_arns: Vec::new(),
11597        }];
11598
11599        // Set visible columns
11600        app.cfn_visible_column_ids = [
11601            CfnColumn::Name,
11602            CfnColumn::Status,
11603            CfnColumn::CreatedTime,
11604            CfnColumn::Description,
11605        ]
11606        .iter()
11607        .map(|c| c.id())
11608        .collect();
11609
11610        app.cfn_state.table.expanded_item = Some(0);
11611
11612        // Verify all visible columns would be shown in expansion
11613        // (This is a structural test - actual rendering is in UI layer)
11614        assert_eq!(app.cfn_visible_column_ids.len(), 4);
11615        assert!(app.cfn_state.table.has_expanded_item());
11616    }
11617
11618    #[test]
11619    fn test_cloudformation_empty_list_shows_page_1() {
11620        let mut app = test_app();
11621        app.current_service = Service::CloudFormationStacks;
11622        app.cfn_state.table.items = vec![];
11623
11624        let filtered = filtered_cloudformation_stacks(&app);
11625        assert_eq!(filtered.len(), 0);
11626
11627        // Pagination should still show [1] even with 0 items
11628        let page_size = app.cfn_state.table.page_size.value();
11629        let total_pages = filtered.len().div_ceil(page_size);
11630        assert_eq!(total_pages, 0);
11631
11632        // render_pagination_text(0, 0) should return "[1]"
11633        // This is tested in UI layer
11634    }
11635}
11636
11637impl App {
11638    pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
11639        let mut all = AwsRegion::all();
11640
11641        // Add latencies to regions
11642        for region in &mut all {
11643            region.latency_ms = self.region_latencies.get(region.code).copied();
11644        }
11645
11646        // Filter by search term
11647        let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
11648            all
11649        } else {
11650            let filter_lower = self.region_filter.to_lowercase();
11651            all.into_iter()
11652                .filter(|r| {
11653                    r.name.to_lowercase().contains(&filter_lower)
11654                        || r.code.to_lowercase().contains(&filter_lower)
11655                        || r.group.to_lowercase().contains(&filter_lower)
11656                })
11657                .collect()
11658        };
11659
11660        // Sort by latency (lowest first), treat None as 1000ms
11661        let mut sorted = filtered;
11662        sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
11663        sorted
11664    }
11665
11666    pub fn measure_region_latencies(&mut self) {
11667        use std::time::Instant;
11668        self.region_latencies.clear();
11669
11670        let regions = AwsRegion::all();
11671        let start_all = Instant::now();
11672        tracing::info!("Starting latency measurement for {} regions", regions.len());
11673
11674        let handles: Vec<_> = regions
11675            .iter()
11676            .map(|region| {
11677                let code = region.code.to_string();
11678                std::thread::spawn(move || {
11679                    // Use STS endpoint - fastest and most reliable
11680                    let endpoint = format!("https://sts.{}.amazonaws.com", code);
11681                    let start = Instant::now();
11682
11683                    match ureq::get(&endpoint)
11684                        .timeout(std::time::Duration::from_secs(2))
11685                        .call()
11686                    {
11687                        Ok(_) => {
11688                            let latency = start.elapsed().as_millis() as u64;
11689                            Some((code, latency))
11690                        }
11691                        Err(e) => {
11692                            tracing::debug!("Failed to measure {}: {}", code, e);
11693                            Some((code, 9999))
11694                        }
11695                    }
11696                })
11697            })
11698            .collect();
11699
11700        for handle in handles {
11701            if let Ok(Some((code, latency))) = handle.join() {
11702                self.region_latencies.insert(code, latency);
11703            }
11704        }
11705
11706        tracing::info!(
11707            "Measured {} regions in {:?}",
11708            self.region_latencies.len(),
11709            start_all.elapsed()
11710        );
11711    }
11712
11713    pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
11714        filter_profiles(&self.available_profiles, &self.profile_filter)
11715    }
11716
11717    pub fn get_filtered_sessions(&self) -> Vec<&Session> {
11718        if self.session_filter.is_empty() {
11719            return self.sessions.iter().collect();
11720        }
11721        let filter_lower = self.session_filter.to_lowercase();
11722        self.sessions
11723            .iter()
11724            .filter(|s| {
11725                s.profile.to_lowercase().contains(&filter_lower)
11726                    || s.region.to_lowercase().contains(&filter_lower)
11727                    || s.account_id.to_lowercase().contains(&filter_lower)
11728                    || s.role_arn.to_lowercase().contains(&filter_lower)
11729            })
11730            .collect()
11731    }
11732
11733    pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
11734        if self.tab_filter.is_empty() {
11735            return self.tabs.iter().enumerate().collect();
11736        }
11737        let filter_lower = self.tab_filter.to_lowercase();
11738        self.tabs
11739            .iter()
11740            .enumerate()
11741            .filter(|(_, tab)| {
11742                tab.title.to_lowercase().contains(&filter_lower)
11743                    || tab.breadcrumb.to_lowercase().contains(&filter_lower)
11744            })
11745            .collect()
11746    }
11747
11748    pub fn load_aws_profiles() -> Vec<AwsProfile> {
11749        AwsProfile::load_all()
11750    }
11751
11752    pub async fn fetch_profile_accounts(&mut self) {
11753        for profile in &mut self.available_profiles {
11754            if profile.account.is_none() {
11755                let region = profile
11756                    .region
11757                    .clone()
11758                    .unwrap_or_else(|| "us-east-1".to_string());
11759                if let Ok(account) =
11760                    rusticity_core::AwsConfig::get_account_for_profile(&profile.name, &region).await
11761                {
11762                    profile.account = Some(account);
11763                }
11764            }
11765        }
11766    }
11767
11768    fn save_current_session(&mut self) {
11769        // If no tabs, delete the session if it exists
11770        if self.tabs.is_empty() {
11771            if let Some(ref session) = self.current_session {
11772                let _ = session.delete();
11773                self.current_session = None;
11774            }
11775            return;
11776        }
11777
11778        let session = if let Some(ref mut current) = self.current_session {
11779            // Update existing session
11780            current.tabs = self
11781                .tabs
11782                .iter()
11783                .map(|t| SessionTab {
11784                    service: format!("{:?}", t.service),
11785                    title: t.title.clone(),
11786                    breadcrumb: t.breadcrumb.clone(),
11787                    filter: match t.service {
11788                        Service::CloudWatchLogGroups => {
11789                            Some(self.log_groups_state.log_groups.filter.clone())
11790                        }
11791                        _ => None,
11792                    },
11793                    selected_item: None,
11794                })
11795                .collect();
11796            current.clone()
11797        } else {
11798            // Create new session
11799            let mut session = Session::new(
11800                self.profile.clone(),
11801                self.region.clone(),
11802                self.config.account_id.clone(),
11803                self.config.role_arn.clone(),
11804            );
11805            session.tabs = self
11806                .tabs
11807                .iter()
11808                .map(|t| SessionTab {
11809                    service: format!("{:?}", t.service),
11810                    title: t.title.clone(),
11811                    breadcrumb: t.breadcrumb.clone(),
11812                    filter: match t.service {
11813                        Service::CloudWatchLogGroups => {
11814                            Some(self.log_groups_state.log_groups.filter.clone())
11815                        }
11816                        _ => None,
11817                    },
11818                    selected_item: None,
11819                })
11820                .collect();
11821            self.current_session = Some(session.clone());
11822            session
11823        };
11824
11825        let _ = session.save();
11826    }
11827}
11828
11829#[cfg(test)]
11830mod iam_policy_view_tests {
11831    use super::*;
11832    use test_helpers::*;
11833
11834    #[test]
11835    fn test_enter_opens_policy_view() {
11836        let mut app = test_app();
11837        app.current_service = Service::IamRoles;
11838        app.service_selected = true;
11839        app.mode = Mode::Normal;
11840        app.view_mode = ViewMode::Detail;
11841        app.iam_state.current_role = Some("TestRole".to_string());
11842        app.iam_state.policies.items = vec![IamPolicy {
11843            policy_name: "TestPolicy".to_string(),
11844            policy_type: "Inline".to_string(),
11845            attached_via: "Direct".to_string(),
11846            attached_entities: "1".to_string(),
11847            description: "Test".to_string(),
11848            creation_time: "2023-01-01".to_string(),
11849            edited_time: "2023-01-01".to_string(),
11850            policy_arn: None,
11851        }];
11852        app.iam_state.policies.reset();
11853
11854        app.handle_action(Action::Select);
11855
11856        assert_eq!(app.view_mode, ViewMode::PolicyView);
11857        assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
11858        assert_eq!(app.iam_state.policy_scroll, 0);
11859        assert!(app.iam_state.policies.loading);
11860    }
11861
11862    #[test]
11863    fn test_escape_closes_policy_view() {
11864        let mut app = test_app();
11865        app.current_service = Service::IamRoles;
11866        app.service_selected = true;
11867        app.mode = Mode::Normal;
11868        app.view_mode = ViewMode::PolicyView;
11869        app.iam_state.current_role = Some("TestRole".to_string());
11870        app.iam_state.current_policy = Some("TestPolicy".to_string());
11871        app.iam_state.policy_document = "{\n  \"test\": \"value\"\n}".to_string();
11872        app.iam_state.policy_scroll = 5;
11873
11874        app.handle_action(Action::PrevPane);
11875
11876        assert_eq!(app.view_mode, ViewMode::Detail);
11877        assert_eq!(app.iam_state.current_policy, None);
11878        assert_eq!(app.iam_state.policy_document, "");
11879        assert_eq!(app.iam_state.policy_scroll, 0);
11880    }
11881
11882    #[test]
11883    fn test_ctrl_d_scrolls_down_in_policy_view() {
11884        let mut app = test_app();
11885        app.current_service = Service::IamRoles;
11886        app.service_selected = true;
11887        app.mode = Mode::Normal;
11888        app.view_mode = ViewMode::PolicyView;
11889        app.iam_state.current_role = Some("TestRole".to_string());
11890        app.iam_state.current_policy = Some("TestPolicy".to_string());
11891        app.iam_state.policy_document = (0..100)
11892            .map(|i| format!("line {}", i))
11893            .collect::<Vec<_>>()
11894            .join("\n");
11895        app.iam_state.policy_scroll = 0;
11896
11897        app.handle_action(Action::ScrollDown);
11898
11899        assert_eq!(app.iam_state.policy_scroll, 10);
11900
11901        app.handle_action(Action::ScrollDown);
11902
11903        assert_eq!(app.iam_state.policy_scroll, 20);
11904    }
11905
11906    #[test]
11907    fn test_ctrl_u_scrolls_up_in_policy_view() {
11908        let mut app = test_app();
11909        app.current_service = Service::IamRoles;
11910        app.service_selected = true;
11911        app.mode = Mode::Normal;
11912        app.view_mode = ViewMode::PolicyView;
11913        app.iam_state.current_role = Some("TestRole".to_string());
11914        app.iam_state.current_policy = Some("TestPolicy".to_string());
11915        app.iam_state.policy_document = (0..100)
11916            .map(|i| format!("line {}", i))
11917            .collect::<Vec<_>>()
11918            .join("\n");
11919        app.iam_state.policy_scroll = 30;
11920
11921        app.handle_action(Action::ScrollUp);
11922
11923        assert_eq!(app.iam_state.policy_scroll, 20);
11924
11925        app.handle_action(Action::ScrollUp);
11926
11927        assert_eq!(app.iam_state.policy_scroll, 10);
11928    }
11929
11930    #[test]
11931    fn test_scroll_does_not_go_negative() {
11932        let mut app = test_app();
11933        app.current_service = Service::IamRoles;
11934        app.service_selected = true;
11935        app.mode = Mode::Normal;
11936        app.view_mode = ViewMode::PolicyView;
11937        app.iam_state.current_role = Some("TestRole".to_string());
11938        app.iam_state.current_policy = Some("TestPolicy".to_string());
11939        app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
11940        app.iam_state.policy_scroll = 0;
11941
11942        app.handle_action(Action::ScrollUp);
11943
11944        assert_eq!(app.iam_state.policy_scroll, 0);
11945    }
11946
11947    #[test]
11948    fn test_scroll_does_not_exceed_max() {
11949        let mut app = test_app();
11950        app.current_service = Service::IamRoles;
11951        app.service_selected = true;
11952        app.mode = Mode::Normal;
11953        app.view_mode = ViewMode::PolicyView;
11954        app.iam_state.current_role = Some("TestRole".to_string());
11955        app.iam_state.current_policy = Some("TestPolicy".to_string());
11956        app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
11957        app.iam_state.policy_scroll = 0;
11958
11959        app.handle_action(Action::ScrollDown);
11960
11961        assert_eq!(app.iam_state.policy_scroll, 2); // Max is 2 (3 lines - 1)
11962    }
11963
11964    #[test]
11965    fn test_policy_view_console_url() {
11966        let mut app = test_app();
11967        app.current_service = Service::IamRoles;
11968        app.service_selected = true;
11969        app.view_mode = ViewMode::PolicyView;
11970        app.iam_state.current_role = Some("TestRole".to_string());
11971        app.iam_state.current_policy = Some("TestPolicy".to_string());
11972
11973        let url = app.get_console_url();
11974
11975        assert!(url.contains("us-east-1.console.aws.amazon.com"));
11976        assert!(url.contains("/roles/details/TestRole"));
11977        assert!(url.contains("/editPolicy/TestPolicy"));
11978        assert!(url.contains("step=addPermissions"));
11979    }
11980
11981    #[test]
11982    fn test_esc_from_policy_view_goes_to_role_detail() {
11983        let mut app = test_app();
11984        app.current_service = Service::IamRoles;
11985        app.service_selected = true;
11986        app.mode = Mode::Normal;
11987        app.view_mode = ViewMode::PolicyView;
11988        app.iam_state.current_role = Some("TestRole".to_string());
11989        app.iam_state.current_policy = Some("TestPolicy".to_string());
11990        app.iam_state.policy_document = "test".to_string();
11991        app.iam_state.policy_scroll = 5;
11992
11993        app.handle_action(Action::GoBack);
11994
11995        assert_eq!(app.view_mode, ViewMode::Detail);
11996        assert_eq!(app.iam_state.current_policy, None);
11997        assert_eq!(app.iam_state.policy_document, "");
11998        assert_eq!(app.iam_state.policy_scroll, 0);
11999        assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
12000    }
12001
12002    #[test]
12003    fn test_esc_from_role_detail_goes_to_role_list() {
12004        let mut app = test_app();
12005        app.current_service = Service::IamRoles;
12006        app.service_selected = true;
12007        app.mode = Mode::Normal;
12008        app.view_mode = ViewMode::Detail;
12009        app.iam_state.current_role = Some("TestRole".to_string());
12010
12011        app.handle_action(Action::GoBack);
12012
12013        assert_eq!(app.iam_state.current_role, None);
12014    }
12015
12016    #[test]
12017    fn test_right_arrow_expands_policy_row() {
12018        let mut app = test_app();
12019        app.current_service = Service::IamRoles;
12020        app.service_selected = true;
12021        app.mode = Mode::Normal;
12022        app.view_mode = ViewMode::Detail;
12023        app.iam_state.current_role = Some("TestRole".to_string());
12024        app.iam_state.policies.items = vec![IamPolicy {
12025            policy_name: "TestPolicy".to_string(),
12026            policy_type: "Inline".to_string(),
12027            attached_via: "Direct".to_string(),
12028            attached_entities: "1".to_string(),
12029            description: "Test".to_string(),
12030            creation_time: "2023-01-01".to_string(),
12031            edited_time: "2023-01-01".to_string(),
12032            policy_arn: None,
12033        }];
12034        app.iam_state.policies.reset();
12035
12036        app.handle_action(Action::NextPane);
12037
12038        // Should expand, not drill down
12039        assert_eq!(app.view_mode, ViewMode::Detail);
12040        assert_eq!(app.iam_state.current_policy, None);
12041        assert_eq!(app.iam_state.policies.expanded_item, Some(0));
12042    }
12043}
12044
12045#[cfg(test)]
12046mod tab_filter_tests {
12047    use super::*;
12048    use test_helpers::*;
12049
12050    #[test]
12051    fn test_space_t_opens_tab_picker() {
12052        let mut app = test_app();
12053        app.tabs = vec![
12054            Tab {
12055                service: Service::CloudWatchLogGroups,
12056                title: "Tab 1".to_string(),
12057                breadcrumb: "CloudWatch > Log groups".to_string(),
12058            },
12059            Tab {
12060                service: Service::S3Buckets,
12061                title: "Tab 2".to_string(),
12062                breadcrumb: "S3 › Buckets".to_string(),
12063            },
12064        ];
12065        app.current_tab = 0;
12066
12067        app.handle_action(Action::OpenTabPicker);
12068
12069        assert_eq!(app.mode, Mode::TabPicker);
12070        assert_eq!(app.tab_picker_selected, 0);
12071    }
12072
12073    #[test]
12074    fn test_tab_filter_works() {
12075        let mut app = test_app();
12076        app.tabs = vec![
12077            Tab {
12078                service: Service::CloudWatchLogGroups,
12079                title: "CloudWatch Logs".to_string(),
12080                breadcrumb: "CloudWatch > Log groups".to_string(),
12081            },
12082            Tab {
12083                service: Service::S3Buckets,
12084                title: "S3 Buckets".to_string(),
12085                breadcrumb: "S3 › Buckets".to_string(),
12086            },
12087            Tab {
12088                service: Service::CloudWatchAlarms,
12089                title: "CloudWatch Alarms".to_string(),
12090                breadcrumb: "CloudWatch › Alarms".to_string(),
12091            },
12092        ];
12093        app.mode = Mode::TabPicker;
12094
12095        // Filter for "s3"
12096        app.handle_action(Action::FilterInput('s'));
12097        app.handle_action(Action::FilterInput('3'));
12098
12099        let filtered = app.get_filtered_tabs();
12100        assert_eq!(filtered.len(), 1);
12101        assert_eq!(filtered[0].1.title, "S3 Buckets");
12102    }
12103
12104    #[test]
12105    fn test_tab_filter_by_breadcrumb() {
12106        let mut app = test_app();
12107        app.tabs = vec![
12108            Tab {
12109                service: Service::CloudWatchLogGroups,
12110                title: "Tab 1".to_string(),
12111                breadcrumb: "CloudWatch > Log groups".to_string(),
12112            },
12113            Tab {
12114                service: Service::S3Buckets,
12115                title: "Tab 2".to_string(),
12116                breadcrumb: "S3 › Buckets".to_string(),
12117            },
12118        ];
12119        app.mode = Mode::TabPicker;
12120
12121        // Filter for "cloudwatch"
12122        app.handle_action(Action::FilterInput('c'));
12123        app.handle_action(Action::FilterInput('l'));
12124        app.handle_action(Action::FilterInput('o'));
12125        app.handle_action(Action::FilterInput('u'));
12126        app.handle_action(Action::FilterInput('d'));
12127
12128        let filtered = app.get_filtered_tabs();
12129        assert_eq!(filtered.len(), 1);
12130        assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
12131    }
12132
12133    #[test]
12134    fn test_tab_filter_backspace() {
12135        let mut app = test_app();
12136        app.tabs = vec![
12137            Tab {
12138                service: Service::CloudWatchLogGroups,
12139                title: "CloudWatch Logs".to_string(),
12140                breadcrumb: "CloudWatch > Log groups".to_string(),
12141            },
12142            Tab {
12143                service: Service::S3Buckets,
12144                title: "S3 Buckets".to_string(),
12145                breadcrumb: "S3 › Buckets".to_string(),
12146            },
12147        ];
12148        app.mode = Mode::TabPicker;
12149
12150        app.handle_action(Action::FilterInput('s'));
12151        app.handle_action(Action::FilterInput('3'));
12152        assert_eq!(app.tab_filter, "s3");
12153
12154        app.handle_action(Action::FilterBackspace);
12155        assert_eq!(app.tab_filter, "s");
12156
12157        let filtered = app.get_filtered_tabs();
12158        assert_eq!(filtered.len(), 2); // Both match "s"
12159    }
12160
12161    #[test]
12162    fn test_tab_selection_with_filter() {
12163        let mut app = test_app();
12164        app.tabs = vec![
12165            Tab {
12166                service: Service::CloudWatchLogGroups,
12167                title: "CloudWatch Logs".to_string(),
12168                breadcrumb: "CloudWatch > Log groups".to_string(),
12169            },
12170            Tab {
12171                service: Service::S3Buckets,
12172                title: "S3 Buckets".to_string(),
12173                breadcrumb: "S3 › Buckets".to_string(),
12174            },
12175        ];
12176        app.mode = Mode::TabPicker;
12177        app.current_tab = 0;
12178
12179        // Filter for "s3"
12180        app.handle_action(Action::FilterInput('s'));
12181        app.handle_action(Action::FilterInput('3'));
12182
12183        // Select the filtered tab
12184        app.handle_action(Action::Select);
12185
12186        assert_eq!(app.current_tab, 1); // Should select the S3 tab (index 1)
12187        assert_eq!(app.mode, Mode::Normal);
12188        assert_eq!(app.tab_filter, ""); // Filter should be cleared
12189    }
12190}
12191
12192#[cfg(test)]
12193mod region_latency_tests {
12194    use super::*;
12195    use test_helpers::*;
12196
12197    #[test]
12198    fn test_regions_sorted_by_latency() {
12199        let mut app = test_app();
12200
12201        // Add some latencies
12202        app.region_latencies.insert("us-west-2".to_string(), 50);
12203        app.region_latencies.insert("us-east-1".to_string(), 10);
12204        app.region_latencies.insert("eu-west-1".to_string(), 100);
12205
12206        let filtered = app.get_filtered_regions();
12207
12208        // Should be sorted by latency (lowest first)
12209        let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
12210
12211        assert!(with_latency.len() >= 3);
12212        assert_eq!(with_latency[0].code, "us-east-1");
12213        assert_eq!(with_latency[0].latency_ms, Some(10));
12214        assert_eq!(with_latency[1].code, "us-west-2");
12215        assert_eq!(with_latency[1].latency_ms, Some(50));
12216        assert_eq!(with_latency[2].code, "eu-west-1");
12217        assert_eq!(with_latency[2].latency_ms, Some(100));
12218    }
12219
12220    #[test]
12221    fn test_regions_with_latency_before_without() {
12222        let mut app = test_app();
12223
12224        // Only add latency for one region
12225        app.region_latencies.insert("eu-west-1".to_string(), 100);
12226
12227        let filtered = app.get_filtered_regions();
12228
12229        // Region with latency should come first
12230        assert_eq!(filtered[0].code, "eu-west-1");
12231        assert_eq!(filtered[0].latency_ms, Some(100));
12232
12233        // Rest should be sorted by name
12234        for region in &filtered[1..] {
12235            assert!(region.latency_ms.is_none());
12236        }
12237    }
12238
12239    #[test]
12240    fn test_region_filter_with_latency() {
12241        let mut app = test_app();
12242
12243        app.region_latencies.insert("us-east-1".to_string(), 10);
12244        app.region_latencies.insert("us-west-2".to_string(), 50);
12245        app.region_filter = "us".to_string();
12246
12247        let filtered = app.get_filtered_regions();
12248
12249        // Should only have US regions, sorted by latency
12250        assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
12251        assert_eq!(filtered[0].code, "us-east-1");
12252        assert_eq!(filtered[1].code, "us-west-2");
12253    }
12254
12255    #[test]
12256    fn test_latency_persists_across_filters() {
12257        let mut app = test_app();
12258
12259        app.region_latencies.insert("us-east-1".to_string(), 10);
12260
12261        // Filter to something else
12262        app.region_filter = "eu".to_string();
12263        let filtered = app.get_filtered_regions();
12264        assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
12265
12266        // Clear filter
12267        app.region_filter.clear();
12268        let all = app.get_filtered_regions();
12269
12270        // Latency should still be there
12271        let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
12272        assert_eq!(us_east.latency_ms, Some(10));
12273    }
12274
12275    #[test]
12276    fn test_measure_region_latencies_clears_previous() {
12277        let mut app = test_app();
12278
12279        // Add some fake latencies
12280        app.region_latencies.insert("us-east-1".to_string(), 100);
12281        app.region_latencies.insert("eu-west-1".to_string(), 200);
12282
12283        // Measure again (will fail to connect but should clear)
12284        app.measure_region_latencies();
12285
12286        // Old latencies should be cleared
12287        assert!(
12288            app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
12289        );
12290    }
12291
12292    #[test]
12293    fn test_regions_with_latency_sorted_first() {
12294        let mut app = test_app();
12295
12296        // Add latencies: one fast, one slow (>1000ms would be treated as >1s)
12297        app.region_latencies.insert("us-east-1".to_string(), 50);
12298        app.region_latencies.insert("eu-west-1".to_string(), 500);
12299
12300        let filtered = app.get_filtered_regions();
12301
12302        // Should show all regions
12303        assert!(filtered.len() > 2);
12304
12305        // Fast regions first
12306        assert_eq!(filtered[0].code, "us-east-1");
12307        assert_eq!(filtered[0].latency_ms, Some(50));
12308        assert_eq!(filtered[1].code, "eu-west-1");
12309        assert_eq!(filtered[1].latency_ms, Some(500));
12310
12311        // Regions without latency treated as 1000ms, so they come after 500ms
12312        for region in &filtered[2..] {
12313            assert!(region.latency_ms.is_none());
12314        }
12315    }
12316
12317    #[test]
12318    fn test_regions_without_latency_sorted_as_1000ms() {
12319        let mut app = test_app();
12320
12321        // Add one region with 1500ms (slower than default 1000ms)
12322        app.region_latencies
12323            .insert("ap-southeast-2".to_string(), 1500);
12324        // Add one region with 50ms (faster)
12325        app.region_latencies.insert("us-east-1".to_string(), 50);
12326
12327        let filtered = app.get_filtered_regions();
12328
12329        // Fast region first
12330        assert_eq!(filtered[0].code, "us-east-1");
12331        assert_eq!(filtered[0].latency_ms, Some(50));
12332
12333        // Regions without latency (treated as 1000ms) come before 1500ms
12334        let slow_region_idx = filtered
12335            .iter()
12336            .position(|r| r.code == "ap-southeast-2")
12337            .unwrap();
12338        assert!(slow_region_idx > 1); // Should be after fast region and regions without latency
12339
12340        // All regions between index 1 and slow_region_idx should have no latency
12341        for region in filtered.iter().take(slow_region_idx).skip(1) {
12342            assert!(region.latency_ms.is_none());
12343        }
12344    }
12345
12346    #[test]
12347    fn test_region_picker_opens_with_latencies() {
12348        let mut app = test_app();
12349
12350        // Simulate opening region picker
12351        app.region_filter.clear();
12352        app.region_picker_selected = 0;
12353        app.measure_region_latencies();
12354
12355        // Should have attempted to measure (even if all fail in test env)
12356        // The map should be initialized
12357        assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
12358    }
12359
12360    #[test]
12361    fn test_ecr_tab_next() {
12362        assert_eq!(EcrTab::Private.next(), EcrTab::Public);
12363        assert_eq!(EcrTab::Public.next(), EcrTab::Private);
12364    }
12365
12366    #[test]
12367    fn test_ecr_tab_switching() {
12368        let mut app = test_app();
12369        app.current_service = Service::EcrRepositories;
12370        app.service_selected = true;
12371        app.ecr_state.tab = EcrTab::Private;
12372
12373        app.handle_action(Action::NextDetailTab);
12374        assert_eq!(app.ecr_state.tab, EcrTab::Public);
12375        assert_eq!(app.ecr_state.repositories.selected, 0);
12376
12377        app.handle_action(Action::NextDetailTab);
12378        assert_eq!(app.ecr_state.tab, EcrTab::Private);
12379    }
12380
12381    #[test]
12382    fn test_ecr_navigation() {
12383        let mut app = test_app();
12384        app.current_service = Service::EcrRepositories;
12385        app.service_selected = true;
12386        app.mode = Mode::Normal;
12387        app.ecr_state.repositories.items = vec![
12388            EcrRepository {
12389                name: "repo1".to_string(),
12390                uri: "uri1".to_string(),
12391                created_at: "2023-01-01".to_string(),
12392                tag_immutability: "MUTABLE".to_string(),
12393                encryption_type: "AES256".to_string(),
12394            },
12395            EcrRepository {
12396                name: "repo2".to_string(),
12397                uri: "uri2".to_string(),
12398                created_at: "2023-01-02".to_string(),
12399                tag_immutability: "IMMUTABLE".to_string(),
12400                encryption_type: "KMS".to_string(),
12401            },
12402        ];
12403
12404        app.handle_action(Action::NextItem);
12405        assert_eq!(app.ecr_state.repositories.selected, 1);
12406
12407        app.handle_action(Action::PrevItem);
12408        assert_eq!(app.ecr_state.repositories.selected, 0);
12409    }
12410
12411    #[test]
12412    fn test_ecr_filter() {
12413        let mut app = test_app();
12414        app.current_service = Service::EcrRepositories;
12415        app.service_selected = true;
12416        app.ecr_state.repositories.items = vec![
12417            EcrRepository {
12418                name: "my-app".to_string(),
12419                uri: "uri1".to_string(),
12420                created_at: "2023-01-01".to_string(),
12421                tag_immutability: "MUTABLE".to_string(),
12422                encryption_type: "AES256".to_string(),
12423            },
12424            EcrRepository {
12425                name: "other-service".to_string(),
12426                uri: "uri2".to_string(),
12427                created_at: "2023-01-02".to_string(),
12428                tag_immutability: "IMMUTABLE".to_string(),
12429                encryption_type: "KMS".to_string(),
12430            },
12431        ];
12432
12433        app.ecr_state.repositories.filter = "app".to_string();
12434        let filtered = filtered_ecr_repositories(&app);
12435        assert_eq!(filtered.len(), 1);
12436        assert_eq!(filtered[0].name, "my-app");
12437    }
12438
12439    #[test]
12440    fn test_ecr_filter_input() {
12441        let mut app = test_app();
12442        app.current_service = Service::EcrRepositories;
12443        app.service_selected = true;
12444        app.mode = Mode::FilterInput;
12445
12446        app.handle_action(Action::FilterInput('t'));
12447        app.handle_action(Action::FilterInput('e'));
12448        app.handle_action(Action::FilterInput('s'));
12449        app.handle_action(Action::FilterInput('t'));
12450        assert_eq!(app.ecr_state.repositories.filter, "test");
12451
12452        app.handle_action(Action::FilterBackspace);
12453        assert_eq!(app.ecr_state.repositories.filter, "tes");
12454    }
12455
12456    #[test]
12457    fn test_ecr_filter_resets_selection() {
12458        let mut app = test_app();
12459        app.current_service = Service::EcrRepositories;
12460        app.service_selected = true;
12461        app.mode = Mode::FilterInput;
12462        app.ecr_state.repositories.items = vec![
12463            EcrRepository {
12464                name: "repo1".to_string(),
12465                uri: "uri1".to_string(),
12466                created_at: "2023-01-01".to_string(),
12467                tag_immutability: "MUTABLE".to_string(),
12468                encryption_type: "AES256".to_string(),
12469            },
12470            EcrRepository {
12471                name: "repo2".to_string(),
12472                uri: "uri2".to_string(),
12473                created_at: "2023-01-02".to_string(),
12474                tag_immutability: "IMMUTABLE".to_string(),
12475                encryption_type: "KMS".to_string(),
12476            },
12477            EcrRepository {
12478                name: "repo3".to_string(),
12479                uri: "uri3".to_string(),
12480                created_at: "2023-01-03".to_string(),
12481                tag_immutability: "MUTABLE".to_string(),
12482                encryption_type: "AES256".to_string(),
12483            },
12484        ];
12485
12486        // Move selection to second item
12487        app.ecr_state.repositories.selected = 2;
12488        assert_eq!(app.ecr_state.repositories.selected, 2);
12489
12490        // Apply filter - selection should reset to 0
12491        app.handle_action(Action::FilterInput('t'));
12492        assert_eq!(app.ecr_state.repositories.filter, "t");
12493        assert_eq!(app.ecr_state.repositories.selected, 0);
12494
12495        // Move selection again
12496        app.ecr_state.repositories.selected = 1;
12497
12498        // Backspace filter - selection should reset to 0
12499        app.handle_action(Action::FilterBackspace);
12500        assert_eq!(app.ecr_state.repositories.filter, "");
12501        assert_eq!(app.ecr_state.repositories.selected, 0);
12502    }
12503
12504    #[test]
12505    fn test_ecr_images_filter_resets_selection() {
12506        let mut app = test_app();
12507        app.current_service = Service::EcrRepositories;
12508        app.service_selected = true;
12509        app.mode = Mode::FilterInput;
12510        app.ecr_state.current_repository = Some("test-repo".to_string());
12511        app.ecr_state.images.items = vec![
12512            EcrImage {
12513                tag: "v1.0.0".to_string(),
12514                artifact_type: "container".to_string(),
12515                digest: "sha256:abc123".to_string(),
12516                pushed_at: "2023-01-01".to_string(),
12517                size_bytes: 1000,
12518                uri: "uri1".to_string(),
12519                last_pull_time: "".to_string(),
12520            },
12521            EcrImage {
12522                tag: "v2.0.0".to_string(),
12523                artifact_type: "container".to_string(),
12524                digest: "sha256:def456".to_string(),
12525                pushed_at: "2023-01-02".to_string(),
12526                size_bytes: 2000,
12527                uri: "uri2".to_string(),
12528                last_pull_time: "".to_string(),
12529            },
12530        ];
12531
12532        // Move selection to second item
12533        app.ecr_state.images.selected = 1;
12534        assert_eq!(app.ecr_state.images.selected, 1);
12535
12536        // Apply filter - selection should reset to 0
12537        app.handle_action(Action::FilterInput('v'));
12538        assert_eq!(app.ecr_state.images.filter, "v");
12539        assert_eq!(app.ecr_state.images.selected, 0);
12540    }
12541
12542    #[test]
12543    fn test_iam_users_filter_input() {
12544        let mut app = test_app();
12545        app.current_service = Service::IamUsers;
12546        app.service_selected = true;
12547        app.mode = Mode::FilterInput;
12548
12549        app.handle_action(Action::FilterInput('a'));
12550        app.handle_action(Action::FilterInput('d'));
12551        app.handle_action(Action::FilterInput('m'));
12552        app.handle_action(Action::FilterInput('i'));
12553        app.handle_action(Action::FilterInput('n'));
12554        assert_eq!(app.iam_state.users.filter, "admin");
12555
12556        app.handle_action(Action::FilterBackspace);
12557        assert_eq!(app.iam_state.users.filter, "admi");
12558    }
12559
12560    #[test]
12561    fn test_iam_policies_filter_input() {
12562        let mut app = test_app();
12563        app.current_service = Service::IamUsers;
12564        app.service_selected = true;
12565        app.iam_state.current_user = Some("testuser".to_string());
12566        app.mode = Mode::FilterInput;
12567
12568        app.handle_action(Action::FilterInput('r'));
12569        app.handle_action(Action::FilterInput('e'));
12570        app.handle_action(Action::FilterInput('a'));
12571        app.handle_action(Action::FilterInput('d'));
12572        assert_eq!(app.iam_state.policies.filter, "read");
12573
12574        app.handle_action(Action::FilterBackspace);
12575        assert_eq!(app.iam_state.policies.filter, "rea");
12576    }
12577
12578    #[test]
12579    fn test_iam_start_filter() {
12580        let mut app = test_app();
12581        app.current_service = Service::IamUsers;
12582        app.service_selected = true;
12583        app.mode = Mode::Normal;
12584
12585        app.handle_action(Action::StartFilter);
12586        assert_eq!(app.mode, Mode::FilterInput);
12587    }
12588
12589    #[test]
12590    fn test_iam_roles_filter_input() {
12591        let mut app = test_app();
12592        app.current_service = Service::IamRoles;
12593        app.service_selected = true;
12594        app.mode = Mode::FilterInput;
12595
12596        app.handle_action(Action::FilterInput('a'));
12597        app.handle_action(Action::FilterInput('d'));
12598        app.handle_action(Action::FilterInput('m'));
12599        app.handle_action(Action::FilterInput('i'));
12600        app.handle_action(Action::FilterInput('n'));
12601        assert_eq!(app.iam_state.roles.filter, "admin");
12602
12603        app.handle_action(Action::FilterBackspace);
12604        assert_eq!(app.iam_state.roles.filter, "admi");
12605    }
12606
12607    #[test]
12608    fn test_iam_roles_start_filter() {
12609        let mut app = test_app();
12610        app.current_service = Service::IamRoles;
12611        app.service_selected = true;
12612        app.mode = Mode::Normal;
12613
12614        app.handle_action(Action::StartFilter);
12615        assert_eq!(app.mode, Mode::FilterInput);
12616    }
12617
12618    #[test]
12619    fn test_iam_roles_navigation() {
12620        let mut app = test_app();
12621        app.current_service = Service::IamRoles;
12622        app.service_selected = true;
12623        app.mode = Mode::Normal;
12624        app.iam_state.roles.items = (0..10)
12625            .map(|i| IamRole {
12626                role_name: format!("role{}", i),
12627                path: "/".to_string(),
12628                trusted_entities: String::new(),
12629                last_activity: String::new(),
12630                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12631                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12632                description: String::new(),
12633                max_session_duration: Some(3600),
12634            })
12635            .collect();
12636
12637        assert_eq!(app.iam_state.roles.selected, 0);
12638
12639        app.handle_action(Action::NextItem);
12640        assert_eq!(app.iam_state.roles.selected, 1);
12641
12642        app.handle_action(Action::NextItem);
12643        assert_eq!(app.iam_state.roles.selected, 2);
12644
12645        app.handle_action(Action::PrevItem);
12646        assert_eq!(app.iam_state.roles.selected, 1);
12647    }
12648
12649    #[test]
12650    fn test_iam_roles_page_hotkey() {
12651        let mut app = test_app();
12652        app.current_service = Service::IamRoles;
12653        app.service_selected = true;
12654        app.mode = Mode::Normal;
12655        app.iam_state.roles.page_size = PageSize::Ten;
12656        app.iam_state.roles.items = (0..100)
12657            .map(|i| IamRole {
12658                role_name: format!("role{}", i),
12659                path: "/".to_string(),
12660                trusted_entities: String::new(),
12661                last_activity: String::new(),
12662                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12663                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12664                description: String::new(),
12665                max_session_duration: Some(3600),
12666            })
12667            .collect();
12668
12669        app.handle_action(Action::FilterInput('2'));
12670        app.handle_action(Action::OpenColumnSelector);
12671        assert_eq!(app.iam_state.roles.selected, 10); // Page 2 = index 10 (with page size 10)
12672    }
12673
12674    #[test]
12675    fn test_iam_users_page_hotkey() {
12676        let mut app = test_app();
12677        app.current_service = Service::IamUsers;
12678        app.service_selected = true;
12679        app.mode = Mode::Normal;
12680        app.iam_state.users.page_size = PageSize::Ten;
12681        app.iam_state.users.items = (0..100)
12682            .map(|i| IamUser {
12683                user_name: format!("user{}", i),
12684                path: "/".to_string(),
12685                groups: String::new(),
12686                last_activity: String::new(),
12687                mfa: String::new(),
12688                password_age: String::new(),
12689                console_last_sign_in: String::new(),
12690                access_key_id: String::new(),
12691                active_key_age: String::new(),
12692                access_key_last_used: String::new(),
12693                arn: format!("arn:aws:iam::123456789012:user/user{}", i),
12694                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12695                console_access: String::new(),
12696                signing_certs: String::new(),
12697            })
12698            .collect();
12699
12700        app.handle_action(Action::FilterInput('3'));
12701        app.handle_action(Action::OpenColumnSelector);
12702        assert_eq!(app.iam_state.users.selected, 20); // Page 3 = index 20 (with page size 10)
12703    }
12704
12705    #[test]
12706    fn test_ecr_scroll_navigation() {
12707        let mut app = test_app();
12708        app.current_service = Service::EcrRepositories;
12709        app.service_selected = true;
12710        app.ecr_state.repositories.items = (0..20)
12711            .map(|i| EcrRepository {
12712                name: format!("repo{}", i),
12713                uri: format!("uri{}", i),
12714                created_at: "2023-01-01".to_string(),
12715                tag_immutability: "MUTABLE".to_string(),
12716                encryption_type: "AES256".to_string(),
12717            })
12718            .collect();
12719
12720        app.handle_action(Action::ScrollDown);
12721        assert_eq!(app.ecr_state.repositories.selected, 10);
12722
12723        app.handle_action(Action::ScrollUp);
12724        assert_eq!(app.ecr_state.repositories.selected, 0);
12725    }
12726
12727    #[test]
12728    fn test_ecr_tab_switching_triggers_reload() {
12729        let mut app = test_app();
12730        app.current_service = Service::EcrRepositories;
12731        app.service_selected = true;
12732        app.ecr_state.tab = EcrTab::Private;
12733        app.ecr_state.repositories.loading = false;
12734        app.ecr_state.repositories.items = vec![EcrRepository {
12735            name: "private-repo".to_string(),
12736            uri: "uri".to_string(),
12737            created_at: "2023-01-01".to_string(),
12738            tag_immutability: "MUTABLE".to_string(),
12739            encryption_type: "AES256".to_string(),
12740        }];
12741
12742        app.handle_action(Action::NextDetailTab);
12743        assert_eq!(app.ecr_state.tab, EcrTab::Public);
12744        assert!(app.ecr_state.repositories.loading);
12745        assert_eq!(app.ecr_state.repositories.selected, 0);
12746    }
12747
12748    #[test]
12749    fn test_ecr_tab_cycles_between_private_and_public() {
12750        let mut app = test_app();
12751        app.current_service = Service::EcrRepositories;
12752        app.service_selected = true;
12753        app.ecr_state.tab = EcrTab::Private;
12754
12755        app.handle_action(Action::NextDetailTab);
12756        assert_eq!(app.ecr_state.tab, EcrTab::Public);
12757
12758        app.handle_action(Action::NextDetailTab);
12759        assert_eq!(app.ecr_state.tab, EcrTab::Private);
12760    }
12761
12762    #[test]
12763    fn test_page_size_values() {
12764        assert_eq!(PageSize::Ten.value(), 10);
12765        assert_eq!(PageSize::TwentyFive.value(), 25);
12766        assert_eq!(PageSize::Fifty.value(), 50);
12767        assert_eq!(PageSize::OneHundred.value(), 100);
12768    }
12769
12770    #[test]
12771    fn test_page_size_next() {
12772        assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
12773        assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
12774        assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
12775        assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
12776    }
12777
12778    #[test]
12779    fn test_ecr_enter_drills_into_repository() {
12780        let mut app = test_app();
12781        app.current_service = Service::EcrRepositories;
12782        app.service_selected = true;
12783        app.mode = Mode::Normal;
12784        app.ecr_state.repositories.items = vec![EcrRepository {
12785            name: "my-repo".to_string(),
12786            uri: "uri".to_string(),
12787            created_at: "2023-01-01".to_string(),
12788            tag_immutability: "MUTABLE".to_string(),
12789            encryption_type: "AES256".to_string(),
12790        }];
12791
12792        app.handle_action(Action::Select);
12793        assert_eq!(
12794            app.ecr_state.current_repository,
12795            Some("my-repo".to_string())
12796        );
12797        assert!(app.ecr_state.repositories.loading);
12798    }
12799
12800    #[test]
12801    fn test_ecr_repository_expansion() {
12802        let mut app = test_app();
12803        app.current_service = Service::EcrRepositories;
12804        app.service_selected = true;
12805        app.ecr_state.repositories.items = vec![EcrRepository {
12806            name: "my-repo".to_string(),
12807            uri: "uri".to_string(),
12808            created_at: "2023-01-01".to_string(),
12809            tag_immutability: "MUTABLE".to_string(),
12810            encryption_type: "AES256".to_string(),
12811        }];
12812        app.ecr_state.repositories.selected = 0;
12813
12814        assert_eq!(app.ecr_state.repositories.expanded_item, None);
12815
12816        app.handle_action(Action::NextPane);
12817        assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
12818
12819        app.handle_action(Action::PrevPane);
12820        assert_eq!(app.ecr_state.repositories.expanded_item, None);
12821    }
12822
12823    #[test]
12824    fn test_ecr_ctrl_d_scrolls_down() {
12825        let mut app = test_app();
12826        app.current_service = Service::EcrRepositories;
12827        app.service_selected = true;
12828        app.mode = Mode::Normal;
12829        app.ecr_state.repositories.items = (0..30)
12830            .map(|i| EcrRepository {
12831                name: format!("repo{}", i),
12832                uri: format!("uri{}", i),
12833                created_at: "2023-01-01".to_string(),
12834                tag_immutability: "MUTABLE".to_string(),
12835                encryption_type: "AES256".to_string(),
12836            })
12837            .collect();
12838        app.ecr_state.repositories.selected = 0;
12839
12840        app.handle_action(Action::PageDown);
12841        assert_eq!(app.ecr_state.repositories.selected, 10);
12842    }
12843
12844    #[test]
12845    fn test_ecr_ctrl_u_scrolls_up() {
12846        let mut app = test_app();
12847        app.current_service = Service::EcrRepositories;
12848        app.service_selected = true;
12849        app.mode = Mode::Normal;
12850        app.ecr_state.repositories.items = (0..30)
12851            .map(|i| EcrRepository {
12852                name: format!("repo{}", i),
12853                uri: format!("uri{}", i),
12854                created_at: "2023-01-01".to_string(),
12855                tag_immutability: "MUTABLE".to_string(),
12856                encryption_type: "AES256".to_string(),
12857            })
12858            .collect();
12859        app.ecr_state.repositories.selected = 15;
12860
12861        app.handle_action(Action::PageUp);
12862        assert_eq!(app.ecr_state.repositories.selected, 5);
12863    }
12864
12865    #[test]
12866    fn test_ecr_images_ctrl_d_scrolls_down() {
12867        let mut app = test_app();
12868        app.current_service = Service::EcrRepositories;
12869        app.service_selected = true;
12870        app.mode = Mode::Normal;
12871        app.ecr_state.current_repository = Some("repo".to_string());
12872        app.ecr_state.images.items = (0..30)
12873            .map(|i| EcrImage {
12874                tag: format!("tag{}", i),
12875                artifact_type: "container".to_string(),
12876                pushed_at: "2023-01-01T12:00:00Z".to_string(),
12877                size_bytes: 104857600,
12878                uri: format!("uri{}", i),
12879                digest: format!("sha256:{}", i),
12880                last_pull_time: String::new(),
12881            })
12882            .collect();
12883        app.ecr_state.images.selected = 0;
12884
12885        app.handle_action(Action::PageDown);
12886        assert_eq!(app.ecr_state.images.selected, 10);
12887    }
12888
12889    #[test]
12890    fn test_ecr_esc_goes_back_from_images_to_repos() {
12891        let mut app = test_app();
12892        app.current_service = Service::EcrRepositories;
12893        app.service_selected = true;
12894        app.mode = Mode::Normal;
12895        app.ecr_state.current_repository = Some("my-repo".to_string());
12896        app.ecr_state.images.items = vec![EcrImage {
12897            tag: "latest".to_string(),
12898            artifact_type: "container".to_string(),
12899            pushed_at: "2023-01-01T12:00:00Z".to_string(),
12900            size_bytes: 104857600,
12901            uri: "uri".to_string(),
12902            digest: "sha256:abc".to_string(),
12903            last_pull_time: String::new(),
12904        }];
12905
12906        app.handle_action(Action::GoBack);
12907        assert_eq!(app.ecr_state.current_repository, None);
12908        assert!(app.ecr_state.images.items.is_empty());
12909    }
12910
12911    #[test]
12912    fn test_ecr_esc_collapses_expanded_image_first() {
12913        let mut app = test_app();
12914        app.current_service = Service::EcrRepositories;
12915        app.service_selected = true;
12916        app.mode = Mode::Normal;
12917        app.ecr_state.current_repository = Some("my-repo".to_string());
12918        app.ecr_state.images.expanded_item = Some(0);
12919
12920        app.handle_action(Action::GoBack);
12921        assert_eq!(app.ecr_state.images.expanded_item, None);
12922        assert_eq!(
12923            app.ecr_state.current_repository,
12924            Some("my-repo".to_string())
12925        );
12926    }
12927
12928    #[test]
12929    fn test_pagination_with_lowercase_p() {
12930        let mut app = test_app();
12931        app.current_service = Service::EcrRepositories;
12932        app.service_selected = true;
12933        app.mode = Mode::Normal;
12934        app.ecr_state.repositories.items = (0..100)
12935            .map(|i| EcrRepository {
12936                name: format!("repo{}", i),
12937                uri: format!("uri{}", i),
12938                created_at: "2023-01-01".to_string(),
12939                tag_immutability: "MUTABLE".to_string(),
12940                encryption_type: "AES256".to_string(),
12941            })
12942            .collect();
12943
12944        // Type "2" then "p" to go to page 2
12945        app.handle_action(Action::FilterInput('2'));
12946        assert_eq!(app.page_input, "2");
12947
12948        app.handle_action(Action::OpenColumnSelector); // 'p' key
12949        assert_eq!(app.ecr_state.repositories.selected, 50); // Page 2 starts at index 50
12950        assert_eq!(app.page_input, ""); // Should be cleared
12951    }
12952
12953    #[test]
12954    fn test_lowercase_p_without_number_opens_preferences() {
12955        let mut app = test_app();
12956        app.current_service = Service::EcrRepositories;
12957        app.service_selected = true;
12958        app.mode = Mode::Normal;
12959
12960        app.handle_action(Action::OpenColumnSelector); // 'p' key without number
12961        assert_eq!(app.mode, Mode::ColumnSelector);
12962    }
12963
12964    #[test]
12965    fn test_ctrl_o_generates_correct_console_url() {
12966        let mut app = test_app();
12967        app.current_service = Service::EcrRepositories;
12968        app.service_selected = true;
12969        app.mode = Mode::Normal;
12970        app.config.account_id = "123456789012".to_string();
12971
12972        // Test repository list URL
12973        let url = app.get_console_url();
12974        assert!(url.contains("ecr/private-registry/repositories"));
12975        assert!(url.contains("region=us-east-1"));
12976
12977        // Test images URL
12978        app.ecr_state.current_repository = Some("my-repo".to_string());
12979        let url = app.get_console_url();
12980        assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
12981        assert!(url.contains("region=us-east-1"));
12982    }
12983
12984    #[test]
12985    fn test_page_input_display_and_reset() {
12986        let mut app = test_app();
12987        app.current_service = Service::EcrRepositories;
12988        app.service_selected = true;
12989        app.mode = Mode::Normal;
12990        app.ecr_state.repositories.items = (0..100)
12991            .map(|i| EcrRepository {
12992                name: format!("repo{}", i),
12993                uri: format!("uri{}", i),
12994                created_at: "2023-01-01".to_string(),
12995                tag_immutability: "MUTABLE".to_string(),
12996                encryption_type: "AES256".to_string(),
12997            })
12998            .collect();
12999
13000        // Type "2"
13001        app.handle_action(Action::FilterInput('2'));
13002        assert_eq!(app.page_input, "2");
13003
13004        // Press 'p' to go to page 2
13005        app.handle_action(Action::OpenColumnSelector);
13006        assert_eq!(app.page_input, ""); // Should be cleared
13007        assert_eq!(app.ecr_state.repositories.selected, 50); // Page 2 starts at index 50
13008    }
13009
13010    #[test]
13011    fn test_page_navigation_updates_scroll_offset_for_cfn() {
13012        let mut app = test_app();
13013        app.current_service = Service::CloudFormationStacks;
13014        app.service_selected = true;
13015        app.mode = Mode::Normal;
13016        app.cfn_state.table.items = (0..100)
13017            .map(|i| CfnStack {
13018                name: format!("stack-{}", i),
13019                stack_id: format!(
13020                    "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
13021                    i
13022                ),
13023                status: "CREATE_COMPLETE".to_string(),
13024                created_time: "2023-01-01T00:00:00Z".to_string(),
13025                updated_time: "2023-01-01T00:00:00Z".to_string(),
13026                deleted_time: String::new(),
13027                drift_status: "IN_SYNC".to_string(),
13028                last_drift_check_time: String::new(),
13029                status_reason: String::new(),
13030                description: String::new(),
13031                detailed_status: String::new(),
13032                root_stack: String::new(),
13033                parent_stack: String::new(),
13034                termination_protection: false,
13035                iam_role: String::new(),
13036                tags: vec![],
13037                stack_policy: String::new(),
13038                rollback_monitoring_time: String::new(),
13039                rollback_alarms: vec![],
13040                notification_arns: vec![],
13041            })
13042            .collect();
13043
13044        // Type "2" then "p" to go to page 2
13045        app.handle_action(Action::FilterInput('2'));
13046        assert_eq!(app.page_input, "2");
13047
13048        app.handle_action(Action::OpenColumnSelector); // 'p' key
13049        assert_eq!(app.page_input, ""); // Should be cleared
13050
13051        // Verify both selected and scroll_offset are updated
13052        let page_size = app.cfn_state.table.page_size.value();
13053        let expected_offset = page_size; // Page 2 starts at page_size
13054        assert_eq!(app.cfn_state.table.selected, expected_offset);
13055        assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
13056
13057        // Verify pagination display shows page 2
13058        let current_page = app.cfn_state.table.scroll_offset / page_size;
13059        assert_eq!(
13060            current_page, 1,
13061            "2p should go to page 2 (0-indexed as 1), not page 3"
13062        ); // 0-indexed, so page 2 is index 1
13063    }
13064
13065    #[test]
13066    fn test_3p_goes_to_page_3_not_page_5() {
13067        let mut app = test_app();
13068        app.current_service = Service::CloudFormationStacks;
13069        app.service_selected = true;
13070        app.mode = Mode::Normal;
13071        app.cfn_state.table.items = (0..200)
13072            .map(|i| CfnStack {
13073                name: format!("stack-{}", i),
13074                stack_id: format!(
13075                    "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
13076                    i
13077                ),
13078                status: "CREATE_COMPLETE".to_string(),
13079                created_time: "2023-01-01T00:00:00Z".to_string(),
13080                updated_time: "2023-01-01T00:00:00Z".to_string(),
13081                deleted_time: String::new(),
13082                drift_status: "IN_SYNC".to_string(),
13083                last_drift_check_time: String::new(),
13084                status_reason: String::new(),
13085                description: String::new(),
13086                detailed_status: String::new(),
13087                root_stack: String::new(),
13088                parent_stack: String::new(),
13089                termination_protection: false,
13090                iam_role: String::new(),
13091                tags: vec![],
13092                stack_policy: String::new(),
13093                rollback_monitoring_time: String::new(),
13094                rollback_alarms: vec![],
13095                notification_arns: vec![],
13096            })
13097            .collect();
13098
13099        // Type "3" then "p" to go to page 3
13100        app.handle_action(Action::FilterInput('3'));
13101        app.handle_action(Action::OpenColumnSelector);
13102
13103        let page_size = app.cfn_state.table.page_size.value();
13104        let current_page = app.cfn_state.table.scroll_offset / page_size;
13105        assert_eq!(
13106            current_page, 2,
13107            "3p should go to page 3 (0-indexed as 2), not page 5"
13108        );
13109        assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
13110    }
13111
13112    #[test]
13113    fn test_log_streams_page_navigation_uses_correct_page_size() {
13114        let mut app = test_app();
13115        app.current_service = Service::CloudWatchLogGroups;
13116        app.view_mode = ViewMode::Detail;
13117        app.service_selected = true;
13118        app.mode = Mode::Normal;
13119        app.log_groups_state.log_streams = (0..100)
13120            .map(|i| LogStream {
13121                name: format!("stream-{}", i),
13122                creation_time: None,
13123                last_event_time: None,
13124            })
13125            .collect();
13126
13127        // Type "2" then "p" to go to page 2
13128        app.handle_action(Action::FilterInput('2'));
13129        app.handle_action(Action::OpenColumnSelector);
13130
13131        // Should go to page 2 (page index 1)
13132        assert_eq!(app.log_groups_state.stream_current_page, 1);
13133        assert_eq!(app.log_groups_state.selected_stream, 0);
13134
13135        // Verify pagination display shows page 2 (not page 3)
13136        assert_eq!(
13137            app.log_groups_state.stream_current_page, 1,
13138            "2p should go to page 2 (0-indexed as 1), not page 3"
13139        );
13140    }
13141
13142    #[test]
13143    fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
13144        let mut app = test_app();
13145        app.current_service = Service::EcrRepositories;
13146        app.service_selected = true;
13147        app.mode = Mode::Normal;
13148        app.ecr_state.repositories.page_size = PageSize::TwentyFive; // Set to 25
13149        app.ecr_state.repositories.items = (0..100)
13150            .map(|i| EcrRepository {
13151                name: format!("repo{}", i),
13152                uri: format!("uri{}", i),
13153                created_at: "2023-01-01".to_string(),
13154                tag_immutability: "MUTABLE".to_string(),
13155                encryption_type: "AES256".to_string(),
13156            })
13157            .collect();
13158
13159        // Type "3" then "p" to go to page 3
13160        app.handle_action(Action::FilterInput('3'));
13161        app.handle_action(Action::OpenColumnSelector);
13162
13163        // With page_size=25, page 3 starts at index 50
13164        assert_eq!(app.ecr_state.repositories.selected, 50);
13165
13166        let page_size = app.ecr_state.repositories.page_size.value();
13167        let current_page = app.ecr_state.repositories.selected / page_size;
13168        assert_eq!(
13169            current_page, 2,
13170            "3p with page_size=25 should go to page 3 (0-indexed as 2)"
13171        );
13172    }
13173
13174    #[test]
13175    fn test_page_navigation_updates_scroll_offset_for_alarms() {
13176        let mut app = test_app();
13177        app.current_service = Service::CloudWatchAlarms;
13178        app.service_selected = true;
13179        app.mode = Mode::Normal;
13180        app.alarms_state.table.items = (0..100)
13181            .map(|i| Alarm {
13182                name: format!("alarm-{}", i),
13183                state: "OK".to_string(),
13184                state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
13185                description: String::new(),
13186                metric_name: "CPUUtilization".to_string(),
13187                namespace: "AWS/EC2".to_string(),
13188                statistic: "Average".to_string(),
13189                period: 300,
13190                comparison_operator: "GreaterThanThreshold".to_string(),
13191                threshold: 80.0,
13192                actions_enabled: true,
13193                state_reason: String::new(),
13194                resource: String::new(),
13195                dimensions: String::new(),
13196                expression: String::new(),
13197                alarm_type: "MetricAlarm".to_string(),
13198                cross_account: String::new(),
13199            })
13200            .collect();
13201
13202        // Type "2" then "p" to go to page 2
13203        app.handle_action(Action::FilterInput('2'));
13204        app.handle_action(Action::OpenColumnSelector);
13205
13206        // Verify both selected and scroll_offset are updated
13207        let page_size = app.alarms_state.table.page_size.value();
13208        let expected_offset = page_size; // Page 2 starts at page_size
13209        assert_eq!(app.alarms_state.table.selected, expected_offset);
13210        assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
13211    }
13212
13213    #[test]
13214    fn test_ecr_pagination_with_65_repos() {
13215        let mut app = test_app();
13216        app.current_service = Service::EcrRepositories;
13217        app.service_selected = true;
13218        app.mode = Mode::Normal;
13219        app.ecr_state.repositories.items = (0..65)
13220            .map(|i| EcrRepository {
13221                name: format!("repo{:02}", i),
13222                uri: format!("uri{}", i),
13223                created_at: "2023-01-01".to_string(),
13224                tag_immutability: "MUTABLE".to_string(),
13225                encryption_type: "AES256".to_string(),
13226            })
13227            .collect();
13228
13229        // Page 1: items 0-49 (50 items)
13230        assert_eq!(app.ecr_state.repositories.selected, 0);
13231        let page_size = 50;
13232        let current_page = app.ecr_state.repositories.selected / page_size;
13233        assert_eq!(current_page, 0);
13234
13235        // Go to page 2
13236        app.handle_action(Action::FilterInput('2'));
13237        app.handle_action(Action::OpenColumnSelector);
13238        assert_eq!(app.ecr_state.repositories.selected, 50);
13239
13240        // Page 2: items 50-64 (15 items)
13241        let current_page = app.ecr_state.repositories.selected / page_size;
13242        assert_eq!(current_page, 1);
13243    }
13244
13245    #[test]
13246    fn test_ecr_repos_input_focus_tab_cycling() {
13247        let mut app = test_app();
13248        app.current_service = Service::EcrRepositories;
13249        app.service_selected = true;
13250        app.mode = Mode::FilterInput;
13251        app.ecr_state.input_focus = InputFocus::Filter;
13252
13253        // Tab should cycle to Pagination
13254        app.handle_action(Action::NextFilterFocus);
13255        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13256
13257        // Tab again should cycle back to Input
13258        app.handle_action(Action::NextFilterFocus);
13259        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13260
13261        // Shift+Tab should cycle backwards to Pagination
13262        app.handle_action(Action::PrevFilterFocus);
13263        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13264
13265        // Shift+Tab again should cycle back to Input
13266        app.handle_action(Action::PrevFilterFocus);
13267        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13268    }
13269
13270    #[test]
13271    fn test_ecr_images_column_toggle_not_off_by_one() {
13272        use crate::ecr::image::Column as ImageColumn;
13273        let mut app = test_app();
13274        app.current_service = Service::EcrRepositories;
13275        app.service_selected = true;
13276        app.mode = Mode::ColumnSelector;
13277        app.ecr_state.current_repository = Some("test-repo".to_string());
13278
13279        // Start with all columns visible
13280        app.ecr_image_visible_column_ids = ImageColumn::ids();
13281        let initial_count = app.ecr_image_visible_column_ids.len();
13282
13283        // Select first column (index 0) and toggle it
13284        app.column_selector_index = 0;
13285        app.handle_action(Action::ToggleColumn);
13286
13287        // First column should be removed
13288        assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
13289        assert!(!app
13290            .ecr_image_visible_column_ids
13291            .contains(&ImageColumn::Tag.id()));
13292
13293        // Toggle it back
13294        app.handle_action(Action::ToggleColumn);
13295        assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
13296        assert!(app
13297            .ecr_image_visible_column_ids
13298            .contains(&ImageColumn::Tag.id()));
13299    }
13300
13301    #[test]
13302    fn test_ecr_repos_column_toggle_works() {
13303        let mut app = test_app();
13304        app.current_service = Service::EcrRepositories;
13305        app.service_selected = true;
13306        app.mode = Mode::ColumnSelector;
13307        app.ecr_state.current_repository = None;
13308
13309        // Start with all columns visible
13310        app.ecr_repo_visible_column_ids = EcrColumn::ids();
13311        let initial_count = app.ecr_repo_visible_column_ids.len();
13312
13313        // Select first column (index 1, since 0 is header) and toggle it
13314        app.column_selector_index = 1;
13315        app.handle_action(Action::ToggleColumn);
13316
13317        // First column should be removed
13318        assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
13319        assert!(!app
13320            .ecr_repo_visible_column_ids
13321            .contains(&EcrColumn::Name.id()));
13322
13323        // Toggle it back
13324        app.handle_action(Action::ToggleColumn);
13325        assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
13326        assert!(app
13327            .ecr_repo_visible_column_ids
13328            .contains(&EcrColumn::Name.id()));
13329    }
13330
13331    #[test]
13332    fn test_ecr_repos_pagination_left_right_navigation() {
13333        use crate::ecr::repo::Repository as EcrRepository;
13334        let mut app = test_app();
13335        app.current_service = Service::EcrRepositories;
13336        app.service_selected = true;
13337        app.mode = Mode::FilterInput;
13338        app.ecr_state.input_focus = InputFocus::Pagination;
13339
13340        // Create 150 repos (3 pages with page size 50)
13341        app.ecr_state.repositories.items = (0..150)
13342            .map(|i| EcrRepository {
13343                name: format!("repo{:03}", i),
13344                uri: format!("uri{}", i),
13345                created_at: "2023-01-01".to_string(),
13346                tag_immutability: "MUTABLE".to_string(),
13347                encryption_type: "AES256".to_string(),
13348            })
13349            .collect();
13350
13351        // Start on page 1 (index 0)
13352        app.ecr_state.repositories.selected = 0;
13353        eprintln!(
13354            "Initial: selected={}, focus={:?}, mode={:?}",
13355            app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
13356        );
13357
13358        // Right arrow (PageDown) should go to page 2
13359        app.handle_action(Action::PageDown);
13360        eprintln!(
13361            "After PageDown: selected={}",
13362            app.ecr_state.repositories.selected
13363        );
13364        assert_eq!(app.ecr_state.repositories.selected, 50);
13365
13366        // Right arrow again should go to page 3
13367        app.handle_action(Action::PageDown);
13368        eprintln!(
13369            "After 2nd PageDown: selected={}",
13370            app.ecr_state.repositories.selected
13371        );
13372        assert_eq!(app.ecr_state.repositories.selected, 100);
13373
13374        // Right arrow at last page should stay at last page
13375        app.handle_action(Action::PageDown);
13376        eprintln!(
13377            "After 3rd PageDown: selected={}",
13378            app.ecr_state.repositories.selected
13379        );
13380        assert_eq!(app.ecr_state.repositories.selected, 100);
13381
13382        // Left arrow (PageUp) should go back to page 2
13383        app.handle_action(Action::PageUp);
13384        eprintln!(
13385            "After PageUp: selected={}",
13386            app.ecr_state.repositories.selected
13387        );
13388        assert_eq!(app.ecr_state.repositories.selected, 50);
13389
13390        // Left arrow again should go to page 1
13391        app.handle_action(Action::PageUp);
13392        eprintln!(
13393            "After 2nd PageUp: selected={}",
13394            app.ecr_state.repositories.selected
13395        );
13396        assert_eq!(app.ecr_state.repositories.selected, 0);
13397
13398        // Left arrow at first page should stay at first page
13399        app.handle_action(Action::PageUp);
13400        eprintln!(
13401            "After 3rd PageUp: selected={}",
13402            app.ecr_state.repositories.selected
13403        );
13404        assert_eq!(app.ecr_state.repositories.selected, 0);
13405    }
13406
13407    #[test]
13408    fn test_ecr_repos_filter_input_when_input_focused() {
13409        use crate::ecr::repo::Repository as EcrRepository;
13410        let mut app = test_app();
13411        app.current_service = Service::EcrRepositories;
13412        app.service_selected = true;
13413        app.mode = Mode::FilterInput;
13414        app.ecr_state.input_focus = InputFocus::Filter;
13415
13416        // Create some repos
13417        app.ecr_state.repositories.items = vec![
13418            EcrRepository {
13419                name: "test-repo".to_string(),
13420                uri: "uri1".to_string(),
13421                created_at: "2023-01-01".to_string(),
13422                tag_immutability: "MUTABLE".to_string(),
13423                encryption_type: "AES256".to_string(),
13424            },
13425            EcrRepository {
13426                name: "prod-repo".to_string(),
13427                uri: "uri2".to_string(),
13428                created_at: "2023-01-01".to_string(),
13429                tag_immutability: "MUTABLE".to_string(),
13430                encryption_type: "AES256".to_string(),
13431            },
13432        ];
13433
13434        // When input is focused, typing should add to filter
13435        assert_eq!(app.ecr_state.repositories.filter, "");
13436        app.handle_action(Action::FilterInput('t'));
13437        assert_eq!(app.ecr_state.repositories.filter, "t");
13438        app.handle_action(Action::FilterInput('e'));
13439        assert_eq!(app.ecr_state.repositories.filter, "te");
13440        app.handle_action(Action::FilterInput('s'));
13441        assert_eq!(app.ecr_state.repositories.filter, "tes");
13442        app.handle_action(Action::FilterInput('t'));
13443        assert_eq!(app.ecr_state.repositories.filter, "test");
13444    }
13445
13446    #[test]
13447    fn test_ecr_repos_digit_input_when_pagination_focused() {
13448        use crate::ecr::repo::Repository as EcrRepository;
13449        let mut app = test_app();
13450        app.current_service = Service::EcrRepositories;
13451        app.service_selected = true;
13452        app.mode = Mode::FilterInput;
13453        app.ecr_state.input_focus = InputFocus::Pagination;
13454
13455        // Create some repos
13456        app.ecr_state.repositories.items = vec![EcrRepository {
13457            name: "test-repo".to_string(),
13458            uri: "uri1".to_string(),
13459            created_at: "2023-01-01".to_string(),
13460            tag_immutability: "MUTABLE".to_string(),
13461            encryption_type: "AES256".to_string(),
13462        }];
13463
13464        // When pagination is focused, digits should go to page_input, not filter
13465        assert_eq!(app.ecr_state.repositories.filter, "");
13466        assert_eq!(app.page_input, "");
13467        app.handle_action(Action::FilterInput('2'));
13468        assert_eq!(app.ecr_state.repositories.filter, "");
13469        assert_eq!(app.page_input, "2");
13470
13471        // Non-digits should not be added to either
13472        app.handle_action(Action::FilterInput('a'));
13473        assert_eq!(app.ecr_state.repositories.filter, "");
13474        assert_eq!(app.page_input, "2");
13475    }
13476
13477    #[test]
13478    fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
13479        use crate::ecr::repo::Repository as EcrRepository;
13480        let mut app = test_app();
13481        app.current_service = Service::EcrRepositories;
13482        app.service_selected = true;
13483        app.mode = Mode::FilterInput;
13484        app.ecr_state.input_focus = InputFocus::Filter;
13485
13486        // Create 150 repos (3 pages)
13487        app.ecr_state.repositories.items = (0..150)
13488            .map(|i| EcrRepository {
13489                name: format!("repo{:03}", i),
13490                uri: format!("uri{}", i),
13491                created_at: "2023-01-01".to_string(),
13492                tag_immutability: "MUTABLE".to_string(),
13493                encryption_type: "AES256".to_string(),
13494            })
13495            .collect();
13496
13497        // Start on page 1
13498        app.ecr_state.repositories.selected = 0;
13499
13500        // When input is focused, left/right should scroll table (not change pages)
13501        app.handle_action(Action::PageDown);
13502        assert_eq!(
13503            app.ecr_state.repositories.selected, 10,
13504            "Should scroll down by 10"
13505        );
13506
13507        app.handle_action(Action::PageUp);
13508        assert_eq!(
13509            app.ecr_state.repositories.selected, 0,
13510            "Should scroll back up"
13511        );
13512    }
13513
13514    #[test]
13515    fn test_ecr_repos_pagination_control_actually_works() {
13516        use crate::ecr::repo::Repository as EcrRepository;
13517
13518        // Test that verifies the exact conditions needed for pagination to work
13519        let mut app = test_app();
13520        app.current_service = Service::EcrRepositories;
13521        app.service_selected = true;
13522        app.mode = Mode::FilterInput;
13523        app.ecr_state.current_repository = None;
13524        app.ecr_state.input_focus = InputFocus::Pagination;
13525
13526        // Create 100 repos (2 pages with page size 50)
13527        app.ecr_state.repositories.items = (0..100)
13528            .map(|i| EcrRepository {
13529                name: format!("repo{:03}", i),
13530                uri: format!("uri{}", i),
13531                created_at: "2023-01-01".to_string(),
13532                tag_immutability: "MUTABLE".to_string(),
13533                encryption_type: "AES256".to_string(),
13534            })
13535            .collect();
13536
13537        app.ecr_state.repositories.selected = 0;
13538
13539        // Verify all conditions are met
13540        assert_eq!(app.mode, Mode::FilterInput);
13541        assert_eq!(app.current_service, Service::EcrRepositories);
13542        assert_eq!(app.ecr_state.current_repository, None);
13543        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13544
13545        // Now test pagination
13546        app.handle_action(Action::PageDown);
13547        assert_eq!(
13548            app.ecr_state.repositories.selected, 50,
13549            "PageDown should move to page 2"
13550        );
13551
13552        app.handle_action(Action::PageUp);
13553        assert_eq!(
13554            app.ecr_state.repositories.selected, 0,
13555            "PageUp should move back to page 1"
13556        );
13557    }
13558
13559    #[test]
13560    fn test_ecr_repos_can_reach_last_page_when_not_multiple_of_page_size() {
13561        use crate::ecr::repo::Repository as EcrRepository;
13562        let mut app = test_app();
13563        app.current_service = Service::EcrRepositories;
13564        app.service_selected = true;
13565        app.mode = Mode::FilterInput;
13566        app.ecr_state.input_focus = InputFocus::Pagination;
13567
13568        // 754 repos, page size 50 — last page starts at 750 (page 16)
13569        app.ecr_state.repositories.items = (0..754)
13570            .map(|i| EcrRepository {
13571                name: format!("repo{:03}", i),
13572                uri: format!("uri{}", i),
13573                created_at: "2023-01-01".to_string(),
13574                tag_immutability: "MUTABLE".to_string(),
13575                encryption_type: "AES256".to_string(),
13576            })
13577            .collect();
13578
13579        // Navigate to page 15 (selected = 700)
13580        app.ecr_state.repositories.selected = 700;
13581
13582        // Right arrow must reach page 16 (selected = 750)
13583        app.handle_action(Action::PageDown);
13584        assert_eq!(
13585            app.ecr_state.repositories.selected, 750,
13586            "Should reach last page (750) but got {}",
13587            app.ecr_state.repositories.selected
13588        );
13589
13590        // Right arrow again must stay at 750 (can't go past last page)
13591        app.handle_action(Action::PageDown);
13592        assert_eq!(app.ecr_state.repositories.selected, 750);
13593    }
13594
13595    #[test]
13596    fn test_ecr_repos_start_filter_resets_focus_to_input() {
13597        let mut app = test_app();
13598        app.current_service = Service::EcrRepositories;
13599        app.service_selected = true;
13600        app.mode = Mode::Normal;
13601        app.ecr_state.current_repository = None;
13602
13603        // Set focus to Pagination
13604        app.ecr_state.input_focus = InputFocus::Pagination;
13605
13606        // Start filter mode
13607        app.handle_action(Action::StartFilter);
13608
13609        // Should reset to Input focus
13610        assert_eq!(app.mode, Mode::FilterInput);
13611        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13612    }
13613
13614    #[test]
13615    fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
13616        use crate::ecr::repo::Repository as EcrRepository;
13617
13618        let mut app = test_app();
13619        app.current_service = Service::EcrRepositories;
13620        app.service_selected = true;
13621        app.mode = Mode::Normal;
13622        app.ecr_state.current_repository = None;
13623
13624        // Create 100 repos (2 pages)
13625        app.ecr_state.repositories.items = (0..100)
13626            .map(|i| EcrRepository {
13627                name: format!("repo{:03}", i),
13628                uri: format!("uri{}", i),
13629                created_at: "2023-01-01".to_string(),
13630                tag_immutability: "MUTABLE".to_string(),
13631                encryption_type: "AES256".to_string(),
13632            })
13633            .collect();
13634
13635        app.ecr_state.repositories.selected = 0;
13636
13637        // User presses 'i' to enter filter mode
13638        app.handle_action(Action::StartFilter);
13639        assert_eq!(app.mode, Mode::FilterInput);
13640        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
13641
13642        // User presses Tab to switch to pagination
13643        app.handle_action(Action::NextFilterFocus);
13644        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
13645
13646        // User presses right arrow (PageDown)
13647        eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
13648            app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
13649        app.handle_action(Action::PageDown);
13650        eprintln!(
13651            "After PageDown: selected={}",
13652            app.ecr_state.repositories.selected
13653        );
13654
13655        // Should move to page 2
13656        assert_eq!(
13657            app.ecr_state.repositories.selected, 50,
13658            "Right arrow should move to page 2"
13659        );
13660
13661        // User presses left arrow (PageUp)
13662        app.handle_action(Action::PageUp);
13663        assert_eq!(
13664            app.ecr_state.repositories.selected, 0,
13665            "Left arrow should move back to page 1"
13666        );
13667    }
13668
13669    #[test]
13670    fn test_apig_filter_input() {
13671        let mut app = test_app();
13672        app.current_service = Service::ApiGatewayApis;
13673        app.service_selected = true;
13674        app.mode = Mode::FilterInput;
13675
13676        app.handle_action(Action::FilterInput('t'));
13677        app.handle_action(Action::FilterInput('e'));
13678        app.handle_action(Action::FilterInput('s'));
13679        app.handle_action(Action::FilterInput('t'));
13680        assert_eq!(app.apig_state.apis.filter, "test");
13681
13682        app.handle_action(Action::FilterBackspace);
13683        assert_eq!(app.apig_state.apis.filter, "tes");
13684    }
13685
13686    #[test]
13687    fn test_apig_start_filter_enters_filter_mode() {
13688        let mut app = test_app();
13689        app.current_service = Service::ApiGatewayApis;
13690        app.service_selected = true;
13691        app.mode = Mode::Normal;
13692
13693        app.handle_action(Action::StartFilter);
13694        assert_eq!(app.mode, Mode::FilterInput);
13695        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13696    }
13697
13698    #[test]
13699    fn test_apig_input_focus_cycles_with_tab() {
13700        let mut app = test_app();
13701        app.current_service = Service::ApiGatewayApis;
13702        app.service_selected = true;
13703        app.mode = Mode::FilterInput;
13704        app.apig_state.input_focus = InputFocus::Filter;
13705
13706        // Tab cycles from Filter to Pagination
13707        app.handle_action(Action::NextFilterFocus);
13708        assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
13709
13710        // Tab cycles back to Filter
13711        app.handle_action(Action::NextFilterFocus);
13712        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13713    }
13714
13715    #[test]
13716    fn test_apig_input_focus_cycles_with_shift_tab() {
13717        let mut app = test_app();
13718        app.current_service = Service::ApiGatewayApis;
13719        app.service_selected = true;
13720        app.mode = Mode::FilterInput;
13721        app.apig_state.input_focus = InputFocus::Filter;
13722
13723        // Shift+Tab cycles from Filter to Pagination (backward)
13724        app.handle_action(Action::PrevFilterFocus);
13725        assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
13726
13727        // Shift+Tab cycles back to Filter
13728        app.handle_action(Action::PrevFilterFocus);
13729        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13730    }
13731
13732    #[test]
13733    fn test_apig_exact_user_flow_i_tab_filter() {
13734        let mut app = test_app();
13735        app.current_service = Service::ApiGatewayApis;
13736        app.service_selected = true;
13737        app.mode = Mode::Normal;
13738
13739        // Create test APIs
13740        app.apig_state.apis.items = vec![
13741            crate::apig::api::RestApi {
13742                id: "api1".to_string(),
13743                name: "test-api".to_string(),
13744                description: "Test API".to_string(),
13745                created_date: "2023-01-01".to_string(),
13746                api_key_source: "HEADER".to_string(),
13747                endpoint_configuration: "REGIONAL".to_string(),
13748                protocol_type: "REST".to_string(),
13749                disable_execute_api_endpoint: false,
13750                status: "AVAILABLE".to_string(),
13751            },
13752            crate::apig::api::RestApi {
13753                id: "api2".to_string(),
13754                name: "prod-api".to_string(),
13755                description: "Production API".to_string(),
13756                created_date: "2023-01-02".to_string(),
13757                api_key_source: "HEADER".to_string(),
13758                endpoint_configuration: "REGIONAL".to_string(),
13759                protocol_type: "REST".to_string(),
13760                disable_execute_api_endpoint: false,
13761                status: "AVAILABLE".to_string(),
13762            },
13763        ];
13764
13765        // User presses 'i' to enter filter mode
13766        app.handle_action(Action::StartFilter);
13767        assert_eq!(app.mode, Mode::FilterInput);
13768        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13769
13770        // User types "test"
13771        app.handle_action(Action::FilterInput('t'));
13772        app.handle_action(Action::FilterInput('e'));
13773        app.handle_action(Action::FilterInput('s'));
13774        app.handle_action(Action::FilterInput('t'));
13775        assert_eq!(app.apig_state.apis.filter, "test");
13776
13777        // User presses Tab to switch to pagination
13778        app.handle_action(Action::NextFilterFocus);
13779        assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
13780
13781        // User presses Tab again to go back to filter
13782        app.handle_action(Action::NextFilterFocus);
13783        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
13784    }
13785
13786    #[test]
13787    fn test_apig_row_expansion() {
13788        let mut app = test_app();
13789        app.current_service = Service::ApiGatewayApis;
13790        app.service_selected = true;
13791        app.apig_state.apis.items = vec![crate::apig::api::RestApi {
13792            id: "api1".to_string(),
13793            name: "test-api".to_string(),
13794            description: "Test API".to_string(),
13795            created_date: "2023-01-01".to_string(),
13796            api_key_source: "HEADER".to_string(),
13797            endpoint_configuration: "REGIONAL".to_string(),
13798            protocol_type: "REST".to_string(),
13799            disable_execute_api_endpoint: false,
13800            status: "AVAILABLE".to_string(),
13801        }];
13802        app.apig_state.apis.selected = 0;
13803
13804        assert_eq!(app.apig_state.apis.expanded_item, None);
13805
13806        // Right arrow (NextPane) should NOT expand rows
13807        app.handle_action(Action::NextPane);
13808        assert_eq!(app.apig_state.apis.expanded_item, None);
13809
13810        // Manually expand using ExpandRow action
13811        app.handle_action(Action::ExpandRow);
13812        assert_eq!(app.apig_state.apis.expanded_item, Some(0));
13813
13814        // Right arrow should NOT collapse expanded row
13815        app.handle_action(Action::NextPane);
13816        assert_eq!(app.apig_state.apis.expanded_item, Some(0));
13817
13818        // Left arrow (PrevPane) should collapse
13819        app.handle_action(Action::PrevPane);
13820        assert_eq!(app.apig_state.apis.expanded_item, None);
13821    }
13822
13823    #[test]
13824    fn test_apig_column_preferences() {
13825        let mut app = test_app();
13826        app.current_service = Service::ApiGatewayApis;
13827        app.service_selected = true;
13828
13829        // All columns should be visible by default
13830        let initial_count = app.apig_api_visible_column_ids.len();
13831        assert_eq!(initial_count, app.apig_api_column_ids.len());
13832
13833        // Open preferences
13834        app.handle_action(Action::OpenColumnSelector);
13835        assert_eq!(app.mode, Mode::ColumnSelector);
13836
13837        // Toggle first column (index 1, since 0 is header)
13838        app.column_selector_index = 1;
13839        let first_col = app.apig_api_column_ids[0];
13840        assert!(app.apig_api_visible_column_ids.contains(&first_col));
13841
13842        app.handle_action(Action::ToggleColumn);
13843
13844        // First column should now be hidden
13845        assert!(!app.apig_api_visible_column_ids.contains(&first_col));
13846        assert_eq!(app.apig_api_visible_column_ids.len(), initial_count - 1);
13847
13848        // Toggle it back
13849        app.handle_action(Action::ToggleColumn);
13850        assert!(app.apig_api_visible_column_ids.contains(&first_col));
13851        assert_eq!(app.apig_api_visible_column_ids.len(), initial_count);
13852    }
13853
13854    #[test]
13855    fn test_apig_page_size_preferences() {
13856        use crate::common::PageSize;
13857
13858        let mut app = test_app();
13859        app.current_service = Service::ApiGatewayApis;
13860        app.service_selected = true;
13861
13862        // Default page size
13863        assert_eq!(app.apig_state.apis.page_size, PageSize::Fifty);
13864
13865        // Open preferences
13866        app.handle_action(Action::OpenColumnSelector);
13867
13868        // Page size options start after: header (1) + columns (8) + blank line (1) + "Page Size" header (1) = 11
13869        // So indices are: 11 (header), 12 (10), 13 (25), 14 (50), 15 (100)
13870        // But the code uses: column_count + 3, +4, +5, +6
13871        // With 8 columns: 11, 12, 13, 14
13872        app.column_selector_index = app.apig_api_column_ids.len() + 3;
13873        app.handle_action(Action::ToggleColumn);
13874        assert_eq!(app.apig_state.apis.page_size, PageSize::Ten);
13875
13876        // Select page size 100
13877        app.column_selector_index = app.apig_api_column_ids.len() + 6;
13878        app.handle_action(Action::ToggleColumn);
13879        assert_eq!(app.apig_state.apis.page_size, PageSize::OneHundred);
13880    }
13881
13882    #[test]
13883    fn test_apig_preferences_skip_blank_row() {
13884        let mut app = test_app();
13885        app.current_service = Service::ApiGatewayApis;
13886        app.service_selected = true;
13887        app.mode = Mode::ColumnSelector;
13888
13889        // Start at last column (index 8)
13890        app.column_selector_index = 8;
13891
13892        // Next should skip blank row (9) and go to page size header (10)
13893        app.handle_action(Action::NextItem);
13894        assert_eq!(app.column_selector_index, 10);
13895
13896        // Prev should skip blank row (9) and go back to last column (8)
13897        app.handle_action(Action::PrevItem);
13898        assert_eq!(app.column_selector_index, 8);
13899    }
13900
13901    #[test]
13902    fn test_apig_preferences_ctrl_d_skip_blank_row() {
13903        let mut app = test_app();
13904        app.current_service = Service::ApiGatewayApis;
13905        app.service_selected = true;
13906        app.mode = Mode::ColumnSelector;
13907
13908        // Start at index 0
13909        app.column_selector_index = 0;
13910
13911        // Ctrl+D (PageDown) by 10 would land on blank row (9), should skip to 10
13912        app.handle_action(Action::PageDown);
13913        assert_eq!(app.column_selector_index, 10);
13914
13915        // Ctrl+U (PageUp) by 10 would land on 0
13916        app.handle_action(Action::PageUp);
13917        assert_eq!(app.column_selector_index, 0);
13918    }
13919
13920    #[test]
13921    fn test_apig_preferences_ctrl_u_skip_blank_row() {
13922        let mut app = test_app();
13923        app.current_service = Service::ApiGatewayApis;
13924        app.service_selected = true;
13925        app.mode = Mode::ColumnSelector;
13926
13927        // Start at page size header (index 10)
13928        app.column_selector_index = 10;
13929
13930        // Ctrl+U (PageUp) by 10 would land on 0
13931        app.handle_action(Action::PageUp);
13932        assert_eq!(app.column_selector_index, 0);
13933
13934        // Go to index 19 (if it exists, otherwise use 14 which is max)
13935        app.column_selector_index = 14; // Max index for APIG
13936
13937        // Ctrl+U (PageUp) by 10 would land on 4
13938        app.handle_action(Action::PageUp);
13939        assert_eq!(app.column_selector_index, 4);
13940    }
13941
13942    #[test]
13943    fn test_apig_preferences_tab_cycles_sections() {
13944        let mut app = test_app();
13945        app.current_service = Service::ApiGatewayApis;
13946        app.service_selected = true;
13947        app.mode = Mode::ColumnSelector;
13948
13949        // Start at columns header (0)
13950        app.column_selector_index = 0;
13951
13952        // Tab should jump to page size section (column_count + 2 = 10)
13953        app.handle_action(Action::NextPreferences);
13954        assert_eq!(app.column_selector_index, 10);
13955
13956        // Tab again should wrap back to columns (0)
13957        app.handle_action(Action::NextPreferences);
13958        assert_eq!(app.column_selector_index, 0);
13959    }
13960
13961    #[test]
13962    fn test_apig_preferences_shift_tab_cycles_sections() {
13963        let mut app = test_app();
13964        app.current_service = Service::ApiGatewayApis;
13965        app.service_selected = true;
13966        app.mode = Mode::ColumnSelector;
13967
13968        // Start at columns header (0)
13969        app.column_selector_index = 0;
13970
13971        // Shift+Tab should jump to page size section (10)
13972        app.handle_action(Action::PrevPreferences);
13973        assert_eq!(app.column_selector_index, 10);
13974
13975        // Shift+Tab again should wrap back to columns (0)
13976        app.handle_action(Action::PrevPreferences);
13977        assert_eq!(app.column_selector_index, 0);
13978    }
13979
13980    #[test]
13981    fn test_apig_arrow_navigation() {
13982        let mut app = test_app();
13983        app.current_service = Service::ApiGatewayApis;
13984        app.service_selected = true;
13985        app.mode = Mode::Normal; // Ensure we're in Normal mode
13986        app.apig_state.apis.filter = String::new(); // Ensure filter is empty
13987        app.apig_state.apis.items = vec![
13988            crate::apig::api::RestApi {
13989                id: "api1".to_string(),
13990                name: "test-api-1".to_string(),
13991                description: "Test API 1".to_string(),
13992                created_date: "2023-01-01".to_string(),
13993                api_key_source: "HEADER".to_string(),
13994                endpoint_configuration: "REGIONAL".to_string(),
13995                protocol_type: "REST".to_string(),
13996                disable_execute_api_endpoint: false,
13997                status: "AVAILABLE".to_string(),
13998            },
13999            crate::apig::api::RestApi {
14000                id: "api2".to_string(),
14001                name: "test-api-2".to_string(),
14002                description: "Test API 2".to_string(),
14003                created_date: "2023-01-02".to_string(),
14004                api_key_source: "HEADER".to_string(),
14005                endpoint_configuration: "REGIONAL".to_string(),
14006                protocol_type: "HTTP".to_string(),
14007                disable_execute_api_endpoint: false,
14008                status: "AVAILABLE".to_string(),
14009            },
14010        ];
14011        app.apig_state.apis.selected = 0;
14012
14013        // Down arrow should move to next item
14014        app.handle_action(Action::NextItem);
14015        assert_eq!(app.apig_state.apis.selected, 1);
14016
14017        // Up arrow should move to previous item
14018        app.handle_action(Action::PrevItem);
14019        assert_eq!(app.apig_state.apis.selected, 0);
14020    }
14021
14022    #[test]
14023    fn test_apig_collapse_row() {
14024        let mut app = test_app();
14025        app.current_service = Service::ApiGatewayApis;
14026        app.service_selected = true;
14027        app.apig_state.apis.items = vec![crate::apig::api::RestApi {
14028            id: "api1".to_string(),
14029            name: "test-api".to_string(),
14030            description: "Test API".to_string(),
14031            created_date: "2023-01-01".to_string(),
14032            api_key_source: "HEADER".to_string(),
14033            endpoint_configuration: "REGIONAL".to_string(),
14034            protocol_type: "REST".to_string(),
14035            disable_execute_api_endpoint: false,
14036            status: "AVAILABLE".to_string(),
14037        }];
14038        app.apig_state.apis.selected = 0;
14039        app.apig_state.apis.expanded_item = Some(0);
14040
14041        // Left arrow should collapse
14042        app.handle_action(Action::CollapseRow);
14043        assert_eq!(app.apig_state.apis.expanded_item, None);
14044    }
14045
14046    #[test]
14047    fn test_apig_filter_resets_selection() {
14048        let mut app = test_app();
14049        app.current_service = Service::ApiGatewayApis;
14050        app.service_selected = true;
14051        app.mode = Mode::FilterInput;
14052        app.apig_state.input_focus = InputFocus::Filter;
14053        app.apig_state.apis.items = vec![
14054            crate::apig::api::RestApi {
14055                id: "api1".to_string(),
14056                name: "alpha-api".to_string(),
14057                description: "Alpha API".to_string(),
14058                created_date: "2023-01-01".to_string(),
14059                api_key_source: "HEADER".to_string(),
14060                endpoint_configuration: "REGIONAL".to_string(),
14061                protocol_type: "REST".to_string(),
14062                disable_execute_api_endpoint: false,
14063                status: "AVAILABLE".to_string(),
14064            },
14065            crate::apig::api::RestApi {
14066                id: "api2".to_string(),
14067                name: "beta-api".to_string(),
14068                description: "Beta API".to_string(),
14069                created_date: "2023-01-02".to_string(),
14070                api_key_source: "HEADER".to_string(),
14071                endpoint_configuration: "REGIONAL".to_string(),
14072                protocol_type: "HTTP".to_string(),
14073                disable_execute_api_endpoint: false,
14074                status: "AVAILABLE".to_string(),
14075            },
14076        ];
14077
14078        // Select second item and expand it
14079        app.apig_state.apis.selected = 1;
14080        app.apig_state.apis.expanded_item = Some(1);
14081
14082        // Type a filter character
14083        app.handle_action(Action::FilterInput('a'));
14084
14085        // Selection and expansion should be reset
14086        assert_eq!(app.apig_state.apis.selected, 0);
14087        assert_eq!(app.apig_state.apis.expanded_item, None);
14088        assert_eq!(app.apig_state.apis.filter, "a");
14089    }
14090
14091    #[test]
14092    fn test_service_picker_starts_in_normal_mode() {
14093        let app = test_app();
14094        assert_eq!(app.mode, Mode::ServicePicker);
14095        assert!(!app.service_picker.filter_active);
14096    }
14097
14098    #[test]
14099    fn test_service_picker_i_key_activates_filter() {
14100        let mut app = test_app();
14101
14102        // Start in ServicePicker mode (Normal mode - filter not active)
14103        assert_eq!(app.mode, Mode::ServicePicker);
14104        assert!(!app.service_picker.filter_active);
14105        assert!(app.service_picker.filter.is_empty());
14106
14107        // Press 'i' to enter filter mode
14108        app.handle_action(Action::EnterFilterMode);
14109
14110        // Should still be in ServicePicker mode but filter should be active
14111        assert_eq!(app.mode, Mode::ServicePicker);
14112        assert!(app.service_picker.filter_active);
14113        assert!(app.service_picker.filter.is_empty());
14114
14115        // Now typing should work
14116        app.handle_action(Action::FilterInput('s'));
14117        assert_eq!(app.service_picker.filter, "s");
14118    }
14119
14120    #[test]
14121    fn test_service_picker_esc_exits_filter_mode() {
14122        let mut app = test_app();
14123        assert_eq!(app.mode, Mode::ServicePicker);
14124        assert!(!app.service_picker.filter_active);
14125
14126        // Enter filter mode
14127        app.handle_action(Action::EnterFilterMode);
14128        assert!(app.service_picker.filter_active);
14129        assert_eq!(app.mode, Mode::ServicePicker);
14130
14131        // Type something
14132        app.handle_action(Action::FilterInput('s'));
14133        assert_eq!(app.service_picker.filter, "s");
14134
14135        // Esc should exit filter mode (not close menu)
14136        app.handle_action(Action::ExitFilterMode);
14137        assert!(!app.service_picker.filter_active);
14138        assert_eq!(app.mode, Mode::ServicePicker); // Still in picker
14139
14140        // Second Esc should close menu
14141        app.handle_action(Action::ExitFilterMode);
14142        assert_eq!(app.mode, Mode::Normal);
14143    }
14144
14145    #[test]
14146    fn test_service_picker_navigation_only_works_in_normal_mode() {
14147        let mut app = test_app();
14148        app.service_picker.selected = 0;
14149
14150        // In normal mode, navigation should work
14151        assert!(!app.service_picker.filter_active);
14152        app.handle_action(Action::NextItem);
14153        assert_eq!(app.service_picker.selected, 1);
14154
14155        // Enter filter mode
14156        app.handle_action(Action::EnterFilterMode);
14157        assert!(app.service_picker.filter_active);
14158
14159        // In filter mode, navigation should NOT work
14160        let prev_selected = app.service_picker.selected;
14161        app.handle_action(Action::NextItem);
14162        assert_eq!(app.service_picker.selected, prev_selected); // Unchanged
14163
14164        // Exit filter mode
14165        app.handle_action(Action::ExitFilterMode);
14166        assert!(!app.service_picker.filter_active);
14167
14168        // Navigation should work again
14169        app.handle_action(Action::NextItem);
14170        assert_eq!(app.service_picker.selected, prev_selected + 1);
14171    }
14172
14173    #[test]
14174    fn test_service_picker_typing_filters_services() {
14175        let mut app = test_app();
14176
14177        // Start in ServicePicker mode
14178        assert_eq!(app.mode, Mode::ServicePicker);
14179        assert!(!app.service_picker.filter_active);
14180
14181        // Enter filter mode
14182        app.handle_action(Action::EnterFilterMode);
14183        assert!(app.service_picker.filter_active);
14184
14185        // Type "s3" to filter
14186        app.handle_action(Action::FilterInput('s'));
14187        app.handle_action(Action::FilterInput('3'));
14188
14189        assert_eq!(app.service_picker.filter, "s3");
14190        assert_eq!(app.mode, Mode::ServicePicker);
14191    }
14192
14193    #[test]
14194    fn test_service_picker_enter_exits_insert_mode() {
14195        let mut app = test_app();
14196        assert_eq!(app.mode, Mode::ServicePicker);
14197        assert!(!app.service_selected);
14198
14199        // Enter filter mode (INSERT mode)
14200        app.handle_action(Action::EnterFilterMode);
14201        assert!(app.service_picker.filter_active);
14202
14203        // Type to filter
14204        app.handle_action(Action::FilterInput('s'));
14205        app.handle_action(Action::FilterInput('3'));
14206
14207        // Press Enter - should exit INSERT mode, not select service
14208        app.handle_action(Action::Select);
14209        assert!(!app.service_selected, "Should not select service");
14210        assert!(!app.service_picker.filter_active, "Should exit INSERT mode");
14211        assert_eq!(app.mode, Mode::ServicePicker);
14212
14213        // Now press Enter again in NORMAL mode - should select service
14214        app.handle_action(Action::Select);
14215        assert!(app.service_selected, "Should select service in NORMAL mode");
14216        assert_eq!(app.mode, Mode::Normal);
14217    }
14218
14219    #[test]
14220    fn test_service_picker_resets_on_open() {
14221        let mut app = test_app();
14222
14223        // Select a service to get into Normal mode
14224        app.service_selected = true;
14225        app.mode = Mode::Normal;
14226
14227        // Simulate having previous filter and selection
14228        app.service_picker.filter = "previous".to_string();
14229        app.service_picker.filter_active = true;
14230        app.service_picker.selected = 5;
14231
14232        // Open space menu (service picker)
14233        app.handle_action(Action::OpenSpaceMenu);
14234
14235        // Filter, filter_active, and selection should be reset
14236        assert_eq!(app.mode, Mode::SpaceMenu);
14237        assert!(app.service_picker.filter.is_empty());
14238        assert!(!app.service_picker.filter_active);
14239        assert_eq!(app.service_picker.selected, 0);
14240    }
14241
14242    #[test]
14243    fn test_no_pii_in_test_data() {
14244        // Ensure test data uses placeholder account IDs, not real ones
14245        let test_repo = EcrRepository {
14246            name: "test-repo".to_string(),
14247            uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
14248            created_at: "2024-01-01".to_string(),
14249            tag_immutability: "MUTABLE".to_string(),
14250            encryption_type: "AES256".to_string(),
14251        };
14252
14253        // Verify placeholder account ID is used
14254        assert!(test_repo.uri.starts_with("123456789012"));
14255        assert!(!test_repo.uri.contains("123456789013")); // Not a real account
14256    }
14257
14258    #[test]
14259    fn test_lambda_versions_tab_triggers_loading() {
14260        let mut app = test_app();
14261        app.current_service = Service::LambdaFunctions;
14262        app.service_selected = true;
14263
14264        // Simulate selecting a function
14265        app.lambda_state.current_function = Some("test-function".to_string());
14266        app.lambda_state.detail_tab = LambdaDetailTab::Code;
14267
14268        // Initially no versions
14269        assert!(app.lambda_state.version_table.items.is_empty());
14270
14271        // Switch to Versions tab
14272        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14273
14274        // The main loop should detect this change and load versions
14275        // We verify the state is set up correctly for loading
14276        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
14277        assert!(app.lambda_state.current_function.is_some());
14278    }
14279
14280    #[test]
14281    fn test_lambda_versions_navigation() {
14282        let mut app = test_app();
14283        app.current_service = Service::LambdaFunctions;
14284        app.service_selected = true;
14285        app.lambda_state.current_function = Some("test-function".to_string());
14286        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14287
14288        // Add test versions
14289        app.lambda_state.version_table.items = vec![
14290            LambdaVersion {
14291                version: "3".to_string(),
14292                aliases: "prod".to_string(),
14293                description: "".to_string(),
14294                last_modified: "".to_string(),
14295                architecture: "X86_64".to_string(),
14296            },
14297            LambdaVersion {
14298                version: "2".to_string(),
14299                aliases: "".to_string(),
14300                description: "".to_string(),
14301                last_modified: "".to_string(),
14302                architecture: "X86_64".to_string(),
14303            },
14304            LambdaVersion {
14305                version: "1".to_string(),
14306                aliases: "".to_string(),
14307                description: "".to_string(),
14308                last_modified: "".to_string(),
14309                architecture: "X86_64".to_string(),
14310            },
14311        ];
14312
14313        // Verify versions are loaded
14314        assert_eq!(app.lambda_state.version_table.items.len(), 3);
14315        assert_eq!(app.lambda_state.version_table.items[0].version, "3");
14316        assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
14317
14318        // Verify selection can be changed
14319        app.lambda_state.version_table.selected = 1;
14320        assert_eq!(app.lambda_state.version_table.selected, 1);
14321    }
14322
14323    #[test]
14324    fn test_lambda_versions_with_aliases() {
14325        let version = LambdaVersion {
14326            version: "35".to_string(),
14327            aliases: "prod, staging".to_string(),
14328            description: "Production version".to_string(),
14329            last_modified: "2024-01-01".to_string(),
14330            architecture: "X86_64".to_string(),
14331        };
14332
14333        assert_eq!(version.aliases, "prod, staging");
14334        assert!(!version.aliases.is_empty());
14335    }
14336
14337    #[test]
14338    fn test_lambda_versions_expansion() {
14339        let mut app = test_app();
14340        app.current_service = Service::LambdaFunctions;
14341        app.service_selected = true;
14342        app.lambda_state.current_function = Some("test-function".to_string());
14343        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14344
14345        // Add test versions
14346        app.lambda_state.version_table.items = vec![
14347            LambdaVersion {
14348                version: "2".to_string(),
14349                aliases: "prod".to_string(),
14350                description: "Production".to_string(),
14351                last_modified: "2024-01-01".to_string(),
14352                architecture: "X86_64".to_string(),
14353            },
14354            LambdaVersion {
14355                version: "1".to_string(),
14356                aliases: "".to_string(),
14357                description: "".to_string(),
14358                last_modified: "2024-01-01".to_string(),
14359                architecture: "Arm64".to_string(),
14360            },
14361        ];
14362
14363        app.lambda_state.version_table.selected = 0;
14364
14365        // Verify expansion can be set
14366        app.lambda_state.version_table.expanded_item = Some(0);
14367        assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
14368
14369        // Select different version
14370        app.lambda_state.version_table.selected = 1;
14371        app.lambda_state.version_table.expanded_item = Some(1);
14372        assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
14373    }
14374
14375    #[test]
14376    fn test_lambda_versions_page_navigation() {
14377        let mut app = test_app();
14378        app.current_service = Service::LambdaFunctions;
14379        app.service_selected = true;
14380        app.lambda_state.current_function = Some("test-function".to_string());
14381        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14382
14383        // Add 30 test versions
14384        app.lambda_state.version_table.items = (1..=30)
14385            .map(|i| LambdaVersion {
14386                version: i.to_string(),
14387                aliases: "".to_string(),
14388                description: "".to_string(),
14389                last_modified: "".to_string(),
14390                architecture: "X86_64".to_string(),
14391            })
14392            .collect();
14393
14394        app.lambda_state.version_table.page_size = PageSize::Ten;
14395        app.lambda_state.version_table.selected = 0;
14396
14397        // Go to page 2
14398        app.page_input = "2".to_string();
14399        app.handle_action(Action::OpenColumnSelector);
14400
14401        // Should be at index 10 (start of page 2)
14402        assert_eq!(app.lambda_state.version_table.selected, 10);
14403    }
14404
14405    #[test]
14406    fn test_lambda_versions_pagination_arrow_keys() {
14407        let mut app = test_app();
14408        app.current_service = Service::LambdaFunctions;
14409        app.service_selected = true;
14410        app.lambda_state.current_function = Some("test-function".to_string());
14411        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14412        app.mode = Mode::FilterInput;
14413        app.lambda_state.version_input_focus = InputFocus::Pagination;
14414
14415        // Add 30 test versions
14416        app.lambda_state.version_table.items = (1..=30)
14417            .map(|i| LambdaVersion {
14418                version: i.to_string(),
14419                aliases: "".to_string(),
14420                description: "".to_string(),
14421                last_modified: "".to_string(),
14422                architecture: "X86_64".to_string(),
14423            })
14424            .collect();
14425
14426        app.lambda_state.version_table.page_size = PageSize::Ten;
14427        app.lambda_state.version_table.selected = 0;
14428
14429        // Right arrow (PageDown) should go to next page
14430        app.handle_action(Action::PageDown);
14431        assert_eq!(app.lambda_state.version_table.selected, 10);
14432
14433        // Left arrow (PageUp) should go back
14434        app.handle_action(Action::PageUp);
14435        assert_eq!(app.lambda_state.version_table.selected, 0);
14436    }
14437
14438    #[test]
14439    fn test_lambda_versions_page_input_in_filter_mode() {
14440        let mut app = test_app();
14441        app.current_service = Service::LambdaFunctions;
14442        app.service_selected = true;
14443        app.lambda_state.current_function = Some("test-function".to_string());
14444        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14445        app.mode = Mode::FilterInput;
14446        app.lambda_state.version_input_focus = InputFocus::Pagination;
14447
14448        // Add 30 test versions
14449        app.lambda_state.version_table.items = (1..=30)
14450            .map(|i| LambdaVersion {
14451                version: i.to_string(),
14452                aliases: "".to_string(),
14453                description: "".to_string(),
14454                last_modified: "".to_string(),
14455                architecture: "X86_64".to_string(),
14456            })
14457            .collect();
14458
14459        app.lambda_state.version_table.page_size = PageSize::Ten;
14460        app.lambda_state.version_table.selected = 0;
14461
14462        // Type "2" when focused on Pagination
14463        app.handle_action(Action::FilterInput('2'));
14464        assert_eq!(app.page_input, "2");
14465        assert_eq!(app.lambda_state.version_table.filter, ""); // Should not go to filter
14466
14467        // Press 'p' to go to page 2
14468        app.handle_action(Action::OpenColumnSelector);
14469        assert_eq!(app.lambda_state.version_table.selected, 10);
14470        assert_eq!(app.page_input, ""); // Should be cleared
14471    }
14472
14473    #[test]
14474    fn test_lambda_versions_filter_input() {
14475        let mut app = test_app();
14476        app.current_service = Service::LambdaFunctions;
14477        app.service_selected = true;
14478        app.lambda_state.current_function = Some("test-function".to_string());
14479        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14480        app.mode = Mode::FilterInput;
14481        app.lambda_state.version_input_focus = InputFocus::Filter;
14482
14483        // Add test versions
14484        app.lambda_state.version_table.items = vec![
14485            LambdaVersion {
14486                version: "1".to_string(),
14487                aliases: "prod".to_string(),
14488                description: "Production".to_string(),
14489                last_modified: "".to_string(),
14490                architecture: "X86_64".to_string(),
14491            },
14492            LambdaVersion {
14493                version: "2".to_string(),
14494                aliases: "staging".to_string(),
14495                description: "Staging".to_string(),
14496                last_modified: "".to_string(),
14497                architecture: "X86_64".to_string(),
14498            },
14499        ];
14500
14501        // Type filter text
14502        app.handle_action(Action::FilterInput('p'));
14503        app.handle_action(Action::FilterInput('r'));
14504        app.handle_action(Action::FilterInput('o'));
14505        app.handle_action(Action::FilterInput('d'));
14506        assert_eq!(app.lambda_state.version_table.filter, "prod");
14507
14508        // Backspace should work
14509        app.handle_action(Action::FilterBackspace);
14510        assert_eq!(app.lambda_state.version_table.filter, "pro");
14511    }
14512
14513    #[test]
14514    fn test_lambda_aliases_table_expansion() {
14515        use crate::lambda::Alias;
14516
14517        let mut app = test_app();
14518        app.current_service = Service::LambdaFunctions;
14519        app.service_selected = true;
14520        app.lambda_state.current_function = Some("test-function".to_string());
14521        app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
14522        app.mode = Mode::Normal;
14523
14524        app.lambda_state.alias_table.items = vec![
14525            Alias {
14526                name: "prod".to_string(),
14527                versions: "1".to_string(),
14528                description: "Production alias".to_string(),
14529            },
14530            Alias {
14531                name: "staging".to_string(),
14532                versions: "2".to_string(),
14533                description: "Staging alias".to_string(),
14534            },
14535        ];
14536
14537        app.lambda_state.alias_table.selected = 0;
14538
14539        // Select first alias - should open alias detail view (no tab change)
14540        app.handle_action(Action::Select);
14541        assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
14542        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
14543
14544        // Go back
14545        app.handle_action(Action::GoBack);
14546        assert_eq!(app.lambda_state.current_alias, None);
14547        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
14548
14549        // Select second alias
14550        app.lambda_state.alias_table.selected = 1;
14551        app.handle_action(Action::Select);
14552        assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
14553    }
14554
14555    #[test]
14556    fn test_lambda_versions_arrow_key_expansion() {
14557        let mut app = test_app();
14558        app.current_service = Service::LambdaFunctions;
14559        app.service_selected = true;
14560        app.lambda_state.current_function = Some("test-function".to_string());
14561        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14562        app.mode = Mode::Normal;
14563
14564        app.lambda_state.version_table.items = vec![LambdaVersion {
14565            version: "1".to_string(),
14566            aliases: "prod".to_string(),
14567            description: "Production".to_string(),
14568            last_modified: "2024-01-01".to_string(),
14569            architecture: "X86_64".to_string(),
14570        }];
14571
14572        app.lambda_state.version_table.selected = 0;
14573
14574        // Right arrow expands
14575        app.handle_action(Action::NextPane);
14576        assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
14577
14578        // Left arrow collapses
14579        app.handle_action(Action::PrevPane);
14580        assert_eq!(app.lambda_state.version_table.expanded_item, None);
14581    }
14582
14583    #[test]
14584    fn test_lambda_version_detail_view() {
14585        use crate::lambda::Function;
14586
14587        let mut app = test_app();
14588        app.current_service = Service::LambdaFunctions;
14589        app.service_selected = true;
14590        app.lambda_state.current_function = Some("test-function".to_string());
14591        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
14592        app.mode = Mode::Normal;
14593
14594        app.lambda_state.table.items = vec![Function {
14595            name: "test-function".to_string(),
14596            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14597            application: None,
14598            description: "Test".to_string(),
14599            package_type: "Zip".to_string(),
14600            runtime: "python3.12".to_string(),
14601            architecture: "X86_64".to_string(),
14602            code_size: 1024,
14603            code_sha256: "hash".to_string(),
14604            memory_mb: 128,
14605            timeout_seconds: 30,
14606            last_modified: "2024-01-01".to_string(),
14607            layers: vec![],
14608        }];
14609
14610        app.lambda_state.version_table.items = vec![LambdaVersion {
14611            version: "1".to_string(),
14612            aliases: "prod".to_string(),
14613            description: "Production".to_string(),
14614            last_modified: "2024-01-01".to_string(),
14615            architecture: "X86_64".to_string(),
14616        }];
14617
14618        app.lambda_state.version_table.selected = 0;
14619
14620        // Select version to open detail view
14621        app.handle_action(Action::Select);
14622        assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
14623        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14624
14625        // GoBack should go back to versions list
14626        app.handle_action(Action::GoBack);
14627        assert_eq!(app.lambda_state.current_version, None);
14628        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
14629    }
14630
14631    #[test]
14632    fn test_lambda_version_detail_tabs() {
14633        use crate::lambda::Function;
14634
14635        let mut app = test_app();
14636        app.current_service = Service::LambdaFunctions;
14637        app.service_selected = true;
14638        app.lambda_state.current_function = Some("test-function".to_string());
14639        app.lambda_state.current_version = Some("1".to_string());
14640        app.lambda_state.detail_tab = LambdaDetailTab::Code;
14641        app.mode = Mode::Normal;
14642
14643        app.lambda_state.table.items = vec![Function {
14644            name: "test-function".to_string(),
14645            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14646            application: None,
14647            description: "Test".to_string(),
14648            package_type: "Zip".to_string(),
14649            runtime: "python3.12".to_string(),
14650            architecture: "X86_64".to_string(),
14651            code_size: 1024,
14652            code_sha256: "hash".to_string(),
14653            memory_mb: 128,
14654            timeout_seconds: 30,
14655            last_modified: "2024-01-01".to_string(),
14656            layers: vec![],
14657        }];
14658
14659        // Tab should cycle between Code, Monitor, and Configuration
14660        app.handle_action(Action::NextDetailTab);
14661        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14662
14663        app.handle_action(Action::NextDetailTab);
14664        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14665
14666        app.handle_action(Action::NextDetailTab);
14667        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14668
14669        // BackTab should cycle backward
14670        app.handle_action(Action::PrevDetailTab);
14671        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14672
14673        app.handle_action(Action::PrevDetailTab);
14674        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14675    }
14676
14677    #[test]
14678    fn test_lambda_aliases_arrow_key_expansion() {
14679        use crate::lambda::Alias;
14680
14681        let mut app = test_app();
14682        app.current_service = Service::LambdaFunctions;
14683        app.service_selected = true;
14684        app.lambda_state.current_function = Some("test-function".to_string());
14685        app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
14686        app.mode = Mode::Normal;
14687
14688        app.lambda_state.alias_table.items = vec![Alias {
14689            name: "prod".to_string(),
14690            versions: "1".to_string(),
14691            description: "Production alias".to_string(),
14692        }];
14693
14694        app.lambda_state.alias_table.selected = 0;
14695
14696        // Right arrow expands
14697        app.handle_action(Action::NextPane);
14698        assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
14699
14700        // Left arrow collapses
14701        app.handle_action(Action::PrevPane);
14702        assert_eq!(app.lambda_state.alias_table.expanded_item, None);
14703    }
14704
14705    #[test]
14706    fn test_lambda_functions_arrow_key_expansion() {
14707        use crate::lambda::Function;
14708
14709        let mut app = test_app();
14710        app.current_service = Service::LambdaFunctions;
14711        app.service_selected = true;
14712        app.mode = Mode::Normal;
14713
14714        app.lambda_state.table.items = vec![Function {
14715            name: "test-function".to_string(),
14716            arn: "arn".to_string(),
14717            application: None,
14718            description: "Test".to_string(),
14719            package_type: "Zip".to_string(),
14720            runtime: "python3.12".to_string(),
14721            architecture: "X86_64".to_string(),
14722            code_size: 1024,
14723            code_sha256: "hash".to_string(),
14724            memory_mb: 128,
14725            timeout_seconds: 30,
14726            last_modified: "2024-01-01".to_string(),
14727            layers: vec![],
14728        }];
14729
14730        app.lambda_state.table.selected = 0;
14731
14732        // Right arrow expands
14733        app.handle_action(Action::NextPane);
14734        assert_eq!(app.lambda_state.table.expanded_item, Some(0));
14735
14736        // Left arrow collapses
14737        app.handle_action(Action::PrevPane);
14738        assert_eq!(app.lambda_state.table.expanded_item, None);
14739    }
14740
14741    #[test]
14742    fn test_lambda_version_detail_with_application() {
14743        use crate::lambda::Function;
14744
14745        let mut app = test_app();
14746        app.current_service = Service::LambdaFunctions;
14747        app.service_selected = true;
14748        app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
14749        app.lambda_state.current_version = Some("1".to_string());
14750        app.lambda_state.detail_tab = LambdaDetailTab::Code;
14751        app.mode = Mode::Normal;
14752
14753        app.lambda_state.table.items = vec![Function {
14754            name: "storefront-studio-beta-api".to_string(),
14755            arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
14756                .to_string(),
14757            application: Some("storefront-studio-beta".to_string()),
14758            description: "API function".to_string(),
14759            package_type: "Zip".to_string(),
14760            runtime: "python3.12".to_string(),
14761            architecture: "X86_64".to_string(),
14762            code_size: 1024,
14763            code_sha256: "hash".to_string(),
14764            memory_mb: 128,
14765            timeout_seconds: 30,
14766            last_modified: "2024-01-01".to_string(),
14767            layers: vec![],
14768        }];
14769
14770        // Verify function has application extracted
14771        assert_eq!(
14772            app.lambda_state.table.items[0].application,
14773            Some("storefront-studio-beta".to_string())
14774        );
14775        assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
14776    }
14777
14778    #[test]
14779    fn test_lambda_layer_navigation() {
14780        use crate::lambda::{Function, Layer};
14781
14782        let mut app = test_app();
14783        app.current_service = Service::LambdaFunctions;
14784        app.service_selected = true;
14785        app.lambda_state.current_function = Some("test-function".to_string());
14786        app.lambda_state.detail_tab = LambdaDetailTab::Code;
14787        app.mode = Mode::Normal;
14788
14789        app.lambda_state.table.items = vec![Function {
14790            name: "test-function".to_string(),
14791            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14792            application: None,
14793            description: "Test".to_string(),
14794            package_type: "Zip".to_string(),
14795            runtime: "python3.12".to_string(),
14796            architecture: "X86_64".to_string(),
14797            code_size: 1024,
14798            code_sha256: "hash".to_string(),
14799            memory_mb: 128,
14800            timeout_seconds: 30,
14801            last_modified: "2024-01-01".to_string(),
14802            layers: vec![
14803                Layer {
14804                    merge_order: "1".to_string(),
14805                    name: "layer1".to_string(),
14806                    layer_version: "1".to_string(),
14807                    compatible_runtimes: "python3.9".to_string(),
14808                    compatible_architectures: "x86_64".to_string(),
14809                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
14810                },
14811                Layer {
14812                    merge_order: "2".to_string(),
14813                    name: "layer2".to_string(),
14814                    layer_version: "2".to_string(),
14815                    compatible_runtimes: "python3.9".to_string(),
14816                    compatible_architectures: "x86_64".to_string(),
14817                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
14818                },
14819                Layer {
14820                    merge_order: "3".to_string(),
14821                    name: "layer3".to_string(),
14822                    layer_version: "3".to_string(),
14823                    compatible_runtimes: "python3.9".to_string(),
14824                    compatible_architectures: "x86_64".to_string(),
14825                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
14826                },
14827            ],
14828        }];
14829
14830        assert_eq!(app.lambda_state.layer_selected, 0);
14831
14832        app.handle_action(Action::NextItem);
14833        assert_eq!(app.lambda_state.layer_selected, 1);
14834
14835        app.handle_action(Action::NextItem);
14836        assert_eq!(app.lambda_state.layer_selected, 2);
14837
14838        app.handle_action(Action::NextItem);
14839        assert_eq!(app.lambda_state.layer_selected, 2);
14840
14841        app.handle_action(Action::PrevItem);
14842        assert_eq!(app.lambda_state.layer_selected, 1);
14843
14844        app.handle_action(Action::PrevItem);
14845        assert_eq!(app.lambda_state.layer_selected, 0);
14846
14847        app.handle_action(Action::PrevItem);
14848        assert_eq!(app.lambda_state.layer_selected, 0);
14849    }
14850
14851    #[test]
14852    fn test_lambda_layer_expansion() {
14853        use crate::lambda::{Function, Layer};
14854
14855        let mut app = test_app();
14856        app.current_service = Service::LambdaFunctions;
14857        app.service_selected = true;
14858        app.lambda_state.current_function = Some("test-function".to_string());
14859        app.lambda_state.detail_tab = LambdaDetailTab::Code;
14860        app.mode = Mode::Normal;
14861
14862        app.lambda_state.table.items = vec![Function {
14863            name: "test-function".to_string(),
14864            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14865            application: None,
14866            description: "Test".to_string(),
14867            package_type: "Zip".to_string(),
14868            runtime: "python3.12".to_string(),
14869            architecture: "X86_64".to_string(),
14870            code_size: 1024,
14871            code_sha256: "hash".to_string(),
14872            memory_mb: 128,
14873            timeout_seconds: 30,
14874            last_modified: "2024-01-01".to_string(),
14875            layers: vec![Layer {
14876                merge_order: "1".to_string(),
14877                name: "test-layer".to_string(),
14878                layer_version: "1".to_string(),
14879                compatible_runtimes: "python3.9".to_string(),
14880                compatible_architectures: "x86_64".to_string(),
14881                version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
14882            }],
14883        }];
14884
14885        assert_eq!(app.lambda_state.layer_expanded, None);
14886
14887        app.handle_action(Action::NextPane);
14888        assert_eq!(app.lambda_state.layer_expanded, Some(0));
14889
14890        app.handle_action(Action::PrevPane);
14891        assert_eq!(app.lambda_state.layer_expanded, None);
14892
14893        app.handle_action(Action::NextPane);
14894        assert_eq!(app.lambda_state.layer_expanded, Some(0));
14895
14896        app.handle_action(Action::NextPane);
14897        assert_eq!(app.lambda_state.layer_expanded, None);
14898    }
14899
14900    #[test]
14901    fn test_lambda_layer_selection_and_expansion_workflow() {
14902        use crate::lambda::{Function, Layer};
14903
14904        let mut app = test_app();
14905        app.current_service = Service::LambdaFunctions;
14906        app.service_selected = true;
14907        app.lambda_state.current_function = Some("test-function".to_string());
14908        app.lambda_state.detail_tab = LambdaDetailTab::Code;
14909        app.mode = Mode::Normal;
14910
14911        app.lambda_state.table.items = vec![Function {
14912            name: "test-function".to_string(),
14913            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
14914            application: None,
14915            description: "Test".to_string(),
14916            package_type: "Zip".to_string(),
14917            runtime: "python3.12".to_string(),
14918            architecture: "X86_64".to_string(),
14919            code_size: 1024,
14920            code_sha256: "hash".to_string(),
14921            memory_mb: 128,
14922            timeout_seconds: 30,
14923            last_modified: "2024-01-01".to_string(),
14924            layers: vec![
14925                Layer {
14926                    merge_order: "1".to_string(),
14927                    name: "layer1".to_string(),
14928                    layer_version: "1".to_string(),
14929                    compatible_runtimes: "python3.9".to_string(),
14930                    compatible_architectures: "x86_64".to_string(),
14931                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
14932                },
14933                Layer {
14934                    merge_order: "2".to_string(),
14935                    name: "layer2".to_string(),
14936                    layer_version: "2".to_string(),
14937                    compatible_runtimes: "python3.9".to_string(),
14938                    compatible_architectures: "x86_64".to_string(),
14939                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
14940                },
14941            ],
14942        }];
14943
14944        // Start at layer 0
14945        assert_eq!(app.lambda_state.layer_selected, 0);
14946        assert_eq!(app.lambda_state.layer_expanded, None);
14947
14948        // Expand layer 0
14949        app.handle_action(Action::NextPane);
14950        assert_eq!(app.lambda_state.layer_selected, 0);
14951        assert_eq!(app.lambda_state.layer_expanded, Some(0));
14952
14953        // Navigate to layer 1 while layer 0 is expanded
14954        app.handle_action(Action::NextItem);
14955        assert_eq!(app.lambda_state.layer_selected, 1);
14956        assert_eq!(app.lambda_state.layer_expanded, Some(0)); // Still expanded
14957
14958        // Expand layer 1 (should collapse layer 0 and expand layer 1)
14959        app.handle_action(Action::NextPane);
14960        assert_eq!(app.lambda_state.layer_selected, 1);
14961        assert_eq!(app.lambda_state.layer_expanded, Some(1));
14962
14963        // Collapse layer 1
14964        app.handle_action(Action::PrevPane);
14965        assert_eq!(app.lambda_state.layer_selected, 1);
14966        assert_eq!(app.lambda_state.layer_expanded, None);
14967
14968        // Navigate back to layer 0
14969        app.handle_action(Action::PrevItem);
14970        assert_eq!(app.lambda_state.layer_selected, 0);
14971        assert_eq!(app.lambda_state.layer_expanded, None);
14972    }
14973
14974    #[test]
14975    fn test_backtab_cycles_detail_tabs_backward() {
14976        let mut app = test_app();
14977        app.mode = Mode::Normal;
14978
14979        // Test Lambda detail tabs
14980        app.current_service = Service::LambdaFunctions;
14981        app.service_selected = true;
14982        app.lambda_state.current_function = Some("test-function".to_string());
14983        app.lambda_state.detail_tab = LambdaDetailTab::Code;
14984
14985        app.handle_action(Action::PrevDetailTab);
14986        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
14987
14988        app.handle_action(Action::PrevDetailTab);
14989        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
14990
14991        // Test IAM Roles detail tabs
14992        app.current_service = Service::IamRoles;
14993        app.iam_state.current_role = Some("test-role".to_string());
14994        app.iam_state.role_tab = RoleTab::Permissions;
14995
14996        app.handle_action(Action::PrevDetailTab);
14997        assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
14998
14999        // Test IAM Users detail tabs
15000        app.current_service = Service::IamUsers;
15001        app.iam_state.current_user = Some("test-user".to_string());
15002        app.iam_state.user_tab = UserTab::Permissions;
15003
15004        app.handle_action(Action::PrevDetailTab);
15005        assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
15006
15007        // Test IAM Groups detail tabs
15008        app.current_service = Service::IamUserGroups;
15009        app.iam_state.current_group = Some("test-group".to_string());
15010        app.iam_state.group_tab = GroupTab::Permissions;
15011
15012        app.handle_action(Action::PrevDetailTab);
15013        assert_eq!(app.iam_state.group_tab, GroupTab::Users);
15014
15015        // Test S3 object tabs
15016        app.current_service = Service::S3Buckets;
15017        app.s3_state.current_bucket = Some("test-bucket".to_string());
15018        app.s3_state.object_tab = S3ObjectTab::Properties;
15019
15020        app.handle_action(Action::PrevDetailTab);
15021        assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
15022
15023        // Test ECR repository tabs (Private/Public)
15024        app.current_service = Service::EcrRepositories;
15025        app.ecr_state.current_repository = None;
15026        app.ecr_state.tab = EcrTab::Private;
15027
15028        app.handle_action(Action::PrevDetailTab);
15029        assert_eq!(app.ecr_state.tab, EcrTab::Public);
15030
15031        // Test CloudFormation detail tabs
15032        app.current_service = Service::CloudFormationStacks;
15033        app.cfn_state.current_stack = Some("test-stack".to_string());
15034        app.cfn_state.detail_tab = CfnDetailTab::Resources;
15035    }
15036
15037    #[test]
15038    fn test_cloudformation_status_filter_active() {
15039        let filter = CfnStatusFilter::Active;
15040        assert!(filter.matches("CREATE_IN_PROGRESS"));
15041        assert!(filter.matches("UPDATE_IN_PROGRESS"));
15042        assert!(!filter.matches("CREATE_COMPLETE"));
15043        assert!(!filter.matches("DELETE_COMPLETE"));
15044        assert!(!filter.matches("CREATE_FAILED"));
15045    }
15046
15047    #[test]
15048    fn test_cloudformation_status_filter_complete() {
15049        let filter = CfnStatusFilter::Complete;
15050        assert!(filter.matches("CREATE_COMPLETE"));
15051        assert!(filter.matches("UPDATE_COMPLETE"));
15052        assert!(!filter.matches("DELETE_COMPLETE"));
15053        assert!(!filter.matches("CREATE_IN_PROGRESS"));
15054    }
15055
15056    #[test]
15057    fn test_cloudformation_status_filter_failed() {
15058        let filter = CfnStatusFilter::Failed;
15059        assert!(filter.matches("CREATE_FAILED"));
15060        assert!(filter.matches("UPDATE_FAILED"));
15061        assert!(!filter.matches("CREATE_COMPLETE"));
15062    }
15063
15064    #[test]
15065    fn test_cloudformation_status_filter_deleted() {
15066        let filter = CfnStatusFilter::Deleted;
15067        assert!(filter.matches("DELETE_COMPLETE"));
15068        assert!(filter.matches("DELETE_IN_PROGRESS"));
15069        assert!(!filter.matches("CREATE_COMPLETE"));
15070    }
15071
15072    #[test]
15073    fn test_cloudformation_status_filter_in_progress() {
15074        let filter = CfnStatusFilter::InProgress;
15075        assert!(filter.matches("CREATE_IN_PROGRESS"));
15076        assert!(filter.matches("UPDATE_IN_PROGRESS"));
15077        assert!(filter.matches("DELETE_IN_PROGRESS"));
15078        assert!(!filter.matches("CREATE_COMPLETE"));
15079    }
15080
15081    #[test]
15082    fn test_cloudformation_status_filter_cycle() {
15083        let filter = CfnStatusFilter::All;
15084        assert_eq!(filter.next(), CfnStatusFilter::Active);
15085        assert_eq!(filter.next().next(), CfnStatusFilter::Complete);
15086        assert_eq!(filter.next().next().next(), CfnStatusFilter::Failed);
15087        assert_eq!(filter.next().next().next().next(), CfnStatusFilter::Deleted);
15088        assert_eq!(
15089            filter.next().next().next().next().next(),
15090            CfnStatusFilter::InProgress
15091        );
15092        assert_eq!(
15093            filter.next().next().next().next().next().next(),
15094            CfnStatusFilter::All
15095        );
15096    }
15097
15098    #[test]
15099    fn test_cloudformation_default_columns() {
15100        let app = test_app();
15101        assert_eq!(app.cfn_visible_column_ids.len(), 4);
15102        assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
15103        assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
15104        assert!(app
15105            .cfn_visible_column_ids
15106            .contains(&CfnColumn::CreatedTime.id()));
15107        assert!(app
15108            .cfn_visible_column_ids
15109            .contains(&CfnColumn::Description.id()));
15110    }
15111
15112    #[test]
15113    fn test_cloudformation_all_columns() {
15114        let app = test_app();
15115        assert_eq!(app.cfn_column_ids.len(), 10);
15116    }
15117
15118    #[test]
15119    fn test_cloudformation_filter_by_name() {
15120        let mut app = test_app();
15121        app.cfn_state.status_filter = CfnStatusFilter::Complete;
15122        app.cfn_state.table.items = vec![
15123            CfnStack {
15124                name: "my-stack".to_string(),
15125                stack_id: "id1".to_string(),
15126                status: "CREATE_COMPLETE".to_string(),
15127                created_time: "2024-01-01".to_string(),
15128                updated_time: String::new(),
15129                deleted_time: String::new(),
15130                drift_status: String::new(),
15131                last_drift_check_time: String::new(),
15132                status_reason: String::new(),
15133                description: String::new(),
15134                detailed_status: String::new(),
15135                root_stack: String::new(),
15136                parent_stack: String::new(),
15137                termination_protection: false,
15138                iam_role: String::new(),
15139                tags: Vec::new(),
15140                stack_policy: String::new(),
15141                rollback_monitoring_time: String::new(),
15142                rollback_alarms: Vec::new(),
15143                notification_arns: Vec::new(),
15144            },
15145            CfnStack {
15146                name: "other-stack".to_string(),
15147                stack_id: "id2".to_string(),
15148                status: "CREATE_COMPLETE".to_string(),
15149                created_time: "2024-01-02".to_string(),
15150                updated_time: String::new(),
15151                deleted_time: String::new(),
15152                drift_status: String::new(),
15153                last_drift_check_time: String::new(),
15154                status_reason: String::new(),
15155                description: String::new(),
15156                detailed_status: String::new(),
15157                root_stack: String::new(),
15158                parent_stack: String::new(),
15159                termination_protection: false,
15160                iam_role: String::new(),
15161                tags: Vec::new(),
15162                stack_policy: String::new(),
15163                rollback_monitoring_time: String::new(),
15164                rollback_alarms: Vec::new(),
15165                notification_arns: Vec::new(),
15166            },
15167        ];
15168
15169        app.cfn_state.table.filter = "my".to_string();
15170        let filtered = filtered_cloudformation_stacks(&app);
15171        assert_eq!(filtered.len(), 1);
15172        assert_eq!(filtered[0].name, "my-stack");
15173    }
15174
15175    #[test]
15176    fn test_cloudformation_filter_by_description() {
15177        let mut app = test_app();
15178        app.cfn_state.status_filter = CfnStatusFilter::Complete;
15179        app.cfn_state.table.items = vec![CfnStack {
15180            name: "stack1".to_string(),
15181            stack_id: "id1".to_string(),
15182            status: "CREATE_COMPLETE".to_string(),
15183            created_time: "2024-01-01".to_string(),
15184            updated_time: String::new(),
15185            deleted_time: String::new(),
15186            drift_status: String::new(),
15187            last_drift_check_time: String::new(),
15188            status_reason: String::new(),
15189            description: "production stack".to_string(),
15190            detailed_status: String::new(),
15191            root_stack: String::new(),
15192            parent_stack: String::new(),
15193            termination_protection: false,
15194            iam_role: String::new(),
15195            tags: Vec::new(),
15196            stack_policy: String::new(),
15197            rollback_monitoring_time: String::new(),
15198            rollback_alarms: Vec::new(),
15199            notification_arns: Vec::new(),
15200        }];
15201
15202        app.cfn_state.table.filter = "production".to_string();
15203        let filtered = filtered_cloudformation_stacks(&app);
15204        assert_eq!(filtered.len(), 1);
15205    }
15206
15207    #[test]
15208    fn test_cloudformation_status_filter_applied() {
15209        let mut app = test_app();
15210        app.cfn_state.table.items = vec![
15211            CfnStack {
15212                name: "complete-stack".to_string(),
15213                stack_id: "id1".to_string(),
15214                status: "CREATE_COMPLETE".to_string(),
15215                created_time: "2024-01-01".to_string(),
15216                updated_time: String::new(),
15217                deleted_time: String::new(),
15218                drift_status: String::new(),
15219                last_drift_check_time: String::new(),
15220                status_reason: String::new(),
15221                description: String::new(),
15222                detailed_status: String::new(),
15223                root_stack: String::new(),
15224                parent_stack: String::new(),
15225                termination_protection: false,
15226                iam_role: String::new(),
15227                tags: Vec::new(),
15228                stack_policy: String::new(),
15229                rollback_monitoring_time: String::new(),
15230                rollback_alarms: Vec::new(),
15231                notification_arns: Vec::new(),
15232            },
15233            CfnStack {
15234                name: "failed-stack".to_string(),
15235                stack_id: "id2".to_string(),
15236                status: "CREATE_FAILED".to_string(),
15237                created_time: "2024-01-02".to_string(),
15238                updated_time: String::new(),
15239                deleted_time: String::new(),
15240                drift_status: String::new(),
15241                last_drift_check_time: String::new(),
15242                status_reason: String::new(),
15243                description: String::new(),
15244                detailed_status: String::new(),
15245                root_stack: String::new(),
15246                parent_stack: String::new(),
15247                termination_protection: false,
15248                iam_role: String::new(),
15249                tags: Vec::new(),
15250                stack_policy: String::new(),
15251                rollback_monitoring_time: String::new(),
15252                rollback_alarms: Vec::new(),
15253                notification_arns: Vec::new(),
15254            },
15255        ];
15256
15257        app.cfn_state.status_filter = CfnStatusFilter::Complete;
15258        let filtered = filtered_cloudformation_stacks(&app);
15259        assert_eq!(filtered.len(), 1);
15260        assert_eq!(filtered[0].name, "complete-stack");
15261
15262        app.cfn_state.status_filter = CfnStatusFilter::Failed;
15263        let filtered = filtered_cloudformation_stacks(&app);
15264        assert_eq!(filtered.len(), 1);
15265        assert_eq!(filtered[0].name, "failed-stack");
15266    }
15267
15268    #[test]
15269    fn test_cloudformation_default_page_size() {
15270        let app = test_app();
15271        assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
15272    }
15273
15274    #[test]
15275    fn test_cloudformation_default_status_filter() {
15276        let app = test_app();
15277        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
15278    }
15279
15280    #[test]
15281    fn test_cloudformation_view_nested_default_false() {
15282        let app = test_app();
15283        assert!(!app.cfn_state.view_nested);
15284    }
15285
15286    #[test]
15287    fn test_cloudformation_pagination_hotkeys() {
15288        let mut app = test_app();
15289        app.current_service = Service::CloudFormationStacks;
15290        app.service_selected = true;
15291        app.cfn_state.status_filter = CfnStatusFilter::All;
15292
15293        // Add 150 stacks
15294        for i in 0..150 {
15295            app.cfn_state.table.items.push(CfnStack {
15296                name: format!("stack-{}", i),
15297                stack_id: format!("id-{}", i),
15298                status: "CREATE_COMPLETE".to_string(),
15299                created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
15300                updated_time: String::new(),
15301                deleted_time: String::new(),
15302                drift_status: String::new(),
15303                last_drift_check_time: String::new(),
15304                status_reason: String::new(),
15305                description: String::new(),
15306                detailed_status: String::new(),
15307                root_stack: String::new(),
15308                parent_stack: String::new(),
15309                termination_protection: false,
15310                iam_role: String::new(),
15311                tags: vec![],
15312                stack_policy: String::new(),
15313                rollback_monitoring_time: String::new(),
15314                rollback_alarms: vec![],
15315                notification_arns: vec![],
15316            });
15317        }
15318
15319        // Go to page 2
15320        app.go_to_page(2);
15321        assert_eq!(app.cfn_state.table.selected, 50);
15322
15323        // Go to page 3
15324        app.go_to_page(3);
15325        assert_eq!(app.cfn_state.table.selected, 100);
15326
15327        // Go to page 1
15328        app.go_to_page(1);
15329        assert_eq!(app.cfn_state.table.selected, 0);
15330    }
15331
15332    #[test]
15333    fn test_cloudformation_tab_cycling_in_filter_mode() {
15334        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
15335        let mut app = test_app();
15336        app.current_service = Service::CloudFormationStacks;
15337        app.service_selected = true;
15338        app.mode = Mode::FilterInput;
15339        app.cfn_state.input_focus = InputFocus::Filter;
15340
15341        // Tab to StatusFilter
15342        app.handle_action(Action::NextFilterFocus);
15343        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
15344
15345        // Tab to ViewNested
15346        app.handle_action(Action::NextFilterFocus);
15347        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
15348
15349        // Tab to Pagination
15350        app.handle_action(Action::NextFilterFocus);
15351        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15352
15353        // Tab back to Filter
15354        app.handle_action(Action::NextFilterFocus);
15355        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15356    }
15357
15358    #[test]
15359    fn test_cloudformation_timestamp_format_includes_utc() {
15360        let stack = CfnStack {
15361            name: "test-stack".to_string(),
15362            stack_id: "id-123".to_string(),
15363            status: "CREATE_COMPLETE".to_string(),
15364            created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
15365            updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
15366            deleted_time: String::new(),
15367            drift_status: String::new(),
15368            last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
15369            status_reason: String::new(),
15370            description: String::new(),
15371            detailed_status: String::new(),
15372            root_stack: String::new(),
15373            parent_stack: String::new(),
15374            termination_protection: false,
15375            iam_role: String::new(),
15376            tags: vec![],
15377            stack_policy: String::new(),
15378            rollback_monitoring_time: String::new(),
15379            rollback_alarms: vec![],
15380            notification_arns: vec![],
15381        };
15382
15383        assert!(stack.created_time.contains("(UTC)"));
15384        assert!(stack.updated_time.contains("(UTC)"));
15385        assert!(stack.last_drift_check_time.contains("(UTC)"));
15386        assert_eq!(stack.created_time.len(), 25);
15387    }
15388
15389    #[test]
15390    fn test_cloudformation_enter_drills_into_stack_view() {
15391        let mut app = test_app();
15392        app.current_service = Service::CloudFormationStacks;
15393        app.service_selected = true;
15394        app.mode = Mode::Normal;
15395        app.cfn_state.status_filter = CfnStatusFilter::All;
15396        app.tabs = vec![Tab {
15397            service: Service::CloudFormationStacks,
15398            title: "CloudFormation › Stacks".to_string(),
15399            breadcrumb: "CloudFormation › Stacks".to_string(),
15400        }];
15401        app.current_tab = 0;
15402
15403        app.cfn_state.table.items.push(CfnStack {
15404            name: "test-stack".to_string(),
15405            stack_id: "id-123".to_string(),
15406            status: "CREATE_COMPLETE".to_string(),
15407            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
15408            updated_time: String::new(),
15409            deleted_time: String::new(),
15410            drift_status: String::new(),
15411            last_drift_check_time: String::new(),
15412            status_reason: String::new(),
15413            description: String::new(),
15414            detailed_status: String::new(),
15415            root_stack: String::new(),
15416            parent_stack: String::new(),
15417            termination_protection: false,
15418            iam_role: String::new(),
15419            tags: vec![],
15420            stack_policy: String::new(),
15421            rollback_monitoring_time: String::new(),
15422            rollback_alarms: vec![],
15423            notification_arns: vec![],
15424        });
15425
15426        app.cfn_state.table.reset();
15427        assert_eq!(app.cfn_state.current_stack, None);
15428
15429        // Press Enter - should drill into stack detail view
15430        app.handle_action(Action::Select);
15431        assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
15432    }
15433
15434    #[test]
15435    fn test_cloudformation_arrow_keys_expand_collapse() {
15436        let mut app = test_app();
15437        app.current_service = Service::CloudFormationStacks;
15438        app.service_selected = true;
15439        app.mode = Mode::Normal;
15440        app.cfn_state.status_filter = CfnStatusFilter::All;
15441
15442        app.cfn_state.table.items.push(CfnStack {
15443            name: "test-stack".to_string(),
15444            stack_id: "id-123".to_string(),
15445            status: "CREATE_COMPLETE".to_string(),
15446            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
15447            updated_time: String::new(),
15448            deleted_time: String::new(),
15449            drift_status: String::new(),
15450            last_drift_check_time: String::new(),
15451            status_reason: String::new(),
15452            description: String::new(),
15453            detailed_status: String::new(),
15454            root_stack: String::new(),
15455            parent_stack: String::new(),
15456            termination_protection: false,
15457            iam_role: String::new(),
15458            tags: vec![],
15459            stack_policy: String::new(),
15460            rollback_monitoring_time: String::new(),
15461            rollback_alarms: vec![],
15462            notification_arns: vec![],
15463        });
15464
15465        app.cfn_state.table.reset();
15466        assert_eq!(app.cfn_state.table.expanded_item, None);
15467
15468        // Right arrow - should expand
15469        app.handle_action(Action::NextPane);
15470        assert_eq!(app.cfn_state.table.expanded_item, Some(0));
15471
15472        // Left arrow - should collapse
15473        app.handle_action(Action::PrevPane);
15474        assert_eq!(app.cfn_state.table.expanded_item, None);
15475
15476        // Verify current_stack is still None (not drilled in)
15477        assert_eq!(app.cfn_state.current_stack, None);
15478    }
15479
15480    #[test]
15481    fn test_cloudformation_tab_cycling() {
15482        use crate::ui::cfn::DetailTab;
15483        let mut app = test_app();
15484        app.current_service = Service::CloudFormationStacks;
15485        app.service_selected = true;
15486        app.mode = Mode::Normal;
15487        app.cfn_state.status_filter = CfnStatusFilter::All;
15488        app.cfn_state.current_stack = Some("test-stack".to_string());
15489
15490        assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
15491    }
15492
15493    #[test]
15494    fn test_cloudformation_console_url() {
15495        use crate::ui::cfn::DetailTab;
15496        let mut app = test_app();
15497        app.current_service = Service::CloudFormationStacks;
15498        app.service_selected = true;
15499        app.cfn_state.status_filter = CfnStatusFilter::All;
15500
15501        app.cfn_state.table.items.push(CfnStack {
15502            name: "test-stack".to_string(),
15503            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
15504                .to_string(),
15505            status: "CREATE_COMPLETE".to_string(),
15506            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
15507            updated_time: String::new(),
15508            deleted_time: String::new(),
15509            drift_status: String::new(),
15510            last_drift_check_time: String::new(),
15511            status_reason: String::new(),
15512            description: String::new(),
15513            detailed_status: String::new(),
15514            root_stack: String::new(),
15515            parent_stack: String::new(),
15516            termination_protection: false,
15517            iam_role: String::new(),
15518            tags: vec![],
15519            stack_policy: String::new(),
15520            rollback_monitoring_time: String::new(),
15521            rollback_alarms: vec![],
15522            notification_arns: vec![],
15523        });
15524
15525        app.cfn_state.current_stack = Some("test-stack".to_string());
15526
15527        // Stack info URL
15528        app.cfn_state.detail_tab = DetailTab::StackInfo;
15529        let url = app.get_console_url();
15530        assert!(url.contains("stackinfo"));
15531        assert!(url.contains("arn%3Aaws%3Acloudformation"));
15532
15533        // Events URL
15534        app.cfn_state.detail_tab = DetailTab::Events;
15535        let url = app.get_console_url();
15536        assert!(url.contains("events"));
15537        assert!(url.contains("arn%3Aaws%3Acloudformation"));
15538    }
15539
15540    #[test]
15541    fn test_iam_role_select() {
15542        let mut app = test_app();
15543        app.current_service = Service::IamRoles;
15544        app.service_selected = true;
15545        app.mode = Mode::Normal;
15546
15547        app.iam_state.roles.items = vec![
15548            IamRole {
15549                role_name: "role1".to_string(),
15550                path: "/".to_string(),
15551                trusted_entities: "AWS Service: ec2".to_string(),
15552                last_activity: "-".to_string(),
15553                arn: "arn:aws:iam::123456789012:role/role1".to_string(),
15554                creation_time: "2025-01-01".to_string(),
15555                description: "Test role 1".to_string(),
15556                max_session_duration: Some(3600),
15557            },
15558            IamRole {
15559                role_name: "role2".to_string(),
15560                path: "/".to_string(),
15561                trusted_entities: "AWS Service: lambda".to_string(),
15562                last_activity: "-".to_string(),
15563                arn: "arn:aws:iam::123456789012:role/role2".to_string(),
15564                creation_time: "2025-01-02".to_string(),
15565                description: "Test role 2".to_string(),
15566                max_session_duration: Some(7200),
15567            },
15568        ];
15569
15570        // Select first role
15571        app.iam_state.roles.selected = 0;
15572        app.handle_action(Action::Select);
15573
15574        assert_eq!(
15575            app.iam_state.current_role,
15576            Some("role1".to_string()),
15577            "Should open role detail view"
15578        );
15579        assert_eq!(
15580            app.iam_state.role_tab,
15581            RoleTab::Permissions,
15582            "Should default to Permissions tab"
15583        );
15584    }
15585
15586    #[test]
15587    fn test_iam_role_back_navigation() {
15588        let mut app = test_app();
15589        app.current_service = Service::IamRoles;
15590        app.service_selected = true;
15591        app.iam_state.current_role = Some("test-role".to_string());
15592
15593        app.handle_action(Action::GoBack);
15594
15595        assert_eq!(
15596            app.iam_state.current_role, None,
15597            "Should return to roles list"
15598        );
15599    }
15600
15601    #[test]
15602    fn test_iam_role_tab_navigation() {
15603        let mut app = test_app();
15604        app.current_service = Service::IamRoles;
15605        app.service_selected = true;
15606        app.iam_state.current_role = Some("test-role".to_string());
15607        app.iam_state.role_tab = RoleTab::Permissions;
15608
15609        app.handle_action(Action::NextDetailTab);
15610
15611        assert_eq!(
15612            app.iam_state.role_tab,
15613            RoleTab::TrustRelationships,
15614            "Should move to next tab"
15615        );
15616    }
15617
15618    #[test]
15619    fn test_iam_role_tab_cycle_order() {
15620        let mut app = test_app();
15621        app.current_service = Service::IamRoles;
15622        app.service_selected = true;
15623        app.iam_state.current_role = Some("test-role".to_string());
15624        app.iam_state.role_tab = RoleTab::Permissions;
15625
15626        app.handle_action(Action::NextDetailTab);
15627        assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
15628
15629        app.handle_action(Action::NextDetailTab);
15630        assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
15631
15632        app.handle_action(Action::NextDetailTab);
15633        assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
15634
15635        app.handle_action(Action::NextDetailTab);
15636        assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
15637
15638        app.handle_action(Action::NextDetailTab);
15639        assert_eq!(
15640            app.iam_state.role_tab,
15641            RoleTab::Permissions,
15642            "Should cycle back to first tab"
15643        );
15644    }
15645
15646    #[test]
15647    fn test_iam_role_pagination() {
15648        let mut app = test_app();
15649        app.current_service = Service::IamRoles;
15650        app.service_selected = true;
15651        app.iam_state.roles.page_size = PageSize::Ten;
15652
15653        app.iam_state.roles.items = (0..25)
15654            .map(|i| IamRole {
15655                role_name: format!("role{}", i),
15656                path: "/".to_string(),
15657                trusted_entities: "AWS Service: ec2".to_string(),
15658                last_activity: "-".to_string(),
15659                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
15660                creation_time: "2025-01-01".to_string(),
15661                description: format!("Test role {}", i),
15662                max_session_duration: Some(3600),
15663            })
15664            .collect();
15665
15666        // Jump to page 2
15667        app.go_to_page(2);
15668
15669        assert_eq!(
15670            app.iam_state.roles.selected, 10,
15671            "Should select first item of page 2"
15672        );
15673        assert_eq!(
15674            app.iam_state.roles.scroll_offset, 10,
15675            "Should update scroll offset"
15676        );
15677    }
15678
15679    #[test]
15680    fn test_tags_table_populated_on_role_detail() {
15681        let mut app = test_app();
15682        app.current_service = Service::IamRoles;
15683        app.service_selected = true;
15684        app.mode = Mode::Normal;
15685        app.iam_state.roles.items = vec![IamRole {
15686            role_name: "TestRole".to_string(),
15687            path: "/".to_string(),
15688            trusted_entities: String::new(),
15689            last_activity: String::new(),
15690            arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
15691            creation_time: "2025-01-01".to_string(),
15692            description: String::new(),
15693            max_session_duration: Some(3600),
15694        }];
15695
15696        // Manually populate tags to test table rendering
15697        app.iam_state.tags.items = vec![
15698            IamRoleTag {
15699                key: "Environment".to_string(),
15700                value: "Production".to_string(),
15701            },
15702            IamRoleTag {
15703                key: "Team".to_string(),
15704                value: "Platform".to_string(),
15705            },
15706        ];
15707
15708        assert_eq!(app.iam_state.tags.items.len(), 2);
15709        assert_eq!(app.iam_state.tags.items[0].key, "Environment");
15710        assert_eq!(app.iam_state.tags.items[0].value, "Production");
15711        assert_eq!(app.iam_state.tags.selected, 0);
15712    }
15713
15714    #[test]
15715    fn test_tags_table_navigation() {
15716        let mut app = test_app();
15717        app.current_service = Service::IamRoles;
15718        app.service_selected = true;
15719        app.mode = Mode::Normal;
15720        app.iam_state.current_role = Some("TestRole".to_string());
15721        app.iam_state.role_tab = RoleTab::Tags;
15722        app.iam_state.tags.items = vec![
15723            IamRoleTag {
15724                key: "Tag1".to_string(),
15725                value: "Value1".to_string(),
15726            },
15727            IamRoleTag {
15728                key: "Tag2".to_string(),
15729                value: "Value2".to_string(),
15730            },
15731        ];
15732
15733        app.handle_action(Action::NextItem);
15734        assert_eq!(app.iam_state.tags.selected, 1);
15735
15736        app.handle_action(Action::PrevItem);
15737        assert_eq!(app.iam_state.tags.selected, 0);
15738    }
15739
15740    #[test]
15741    fn test_last_accessed_table_navigation() {
15742        let mut app = test_app();
15743        app.current_service = Service::IamRoles;
15744        app.service_selected = true;
15745        app.mode = Mode::Normal;
15746        app.iam_state.current_role = Some("TestRole".to_string());
15747        app.iam_state.role_tab = RoleTab::LastAccessed;
15748        app.iam_state.last_accessed_services.items = vec![
15749            LastAccessedService {
15750                service: "S3".to_string(),
15751                policies_granting: "Policy1".to_string(),
15752                last_accessed: "2025-01-01".to_string(),
15753            },
15754            LastAccessedService {
15755                service: "EC2".to_string(),
15756                policies_granting: "Policy2".to_string(),
15757                last_accessed: "2025-01-02".to_string(),
15758            },
15759        ];
15760
15761        app.handle_action(Action::NextItem);
15762        assert_eq!(app.iam_state.last_accessed_services.selected, 1);
15763
15764        app.handle_action(Action::PrevItem);
15765        assert_eq!(app.iam_state.last_accessed_services.selected, 0);
15766    }
15767
15768    #[test]
15769    fn test_cfn_input_focus_next() {
15770        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
15771        let mut app = test_app();
15772        app.current_service = Service::CloudFormationStacks;
15773        app.mode = Mode::FilterInput;
15774        app.cfn_state.input_focus = InputFocus::Filter;
15775
15776        app.handle_action(Action::NextFilterFocus);
15777        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
15778
15779        app.handle_action(Action::NextFilterFocus);
15780        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
15781
15782        app.handle_action(Action::NextFilterFocus);
15783        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15784
15785        app.handle_action(Action::NextFilterFocus);
15786        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15787    }
15788
15789    #[test]
15790    fn test_cfn_input_focus_prev() {
15791        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
15792        let mut app = test_app();
15793        app.current_service = Service::CloudFormationStacks;
15794        app.mode = Mode::FilterInput;
15795        app.cfn_state.input_focus = InputFocus::Filter;
15796
15797        app.handle_action(Action::PrevFilterFocus);
15798        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15799
15800        app.handle_action(Action::PrevFilterFocus);
15801        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
15802
15803        app.handle_action(Action::PrevFilterFocus);
15804        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
15805
15806        app.handle_action(Action::PrevFilterFocus);
15807        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15808    }
15809
15810    #[test]
15811    fn test_cw_logs_input_focus_prev() {
15812        let mut app = test_app();
15813        app.current_service = Service::CloudWatchLogGroups;
15814        app.mode = Mode::FilterInput;
15815        app.view_mode = ViewMode::Detail;
15816        app.log_groups_state.detail_tab = CwLogsDetailTab::LogStreams;
15817        app.log_groups_state.input_focus = InputFocus::Filter;
15818
15819        app.handle_action(Action::PrevFilterFocus);
15820        assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
15821
15822        app.handle_action(Action::PrevFilterFocus);
15823        assert_eq!(
15824            app.log_groups_state.input_focus,
15825            InputFocus::Checkbox("ShowExpired")
15826        );
15827
15828        app.handle_action(Action::PrevFilterFocus);
15829        assert_eq!(
15830            app.log_groups_state.input_focus,
15831            InputFocus::Checkbox("ExactMatch")
15832        );
15833
15834        app.handle_action(Action::PrevFilterFocus);
15835        assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
15836    }
15837
15838    #[test]
15839    fn test_cw_events_input_focus_prev() {
15840        use crate::ui::cw::logs::EventFilterFocus;
15841        let mut app = test_app();
15842        app.mode = Mode::EventFilterInput;
15843        app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
15844
15845        app.handle_action(Action::PrevFilterFocus);
15846        assert_eq!(
15847            app.log_groups_state.event_input_focus,
15848            EventFilterFocus::DateRange
15849        );
15850
15851        app.handle_action(Action::PrevFilterFocus);
15852        assert_eq!(
15853            app.log_groups_state.event_input_focus,
15854            EventFilterFocus::Filter
15855        );
15856    }
15857
15858    #[test]
15859    fn test_cfn_input_focus_cycle_complete() {
15860        let mut app = test_app();
15861        app.current_service = Service::CloudFormationStacks;
15862        app.mode = Mode::FilterInput;
15863        app.cfn_state.input_focus = InputFocus::Filter;
15864
15865        // Cycle forward through all controls
15866        for _ in 0..4 {
15867            app.handle_action(Action::NextFilterFocus);
15868        }
15869        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15870
15871        // Cycle backward through all controls
15872        for _ in 0..4 {
15873            app.handle_action(Action::PrevFilterFocus);
15874        }
15875        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15876    }
15877
15878    #[test]
15879    fn test_cfn_filter_status_arrow_keys() {
15880        use crate::ui::cfn::STATUS_FILTER;
15881        let mut app = test_app();
15882        app.current_service = Service::CloudFormationStacks;
15883        app.mode = Mode::FilterInput;
15884        app.cfn_state.input_focus = STATUS_FILTER;
15885        app.cfn_state.status_filter = CfnStatusFilter::All;
15886
15887        app.handle_action(Action::NextItem);
15888        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
15889
15890        app.handle_action(Action::PrevItem);
15891        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
15892    }
15893
15894    #[test]
15895    fn test_cfn_filter_shift_tab_cycles_backward() {
15896        use crate::ui::cfn::STATUS_FILTER;
15897        let mut app = test_app();
15898        app.current_service = Service::CloudFormationStacks;
15899        app.mode = Mode::FilterInput;
15900        app.cfn_state.input_focus = STATUS_FILTER;
15901
15902        // Shift+Tab should go backward
15903        app.handle_action(Action::PrevFilterFocus);
15904        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
15905
15906        // From Input, Shift+Tab should wrap to Pagination
15907        app.handle_action(Action::PrevFilterFocus);
15908        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
15909    }
15910
15911    #[test]
15912    fn test_cfn_pagination_arrow_keys() {
15913        let mut app = test_app();
15914        app.current_service = Service::CloudFormationStacks;
15915        app.mode = Mode::FilterInput;
15916        app.cfn_state.input_focus = InputFocus::Pagination;
15917        app.cfn_state.table.scroll_offset = 0;
15918        app.cfn_state.table.page_size = PageSize::Ten;
15919
15920        // Add some test stacks
15921        app.cfn_state.table.items = (0..30)
15922            .map(|i| CfnStack {
15923                name: format!("stack-{}", i),
15924                stack_id: format!("id-{}", i),
15925                status: "CREATE_COMPLETE".to_string(),
15926                created_time: "2024-01-01".to_string(),
15927                updated_time: String::new(),
15928                deleted_time: String::new(),
15929                drift_status: String::new(),
15930                last_drift_check_time: String::new(),
15931                status_reason: String::new(),
15932                description: String::new(),
15933                detailed_status: String::new(),
15934                root_stack: String::new(),
15935                parent_stack: String::new(),
15936                termination_protection: false,
15937                iam_role: String::new(),
15938                tags: Vec::new(),
15939                stack_policy: String::new(),
15940                rollback_monitoring_time: String::new(),
15941                rollback_alarms: Vec::new(),
15942                notification_arns: Vec::new(),
15943            })
15944            .collect();
15945
15946        // Right arrow should page forward
15947        app.handle_action(Action::PageDown);
15948        assert_eq!(app.cfn_state.table.scroll_offset, 10);
15949        // Verify page number calculation
15950        let page_size = app.cfn_state.table.page_size.value();
15951        let current_page = app.cfn_state.table.scroll_offset / page_size;
15952        assert_eq!(current_page, 1);
15953
15954        // Left arrow should page backward
15955        app.handle_action(Action::PageUp);
15956        assert_eq!(app.cfn_state.table.scroll_offset, 0);
15957        let current_page = app.cfn_state.table.scroll_offset / page_size;
15958        assert_eq!(current_page, 0);
15959    }
15960
15961    #[test]
15962    fn test_cfn_page_navigation_updates_selection() {
15963        let mut app = test_app();
15964        app.current_service = Service::CloudFormationStacks;
15965        app.mode = Mode::Normal;
15966
15967        // Add 30 test stacks
15968        app.cfn_state.table.items = (0..30)
15969            .map(|i| CfnStack {
15970                name: format!("stack-{}", i),
15971                stack_id: format!("id-{}", i),
15972                status: "CREATE_COMPLETE".to_string(),
15973                created_time: "2024-01-01".to_string(),
15974                updated_time: String::new(),
15975                deleted_time: String::new(),
15976                drift_status: String::new(),
15977                last_drift_check_time: String::new(),
15978                status_reason: String::new(),
15979                description: String::new(),
15980                detailed_status: String::new(),
15981                root_stack: String::new(),
15982                parent_stack: String::new(),
15983                termination_protection: false,
15984                iam_role: String::new(),
15985                tags: Vec::new(),
15986                stack_policy: String::new(),
15987                rollback_monitoring_time: String::new(),
15988                rollback_alarms: Vec::new(),
15989                notification_arns: Vec::new(),
15990            })
15991            .collect();
15992
15993        app.cfn_state.table.reset();
15994        app.cfn_state.table.scroll_offset = 0;
15995
15996        // Page down should update selection
15997        app.handle_action(Action::PageDown);
15998        assert_eq!(app.cfn_state.table.selected, 10);
15999
16000        // Page down again
16001        app.handle_action(Action::PageDown);
16002        assert_eq!(app.cfn_state.table.selected, 20);
16003
16004        // Page up should update selection
16005        app.handle_action(Action::PageUp);
16006        assert_eq!(app.cfn_state.table.selected, 10);
16007    }
16008
16009    #[test]
16010    fn test_cfn_filter_input_only_when_focused() {
16011        use crate::ui::cfn::STATUS_FILTER;
16012        let mut app = test_app();
16013        app.current_service = Service::CloudFormationStacks;
16014        app.mode = Mode::FilterInput;
16015        app.cfn_state.input_focus = STATUS_FILTER;
16016        app.cfn_state.table.filter = String::new();
16017
16018        // Typing should not add to filter when focus is not on Input
16019        app.handle_action(Action::FilterInput('t'));
16020        app.handle_action(Action::FilterInput('e'));
16021        app.handle_action(Action::FilterInput('s'));
16022        app.handle_action(Action::FilterInput('t'));
16023        assert_eq!(app.cfn_state.table.filter, "");
16024
16025        // Switch to Input focus
16026        app.cfn_state.input_focus = InputFocus::Filter;
16027        app.handle_action(Action::FilterInput('t'));
16028        app.handle_action(Action::FilterInput('e'));
16029        app.handle_action(Action::FilterInput('s'));
16030        app.handle_action(Action::FilterInput('t'));
16031        assert_eq!(app.cfn_state.table.filter, "test");
16032    }
16033
16034    #[test]
16035    fn test_cfn_input_focus_resets_on_start() {
16036        let mut app = test_app();
16037        app.current_service = Service::CloudFormationStacks;
16038        app.service_selected = true;
16039        app.mode = Mode::Normal;
16040        app.cfn_state.input_focus = InputFocus::Pagination;
16041
16042        // Start filter should reset focus to Input
16043        app.handle_action(Action::StartFilter);
16044        assert_eq!(app.mode, Mode::FilterInput);
16045        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
16046    }
16047
16048    #[test]
16049    fn test_iam_roles_input_focus_cycles_forward() {
16050        let mut app = test_app();
16051        app.current_service = Service::IamRoles;
16052        app.mode = Mode::FilterInput;
16053        app.iam_state.role_input_focus = InputFocus::Filter;
16054
16055        app.handle_action(Action::NextFilterFocus);
16056        assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
16057
16058        app.handle_action(Action::NextFilterFocus);
16059        assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
16060    }
16061
16062    #[test]
16063    fn test_iam_roles_input_focus_cycles_backward() {
16064        let mut app = test_app();
16065        app.current_service = Service::IamRoles;
16066        app.mode = Mode::FilterInput;
16067        app.iam_state.role_input_focus = InputFocus::Filter;
16068
16069        app.handle_action(Action::PrevFilterFocus);
16070        assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
16071
16072        app.handle_action(Action::PrevFilterFocus);
16073        assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
16074    }
16075
16076    #[test]
16077    fn test_iam_roles_filter_input_only_when_focused() {
16078        let mut app = test_app();
16079        app.current_service = Service::IamRoles;
16080        app.mode = Mode::FilterInput;
16081        app.iam_state.role_input_focus = InputFocus::Pagination;
16082        app.iam_state.roles.filter = String::new();
16083
16084        // Typing should not add to filter when focus is on Pagination
16085        app.handle_action(Action::FilterInput('t'));
16086        app.handle_action(Action::FilterInput('e'));
16087        app.handle_action(Action::FilterInput('s'));
16088        app.handle_action(Action::FilterInput('t'));
16089        assert_eq!(app.iam_state.roles.filter, "");
16090
16091        // Switch to Input focus
16092        app.iam_state.role_input_focus = InputFocus::Filter;
16093        app.handle_action(Action::FilterInput('t'));
16094        app.handle_action(Action::FilterInput('e'));
16095        app.handle_action(Action::FilterInput('s'));
16096        app.handle_action(Action::FilterInput('t'));
16097        assert_eq!(app.iam_state.roles.filter, "test");
16098    }
16099
16100    #[test]
16101    fn test_iam_roles_page_down_updates_scroll_offset() {
16102        let mut app = test_app();
16103        app.current_service = Service::IamRoles;
16104        app.mode = Mode::Normal;
16105        app.iam_state.roles.items = (0..50)
16106            .map(|i| IamRole {
16107                role_name: format!("role-{}", i),
16108                path: "/".to_string(),
16109                trusted_entities: "AWS Service".to_string(),
16110                last_activity: "N/A".to_string(),
16111                arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
16112                creation_time: "2024-01-01".to_string(),
16113                description: String::new(),
16114                max_session_duration: Some(3600),
16115            })
16116            .collect();
16117
16118        app.iam_state.roles.selected = 0;
16119        app.iam_state.roles.scroll_offset = 0;
16120
16121        // Page down should update both selected and scroll_offset
16122        app.handle_action(Action::PageDown);
16123        assert_eq!(app.iam_state.roles.selected, 10);
16124        // scroll_offset should be updated to keep selection visible
16125        assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
16126
16127        // Page down again
16128        app.handle_action(Action::PageDown);
16129        assert_eq!(app.iam_state.roles.selected, 20);
16130        assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
16131    }
16132
16133    #[test]
16134    fn test_application_selection_and_deployments_tab() {
16135        use crate::lambda::Application as LambdaApplication;
16136        use LambdaApplicationDetailTab;
16137
16138        let mut app = test_app();
16139        app.current_service = Service::LambdaApplications;
16140        app.service_selected = true;
16141        app.mode = Mode::Normal;
16142
16143        app.lambda_application_state.table.items = vec![LambdaApplication {
16144            name: "test-app".to_string(),
16145            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
16146            description: "Test application".to_string(),
16147            status: "CREATE_COMPLETE".to_string(),
16148            last_modified: "2024-01-01".to_string(),
16149        }];
16150
16151        // Select application
16152        app.handle_action(Action::Select);
16153        assert_eq!(
16154            app.lambda_application_state.current_application,
16155            Some("test-app".to_string())
16156        );
16157        assert_eq!(
16158            app.lambda_application_state.detail_tab,
16159            LambdaApplicationDetailTab::Overview
16160        );
16161
16162        // Switch to Deployments tab
16163        app.handle_action(Action::NextDetailTab);
16164        assert_eq!(
16165            app.lambda_application_state.detail_tab,
16166            LambdaApplicationDetailTab::Deployments
16167        );
16168
16169        // Go back
16170        app.handle_action(Action::GoBack);
16171        assert_eq!(app.lambda_application_state.current_application, None);
16172    }
16173
16174    #[test]
16175    fn test_application_resources_filter_and_pagination() {
16176        use crate::lambda::Application as LambdaApplication;
16177        use LambdaApplicationDetailTab;
16178
16179        let mut app = test_app();
16180        app.current_service = Service::LambdaApplications;
16181        app.service_selected = true;
16182        app.mode = Mode::Normal;
16183
16184        app.lambda_application_state.table.items = vec![LambdaApplication {
16185            name: "test-app".to_string(),
16186            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
16187            description: "Test application".to_string(),
16188            status: "CREATE_COMPLETE".to_string(),
16189            last_modified: "2024-01-01".to_string(),
16190        }];
16191
16192        // Select application
16193        app.handle_action(Action::Select);
16194        assert_eq!(
16195            app.lambda_application_state.detail_tab,
16196            LambdaApplicationDetailTab::Overview
16197        );
16198
16199        // Verify resources were loaded
16200        assert!(!app.lambda_application_state.resources.items.is_empty());
16201
16202        // Test filter focus cycling
16203        app.mode = Mode::FilterInput;
16204        assert_eq!(
16205            app.lambda_application_state.resource_input_focus,
16206            InputFocus::Filter
16207        );
16208
16209        app.handle_action(Action::NextFilterFocus);
16210        assert_eq!(
16211            app.lambda_application_state.resource_input_focus,
16212            InputFocus::Pagination
16213        );
16214
16215        app.handle_action(Action::PrevFilterFocus);
16216        assert_eq!(
16217            app.lambda_application_state.resource_input_focus,
16218            InputFocus::Filter
16219        );
16220    }
16221
16222    #[test]
16223    fn test_application_deployments_filter_and_pagination() {
16224        use crate::lambda::Application as LambdaApplication;
16225        use LambdaApplicationDetailTab;
16226
16227        let mut app = test_app();
16228        app.current_service = Service::LambdaApplications;
16229        app.service_selected = true;
16230        app.mode = Mode::Normal;
16231
16232        app.lambda_application_state.table.items = vec![LambdaApplication {
16233            name: "test-app".to_string(),
16234            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
16235            description: "Test application".to_string(),
16236            status: "CREATE_COMPLETE".to_string(),
16237            last_modified: "2024-01-01".to_string(),
16238        }];
16239
16240        // Select application and switch to Deployments tab
16241        app.handle_action(Action::Select);
16242        app.handle_action(Action::NextDetailTab);
16243        assert_eq!(
16244            app.lambda_application_state.detail_tab,
16245            LambdaApplicationDetailTab::Deployments
16246        );
16247
16248        // Verify deployments were loaded
16249        assert!(!app.lambda_application_state.deployments.items.is_empty());
16250
16251        // Test filter focus cycling
16252        app.mode = Mode::FilterInput;
16253        assert_eq!(
16254            app.lambda_application_state.deployment_input_focus,
16255            InputFocus::Filter
16256        );
16257
16258        app.handle_action(Action::NextFilterFocus);
16259        assert_eq!(
16260            app.lambda_application_state.deployment_input_focus,
16261            InputFocus::Pagination
16262        );
16263
16264        app.handle_action(Action::PrevFilterFocus);
16265        assert_eq!(
16266            app.lambda_application_state.deployment_input_focus,
16267            InputFocus::Filter
16268        );
16269    }
16270
16271    #[test]
16272    fn test_application_resource_expansion() {
16273        use crate::lambda::Application as LambdaApplication;
16274        use LambdaApplicationDetailTab;
16275
16276        let mut app = test_app();
16277        app.current_service = Service::LambdaApplications;
16278        app.service_selected = true;
16279        app.mode = Mode::Normal;
16280
16281        app.lambda_application_state.table.items = vec![LambdaApplication {
16282            name: "test-app".to_string(),
16283            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
16284            description: "Test application".to_string(),
16285            status: "CREATE_COMPLETE".to_string(),
16286            last_modified: "2024-01-01".to_string(),
16287        }];
16288
16289        // Select application (Overview tab by default)
16290        app.handle_action(Action::Select);
16291        assert_eq!(
16292            app.lambda_application_state.detail_tab,
16293            LambdaApplicationDetailTab::Overview
16294        );
16295
16296        // Expand resource
16297        app.handle_action(Action::NextPane);
16298        assert_eq!(
16299            app.lambda_application_state.resources.expanded_item,
16300            Some(0)
16301        );
16302
16303        // Collapse resource
16304        app.handle_action(Action::PrevPane);
16305        assert_eq!(app.lambda_application_state.resources.expanded_item, None);
16306    }
16307
16308    #[test]
16309    fn test_application_deployment_expansion() {
16310        use crate::lambda::Application as LambdaApplication;
16311        use LambdaApplicationDetailTab;
16312
16313        let mut app = test_app();
16314        app.current_service = Service::LambdaApplications;
16315        app.service_selected = true;
16316        app.mode = Mode::Normal;
16317
16318        app.lambda_application_state.table.items = vec![LambdaApplication {
16319            name: "test-app".to_string(),
16320            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
16321            description: "Test application".to_string(),
16322            status: "CREATE_COMPLETE".to_string(),
16323            last_modified: "2024-01-01".to_string(),
16324        }];
16325
16326        // Select application and switch to Deployments tab
16327        app.handle_action(Action::Select);
16328        app.handle_action(Action::NextDetailTab);
16329        assert_eq!(
16330            app.lambda_application_state.detail_tab,
16331            LambdaApplicationDetailTab::Deployments
16332        );
16333
16334        // Expand deployment
16335        app.handle_action(Action::NextPane);
16336        assert_eq!(
16337            app.lambda_application_state.deployments.expanded_item,
16338            Some(0)
16339        );
16340
16341        // Collapse deployment
16342        app.handle_action(Action::PrevPane);
16343        assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
16344    }
16345
16346    #[test]
16347    fn test_s3_nested_prefix_expansion() {
16348        use crate::s3::Bucket;
16349        use S3Object;
16350
16351        let mut app = test_app();
16352        app.current_service = Service::S3Buckets;
16353        app.service_selected = true;
16354        app.mode = Mode::Normal;
16355
16356        // Setup bucket with nested prefixes (2 levels)
16357        app.s3_state.buckets.items = vec![Bucket {
16358            name: "test-bucket".to_string(),
16359            region: "us-east-1".to_string(),
16360            creation_date: "2024-01-01".to_string(),
16361        }];
16362
16363        // Level 1: bucket preview
16364        app.s3_state.bucket_preview.insert(
16365            "test-bucket".to_string(),
16366            vec![S3Object {
16367                key: "level1/".to_string(),
16368                size: 0,
16369                last_modified: "".to_string(),
16370                is_prefix: true,
16371                storage_class: "".to_string(),
16372            }],
16373        );
16374
16375        // Level 2: nested prefix
16376        app.s3_state.prefix_preview.insert(
16377            "level1/".to_string(),
16378            vec![S3Object {
16379                key: "level1/level2/".to_string(),
16380                size: 0,
16381                last_modified: "".to_string(),
16382                is_prefix: true,
16383                storage_class: "".to_string(),
16384            }],
16385        );
16386
16387        // Expand bucket (row 0)
16388        app.s3_state.selected_row = 0;
16389        app.handle_action(Action::NextPane);
16390        assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
16391
16392        // Expand level1/ (row 1)
16393        app.s3_state.selected_row = 1;
16394        app.handle_action(Action::NextPane);
16395        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
16396
16397        // Expand level2/ (row 2) - verifies nested expansion works
16398        app.s3_state.selected_row = 2;
16399        app.handle_action(Action::NextPane);
16400        assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
16401
16402        // Verify all are still expanded
16403        assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
16404        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
16405    }
16406
16407    #[test]
16408    fn test_s3_nested_prefix_collapse() {
16409        use crate::s3::Bucket;
16410        use S3Object;
16411
16412        let mut app = test_app();
16413        app.current_service = Service::S3Buckets;
16414        app.service_selected = true;
16415        app.mode = Mode::Normal;
16416
16417        app.s3_state.buckets.items = vec![Bucket {
16418            name: "test-bucket".to_string(),
16419            region: "us-east-1".to_string(),
16420            creation_date: "2024-01-01".to_string(),
16421        }];
16422
16423        app.s3_state.bucket_preview.insert(
16424            "test-bucket".to_string(),
16425            vec![S3Object {
16426                key: "level1/".to_string(),
16427                size: 0,
16428                last_modified: "".to_string(),
16429                is_prefix: true,
16430                storage_class: "".to_string(),
16431            }],
16432        );
16433
16434        app.s3_state.prefix_preview.insert(
16435            "level1/".to_string(),
16436            vec![S3Object {
16437                key: "level1/level2/".to_string(),
16438                size: 0,
16439                last_modified: "".to_string(),
16440                is_prefix: true,
16441                storage_class: "".to_string(),
16442            }],
16443        );
16444
16445        // Pre-expand all levels
16446        app.s3_state
16447            .expanded_prefixes
16448            .insert("test-bucket".to_string());
16449        app.s3_state.expanded_prefixes.insert("level1/".to_string());
16450        app.s3_state
16451            .expanded_prefixes
16452            .insert("level1/level2/".to_string());
16453
16454        // Collapse level2/ (row 2)
16455        app.s3_state.selected_row = 2;
16456        app.handle_action(Action::PrevPane);
16457        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
16458        assert!(app.s3_state.expanded_prefixes.contains("level1/")); // Parent still expanded
16459
16460        // Collapse level1/ (row 1)
16461        app.s3_state.selected_row = 1;
16462        app.handle_action(Action::PrevPane);
16463        assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
16464        assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); // Bucket still expanded
16465
16466        // Collapse bucket (row 0)
16467        app.s3_state.selected_row = 0;
16468        app.handle_action(Action::PrevPane);
16469        assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
16470    }
16471}
16472
16473#[cfg(test)]
16474mod sqs_tests {
16475    use super::*;
16476    use test_helpers::*;
16477
16478    #[test]
16479    fn test_sqs_filter_input() {
16480        let mut app = test_app();
16481        app.current_service = Service::SqsQueues;
16482        app.service_selected = true;
16483        app.mode = Mode::FilterInput;
16484
16485        app.handle_action(Action::FilterInput('t'));
16486        app.handle_action(Action::FilterInput('e'));
16487        app.handle_action(Action::FilterInput('s'));
16488        app.handle_action(Action::FilterInput('t'));
16489        assert_eq!(app.sqs_state.queues.filter, "test");
16490
16491        app.handle_action(Action::FilterBackspace);
16492        assert_eq!(app.sqs_state.queues.filter, "tes");
16493    }
16494
16495    #[test]
16496    fn test_sqs_start_filter() {
16497        let mut app = test_app();
16498        app.current_service = Service::SqsQueues;
16499        app.service_selected = true;
16500        app.mode = Mode::Normal;
16501
16502        app.handle_action(Action::StartFilter);
16503        assert_eq!(app.mode, Mode::FilterInput);
16504        assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
16505    }
16506
16507    #[test]
16508    fn test_sqs_filter_focus_cycling() {
16509        let mut app = test_app();
16510        app.current_service = Service::SqsQueues;
16511        app.service_selected = true;
16512        app.mode = Mode::FilterInput;
16513        app.sqs_state.input_focus = InputFocus::Filter;
16514
16515        app.handle_action(Action::NextFilterFocus);
16516        assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
16517
16518        app.handle_action(Action::NextFilterFocus);
16519        assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
16520
16521        app.handle_action(Action::PrevFilterFocus);
16522        assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
16523    }
16524
16525    #[test]
16526    fn test_sqs_navigation() {
16527        let mut app = test_app();
16528        app.current_service = Service::SqsQueues;
16529        app.service_selected = true;
16530        app.mode = Mode::Normal;
16531        app.sqs_state.queues.items = (0..10)
16532            .map(|i| SqsQueue {
16533                name: format!("queue{}", i),
16534                url: String::new(),
16535                queue_type: "Standard".to_string(),
16536                created_timestamp: String::new(),
16537                messages_available: "0".to_string(),
16538                messages_in_flight: "0".to_string(),
16539                encryption: "Disabled".to_string(),
16540                content_based_deduplication: "Disabled".to_string(),
16541                last_modified_timestamp: String::new(),
16542                visibility_timeout: String::new(),
16543                message_retention_period: String::new(),
16544                maximum_message_size: String::new(),
16545                delivery_delay: String::new(),
16546                receive_message_wait_time: String::new(),
16547                high_throughput_fifo: "N/A".to_string(),
16548                deduplication_scope: "N/A".to_string(),
16549                fifo_throughput_limit: "N/A".to_string(),
16550                dead_letter_queue: "-".to_string(),
16551                messages_delayed: "0".to_string(),
16552                redrive_allow_policy: "-".to_string(),
16553                redrive_policy: "".to_string(),
16554                redrive_task_id: "-".to_string(),
16555                redrive_task_start_time: "-".to_string(),
16556                redrive_task_status: "-".to_string(),
16557                redrive_task_percent: "-".to_string(),
16558                redrive_task_destination: "-".to_string(),
16559            })
16560            .collect();
16561
16562        app.handle_action(Action::NextItem);
16563        assert_eq!(app.sqs_state.queues.selected, 1);
16564
16565        app.handle_action(Action::PrevItem);
16566        assert_eq!(app.sqs_state.queues.selected, 0);
16567    }
16568
16569    #[test]
16570    fn test_sqs_page_navigation() {
16571        let mut app = test_app();
16572        app.current_service = Service::SqsQueues;
16573        app.service_selected = true;
16574        app.mode = Mode::Normal;
16575        app.sqs_state.queues.items = (0..100)
16576            .map(|i| SqsQueue {
16577                name: format!("queue{}", i),
16578                url: String::new(),
16579                queue_type: "Standard".to_string(),
16580                created_timestamp: String::new(),
16581                messages_available: "0".to_string(),
16582                messages_in_flight: "0".to_string(),
16583                encryption: "Disabled".to_string(),
16584                content_based_deduplication: "Disabled".to_string(),
16585                last_modified_timestamp: String::new(),
16586                visibility_timeout: String::new(),
16587                message_retention_period: String::new(),
16588                maximum_message_size: String::new(),
16589                delivery_delay: String::new(),
16590                receive_message_wait_time: String::new(),
16591                high_throughput_fifo: "N/A".to_string(),
16592                deduplication_scope: "N/A".to_string(),
16593                fifo_throughput_limit: "N/A".to_string(),
16594                dead_letter_queue: "-".to_string(),
16595                messages_delayed: "0".to_string(),
16596                redrive_allow_policy: "-".to_string(),
16597                redrive_policy: "".to_string(),
16598                redrive_task_id: "-".to_string(),
16599                redrive_task_start_time: "-".to_string(),
16600                redrive_task_status: "-".to_string(),
16601                redrive_task_percent: "-".to_string(),
16602                redrive_task_destination: "-".to_string(),
16603            })
16604            .collect();
16605
16606        app.handle_action(Action::PageDown);
16607        assert_eq!(app.sqs_state.queues.selected, 10);
16608
16609        app.handle_action(Action::PageUp);
16610        assert_eq!(app.sqs_state.queues.selected, 0);
16611    }
16612
16613    #[test]
16614    fn test_sqs_queue_expansion() {
16615        let mut app = test_app();
16616        app.current_service = Service::SqsQueues;
16617        app.service_selected = true;
16618        app.sqs_state.queues.items = vec![SqsQueue {
16619            name: "my-queue".to_string(),
16620            url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
16621            queue_type: "Standard".to_string(),
16622            created_timestamp: "2023-01-01".to_string(),
16623            messages_available: "5".to_string(),
16624            messages_in_flight: "2".to_string(),
16625            encryption: "Enabled".to_string(),
16626            content_based_deduplication: "Disabled".to_string(),
16627            last_modified_timestamp: "2023-01-02".to_string(),
16628            visibility_timeout: "30".to_string(),
16629            message_retention_period: "345600".to_string(),
16630            maximum_message_size: "262144".to_string(),
16631            delivery_delay: "0".to_string(),
16632            receive_message_wait_time: "0".to_string(),
16633            high_throughput_fifo: "N/A".to_string(),
16634            deduplication_scope: "N/A".to_string(),
16635            fifo_throughput_limit: "N/A".to_string(),
16636            dead_letter_queue: "-".to_string(),
16637            messages_delayed: "0".to_string(),
16638            redrive_allow_policy: "-".to_string(),
16639            redrive_policy: "".to_string(),
16640            redrive_task_id: "-".to_string(),
16641            redrive_task_start_time: "-".to_string(),
16642            redrive_task_status: "-".to_string(),
16643            redrive_task_percent: "-".to_string(),
16644            redrive_task_destination: "-".to_string(),
16645        }];
16646        app.sqs_state.queues.selected = 0;
16647
16648        assert_eq!(app.sqs_state.queues.expanded_item, None);
16649
16650        // Right arrow expands
16651        app.handle_action(Action::NextPane);
16652        assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
16653
16654        // Right arrow again keeps it expanded
16655        app.handle_action(Action::NextPane);
16656        assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
16657
16658        // Left arrow collapses
16659        app.handle_action(Action::PrevPane);
16660        assert_eq!(app.sqs_state.queues.expanded_item, None);
16661
16662        // Left arrow again keeps it collapsed
16663        app.handle_action(Action::PrevPane);
16664        assert_eq!(app.sqs_state.queues.expanded_item, None);
16665    }
16666
16667    #[test]
16668    fn test_sqs_column_toggle() {
16669        use crate::sqs::queue::Column as SqsColumn;
16670        let mut app = test_app();
16671        app.current_service = Service::SqsQueues;
16672        app.service_selected = true;
16673        app.mode = Mode::ColumnSelector;
16674
16675        // Start with all columns visible
16676        app.sqs_visible_column_ids = SqsColumn::ids();
16677        let initial_count = app.sqs_visible_column_ids.len();
16678
16679        // Select first column (index 0) and toggle it
16680        app.column_selector_index = 0;
16681        app.handle_action(Action::ToggleColumn);
16682
16683        // First column should be removed
16684        assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
16685        assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
16686
16687        // Toggle it back
16688        app.handle_action(Action::ToggleColumn);
16689        assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
16690        assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
16691    }
16692
16693    #[test]
16694    fn test_sqs_column_selector_navigation() {
16695        let mut app = test_app();
16696        app.current_service = Service::SqsQueues;
16697        app.service_selected = true;
16698        app.mode = Mode::ColumnSelector;
16699        app.column_selector_index = 0;
16700
16701        // Should be able to navigate through all columns
16702        let max_index = app.sqs_column_ids.len() - 1;
16703
16704        // Navigate to last column
16705        for _ in 0..max_index {
16706            app.handle_action(Action::NextItem);
16707        }
16708        assert_eq!(app.column_selector_index, max_index);
16709
16710        // Navigate back to first
16711        for _ in 0..max_index {
16712            app.handle_action(Action::PrevItem);
16713        }
16714        assert_eq!(app.column_selector_index, 0);
16715    }
16716
16717    #[test]
16718    fn test_sqs_queue_selection() {
16719        let mut app = test_app();
16720        app.current_service = Service::SqsQueues;
16721        app.service_selected = true;
16722        app.mode = Mode::Normal;
16723        app.sqs_state.queues.items = vec![SqsQueue {
16724            name: "my-queue".to_string(),
16725            url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
16726            queue_type: "Standard".to_string(),
16727            created_timestamp: "2023-01-01".to_string(),
16728            messages_available: "5".to_string(),
16729            messages_in_flight: "2".to_string(),
16730            encryption: "Enabled".to_string(),
16731            content_based_deduplication: "Disabled".to_string(),
16732            last_modified_timestamp: "2023-01-02".to_string(),
16733            visibility_timeout: "30".to_string(),
16734            message_retention_period: "345600".to_string(),
16735            maximum_message_size: "262144".to_string(),
16736            delivery_delay: "0".to_string(),
16737            receive_message_wait_time: "0".to_string(),
16738            high_throughput_fifo: "N/A".to_string(),
16739            deduplication_scope: "N/A".to_string(),
16740            fifo_throughput_limit: "N/A".to_string(),
16741            dead_letter_queue: "-".to_string(),
16742            messages_delayed: "0".to_string(),
16743            redrive_allow_policy: "-".to_string(),
16744            redrive_policy: "".to_string(),
16745            redrive_task_id: "-".to_string(),
16746            redrive_task_start_time: "-".to_string(),
16747            redrive_task_status: "-".to_string(),
16748            redrive_task_percent: "-".to_string(),
16749            redrive_task_destination: "-".to_string(),
16750        }];
16751        app.sqs_state.queues.selected = 0;
16752
16753        assert_eq!(app.sqs_state.current_queue, None);
16754
16755        // Select queue
16756        app.handle_action(Action::Select);
16757        assert_eq!(
16758            app.sqs_state.current_queue,
16759            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
16760        );
16761
16762        // Go back
16763        app.handle_action(Action::GoBack);
16764        assert_eq!(app.sqs_state.current_queue, None);
16765    }
16766
16767    #[test]
16768    fn test_sqs_lambda_triggers_expand_collapse() {
16769        let mut app = test_app();
16770        app.current_service = Service::SqsQueues;
16771        app.service_selected = true;
16772        app.sqs_state.current_queue =
16773            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16774        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16775        app.sqs_state.triggers.items = vec![LambdaTrigger {
16776            uuid: "test-uuid".to_string(),
16777            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
16778            status: "Enabled".to_string(),
16779            last_modified: "2024-01-01T00:00:00Z".to_string(),
16780        }];
16781        app.sqs_state.triggers.selected = 0;
16782
16783        assert_eq!(app.sqs_state.triggers.expanded_item, None);
16784
16785        // Right arrow expands
16786        app.handle_action(Action::NextPane);
16787        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
16788
16789        // Left arrow collapses
16790        app.handle_action(Action::PrevPane);
16791        assert_eq!(app.sqs_state.triggers.expanded_item, None);
16792    }
16793
16794    #[test]
16795    fn test_sqs_lambda_triggers_expand_toggle() {
16796        let mut app = test_app();
16797        app.current_service = Service::SqsQueues;
16798        app.service_selected = true;
16799        app.sqs_state.current_queue =
16800            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16801        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16802        app.sqs_state.triggers.items = vec![LambdaTrigger {
16803            uuid: "test-uuid".to_string(),
16804            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
16805            status: "Enabled".to_string(),
16806            last_modified: "2024-01-01T00:00:00Z".to_string(),
16807        }];
16808        app.sqs_state.triggers.selected = 0;
16809
16810        // Expand
16811        app.handle_action(Action::NextPane);
16812        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
16813
16814        // Toggle collapses
16815        app.handle_action(Action::NextPane);
16816        assert_eq!(app.sqs_state.triggers.expanded_item, None);
16817
16818        // Toggle expands again
16819        app.handle_action(Action::NextPane);
16820        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
16821    }
16822
16823    #[test]
16824    fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
16825        use crate::ui::sqs::filtered_lambda_triggers;
16826
16827        let mut app = test_app();
16828        app.current_service = Service::SqsQueues;
16829        app.service_selected = true;
16830        app.sqs_state.current_queue =
16831            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16832        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16833        app.sqs_state.triggers.items = vec![
16834            LambdaTrigger {
16835                uuid: "uuid-3".to_string(),
16836                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
16837                status: "Enabled".to_string(),
16838                last_modified: "2024-03-01T00:00:00Z".to_string(),
16839            },
16840            LambdaTrigger {
16841                uuid: "uuid-1".to_string(),
16842                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
16843                status: "Enabled".to_string(),
16844                last_modified: "2024-01-01T00:00:00Z".to_string(),
16845            },
16846            LambdaTrigger {
16847                uuid: "uuid-2".to_string(),
16848                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
16849                status: "Enabled".to_string(),
16850                last_modified: "2024-02-01T00:00:00Z".to_string(),
16851            },
16852        ];
16853
16854        let sorted = filtered_lambda_triggers(&app);
16855
16856        // Should be sorted by last_modified ASC
16857        assert_eq!(sorted.len(), 3);
16858        assert_eq!(sorted[0].uuid, "uuid-1");
16859        assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
16860        assert_eq!(sorted[1].uuid, "uuid-2");
16861        assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
16862        assert_eq!(sorted[2].uuid, "uuid-3");
16863        assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
16864    }
16865
16866    #[test]
16867    fn test_sqs_lambda_triggers_filter_input() {
16868        let mut app = test_app();
16869        app.current_service = Service::SqsQueues;
16870        app.service_selected = true;
16871        app.mode = Mode::FilterInput;
16872        app.sqs_state.current_queue =
16873            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16874        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16875        app.sqs_state.input_focus = InputFocus::Filter;
16876
16877        assert_eq!(app.sqs_state.triggers.filter, "");
16878
16879        // Type characters
16880        app.handle_action(Action::FilterInput('t'));
16881        assert_eq!(app.sqs_state.triggers.filter, "t");
16882
16883        app.handle_action(Action::FilterInput('e'));
16884        assert_eq!(app.sqs_state.triggers.filter, "te");
16885
16886        app.handle_action(Action::FilterInput('s'));
16887        assert_eq!(app.sqs_state.triggers.filter, "tes");
16888
16889        app.handle_action(Action::FilterInput('t'));
16890        assert_eq!(app.sqs_state.triggers.filter, "test");
16891
16892        // Backspace
16893        app.handle_action(Action::FilterBackspace);
16894        assert_eq!(app.sqs_state.triggers.filter, "tes");
16895    }
16896
16897    #[test]
16898    fn test_sqs_lambda_triggers_filter_applied() {
16899        use crate::ui::sqs::filtered_lambda_triggers;
16900
16901        let mut app = test_app();
16902        app.current_service = Service::SqsQueues;
16903        app.service_selected = true;
16904        app.sqs_state.current_queue =
16905            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
16906        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16907        app.sqs_state.triggers.items = vec![
16908            LambdaTrigger {
16909                uuid: "uuid-1".to_string(),
16910                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
16911                status: "Enabled".to_string(),
16912                last_modified: "2024-01-01T00:00:00Z".to_string(),
16913            },
16914            LambdaTrigger {
16915                uuid: "uuid-2".to_string(),
16916                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
16917                status: "Enabled".to_string(),
16918                last_modified: "2024-02-01T00:00:00Z".to_string(),
16919            },
16920            LambdaTrigger {
16921                uuid: "uuid-3".to_string(),
16922                arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
16923                status: "Enabled".to_string(),
16924                last_modified: "2024-03-01T00:00:00Z".to_string(),
16925            },
16926        ];
16927
16928        // No filter - all items
16929        let filtered = filtered_lambda_triggers(&app);
16930        assert_eq!(filtered.len(), 3);
16931
16932        // Filter by "alpha"
16933        app.sqs_state.triggers.filter = "alpha".to_string();
16934        let filtered = filtered_lambda_triggers(&app);
16935        assert_eq!(filtered.len(), 1);
16936        assert_eq!(
16937            filtered[0].arn,
16938            "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
16939        );
16940
16941        // Filter by "test" - matches 2
16942        app.sqs_state.triggers.filter = "test".to_string();
16943        let filtered = filtered_lambda_triggers(&app);
16944        assert_eq!(filtered.len(), 2);
16945        assert_eq!(
16946            filtered[0].arn,
16947            "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
16948        );
16949        assert_eq!(
16950            filtered[1].arn,
16951            "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
16952        );
16953
16954        // Filter by uuid
16955        app.sqs_state.triggers.filter = "uuid-3".to_string();
16956        let filtered = filtered_lambda_triggers(&app);
16957        assert_eq!(filtered.len(), 1);
16958        assert_eq!(filtered[0].uuid, "uuid-3");
16959    }
16960
16961    #[test]
16962    fn test_sqs_triggers_navigation() {
16963        let mut app = test_app();
16964        app.service_selected = true;
16965        app.mode = Mode::Normal;
16966        app.current_service = Service::SqsQueues;
16967        app.sqs_state.current_queue = Some("test-queue".to_string());
16968        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
16969        app.sqs_state.triggers.items = vec![
16970            LambdaTrigger {
16971                uuid: "1".to_string(),
16972                arn: "arn1".to_string(),
16973                status: "Enabled".to_string(),
16974                last_modified: "2024-01-01".to_string(),
16975            },
16976            LambdaTrigger {
16977                uuid: "2".to_string(),
16978                arn: "arn2".to_string(),
16979                status: "Enabled".to_string(),
16980                last_modified: "2024-01-02".to_string(),
16981            },
16982        ];
16983
16984        assert_eq!(app.sqs_state.triggers.selected, 0);
16985        app.next_item();
16986        assert_eq!(app.sqs_state.triggers.selected, 1);
16987        app.prev_item();
16988        assert_eq!(app.sqs_state.triggers.selected, 0);
16989    }
16990
16991    #[test]
16992    fn test_sqs_pipes_navigation() {
16993        let mut app = test_app();
16994        app.service_selected = true;
16995        app.mode = Mode::Normal;
16996        app.current_service = Service::SqsQueues;
16997        app.sqs_state.current_queue = Some("test-queue".to_string());
16998        app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
16999        app.sqs_state.pipes.items = vec![
17000            EventBridgePipe {
17001                name: "pipe1".to_string(),
17002                status: "RUNNING".to_string(),
17003                target: "target1".to_string(),
17004                last_modified: "2024-01-01".to_string(),
17005            },
17006            EventBridgePipe {
17007                name: "pipe2".to_string(),
17008                status: "RUNNING".to_string(),
17009                target: "target2".to_string(),
17010                last_modified: "2024-01-02".to_string(),
17011            },
17012        ];
17013
17014        assert_eq!(app.sqs_state.pipes.selected, 0);
17015        app.next_item();
17016        assert_eq!(app.sqs_state.pipes.selected, 1);
17017        app.prev_item();
17018        assert_eq!(app.sqs_state.pipes.selected, 0);
17019    }
17020
17021    #[test]
17022    fn test_sqs_tags_navigation() {
17023        let mut app = test_app();
17024        app.service_selected = true;
17025        app.mode = Mode::Normal;
17026        app.current_service = Service::SqsQueues;
17027        app.sqs_state.current_queue = Some("test-queue".to_string());
17028        app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
17029        app.sqs_state.tags.items = vec![
17030            SqsQueueTag {
17031                key: "Env".to_string(),
17032                value: "prod".to_string(),
17033            },
17034            SqsQueueTag {
17035                key: "Team".to_string(),
17036                value: "backend".to_string(),
17037            },
17038        ];
17039
17040        assert_eq!(app.sqs_state.tags.selected, 0);
17041        app.next_item();
17042        assert_eq!(app.sqs_state.tags.selected, 1);
17043        app.prev_item();
17044        assert_eq!(app.sqs_state.tags.selected, 0);
17045    }
17046
17047    #[test]
17048    fn test_sqs_queues_navigation() {
17049        let mut app = test_app();
17050        app.service_selected = true;
17051        app.mode = Mode::Normal;
17052        app.current_service = Service::SqsQueues;
17053        app.sqs_state.queues.items = vec![
17054            SqsQueue {
17055                name: "queue1".to_string(),
17056                url: "url1".to_string(),
17057                queue_type: "Standard".to_string(),
17058                created_timestamp: "".to_string(),
17059                messages_available: "0".to_string(),
17060                messages_in_flight: "0".to_string(),
17061                encryption: "Disabled".to_string(),
17062                content_based_deduplication: "Disabled".to_string(),
17063                last_modified_timestamp: "".to_string(),
17064                visibility_timeout: "".to_string(),
17065                message_retention_period: "".to_string(),
17066                maximum_message_size: "".to_string(),
17067                delivery_delay: "".to_string(),
17068                receive_message_wait_time: "".to_string(),
17069                high_throughput_fifo: "-".to_string(),
17070                deduplication_scope: "-".to_string(),
17071                fifo_throughput_limit: "-".to_string(),
17072                dead_letter_queue: "-".to_string(),
17073                messages_delayed: "0".to_string(),
17074                redrive_allow_policy: "-".to_string(),
17075                redrive_policy: "".to_string(),
17076                redrive_task_id: "-".to_string(),
17077                redrive_task_start_time: "-".to_string(),
17078                redrive_task_status: "-".to_string(),
17079                redrive_task_percent: "-".to_string(),
17080                redrive_task_destination: "-".to_string(),
17081            },
17082            SqsQueue {
17083                name: "queue2".to_string(),
17084                url: "url2".to_string(),
17085                queue_type: "Standard".to_string(),
17086                created_timestamp: "".to_string(),
17087                messages_available: "0".to_string(),
17088                messages_in_flight: "0".to_string(),
17089                encryption: "Disabled".to_string(),
17090                content_based_deduplication: "Disabled".to_string(),
17091                last_modified_timestamp: "".to_string(),
17092                visibility_timeout: "".to_string(),
17093                message_retention_period: "".to_string(),
17094                maximum_message_size: "".to_string(),
17095                delivery_delay: "".to_string(),
17096                receive_message_wait_time: "".to_string(),
17097                high_throughput_fifo: "-".to_string(),
17098                deduplication_scope: "-".to_string(),
17099                fifo_throughput_limit: "-".to_string(),
17100                dead_letter_queue: "-".to_string(),
17101                messages_delayed: "0".to_string(),
17102                redrive_allow_policy: "-".to_string(),
17103                redrive_policy: "".to_string(),
17104                redrive_task_id: "-".to_string(),
17105                redrive_task_start_time: "-".to_string(),
17106                redrive_task_status: "-".to_string(),
17107                redrive_task_percent: "-".to_string(),
17108                redrive_task_destination: "-".to_string(),
17109            },
17110        ];
17111
17112        assert_eq!(app.sqs_state.queues.selected, 0);
17113        app.next_item();
17114        assert_eq!(app.sqs_state.queues.selected, 1);
17115        app.prev_item();
17116        assert_eq!(app.sqs_state.queues.selected, 0);
17117    }
17118
17119    #[test]
17120    fn test_sqs_subscriptions_navigation() {
17121        let mut app = test_app();
17122        app.service_selected = true;
17123        app.mode = Mode::Normal;
17124        app.current_service = Service::SqsQueues;
17125        app.sqs_state.current_queue = Some("test-queue".to_string());
17126        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
17127        app.sqs_state.subscriptions.items = vec![
17128            SnsSubscription {
17129                subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
17130                topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
17131            },
17132            SnsSubscription {
17133                subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
17134                topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
17135            },
17136        ];
17137
17138        assert_eq!(app.sqs_state.subscriptions.selected, 0);
17139        app.next_item();
17140        assert_eq!(app.sqs_state.subscriptions.selected, 1);
17141        app.prev_item();
17142        assert_eq!(app.sqs_state.subscriptions.selected, 0);
17143    }
17144
17145    #[test]
17146    fn test_sqs_subscription_region_dropdown_navigation() {
17147        let mut app = test_app();
17148        app.service_selected = true;
17149        app.mode = Mode::FilterInput;
17150        app.current_service = Service::SqsQueues;
17151        app.sqs_state.current_queue = Some("test-queue".to_string());
17152        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
17153        app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
17154
17155        assert_eq!(app.sqs_state.subscription_region_selected, 0);
17156        app.next_item();
17157        assert_eq!(app.sqs_state.subscription_region_selected, 1);
17158        app.next_item();
17159        assert_eq!(app.sqs_state.subscription_region_selected, 2);
17160        app.prev_item();
17161        assert_eq!(app.sqs_state.subscription_region_selected, 1);
17162        app.prev_item();
17163        assert_eq!(app.sqs_state.subscription_region_selected, 0);
17164    }
17165
17166    #[test]
17167    fn test_sqs_subscription_region_selection() {
17168        let mut app = test_app();
17169        app.service_selected = true;
17170        app.mode = Mode::FilterInput;
17171        app.current_service = Service::SqsQueues;
17172        app.sqs_state.current_queue = Some("test-queue".to_string());
17173        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
17174        app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
17175        app.sqs_state.subscription_region_selected = 2; // us-west-1
17176
17177        assert_eq!(app.sqs_state.subscription_region_filter, "");
17178        app.handle_action(Action::ApplyFilter);
17179        assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
17180        assert_eq!(app.mode, Mode::Normal);
17181    }
17182
17183    #[test]
17184    fn test_sqs_subscription_region_change_resets_selection() {
17185        let mut app = test_app();
17186        app.service_selected = true;
17187        app.mode = Mode::FilterInput;
17188        app.current_service = Service::SqsQueues;
17189        app.sqs_state.current_queue = Some("test-queue".to_string());
17190        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
17191        app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
17192        app.sqs_state.subscription_region_selected = 0;
17193        app.sqs_state.subscriptions.selected = 5;
17194
17195        app.handle_action(Action::NextItem);
17196
17197        assert_eq!(app.sqs_state.subscription_region_selected, 1);
17198        assert_eq!(app.sqs_state.subscriptions.selected, 0);
17199    }
17200
17201    #[test]
17202    fn test_s3_object_filter_resets_selection() {
17203        let mut app = test_app();
17204        app.service_selected = true;
17205        app.current_service = Service::S3Buckets;
17206        app.s3_state.current_bucket = Some("test-bucket".to_string());
17207        app.s3_state.selected_row = 5;
17208        app.mode = Mode::FilterInput;
17209
17210        app.handle_action(Action::CloseMenu);
17211
17212        assert_eq!(app.s3_state.selected_row, 0);
17213        assert_eq!(app.mode, Mode::Normal);
17214    }
17215
17216    #[test]
17217    fn test_s3_bucket_filter_resets_selection() {
17218        let mut app = test_app();
17219        app.service_selected = true;
17220        app.current_service = Service::S3Buckets;
17221        app.s3_state.selected_row = 10;
17222        app.mode = Mode::FilterInput;
17223
17224        app.handle_action(Action::CloseMenu);
17225
17226        assert_eq!(app.s3_state.selected_row, 0);
17227        assert_eq!(app.mode, Mode::Normal);
17228    }
17229
17230    #[test]
17231    fn test_s3_selection_stays_in_bounds() {
17232        let mut app = test_app();
17233        app.service_selected = true;
17234        app.current_service = Service::S3Buckets;
17235        app.s3_state.selected_row = 0;
17236        app.s3_state.selected_object = 0;
17237
17238        // Simulate going up from row 0
17239        app.prev_item();
17240
17241        // Should stay at 0, not wrap to negative
17242        assert_eq!(app.s3_state.selected_row, 0);
17243        assert_eq!(app.s3_state.selected_object, 0);
17244    }
17245
17246    #[test]
17247    fn test_cfn_filter_resets_selection() {
17248        let mut app = test_app();
17249        app.service_selected = true;
17250        app.current_service = Service::CloudFormationStacks;
17251        app.cfn_state.table.selected = 10;
17252        app.mode = Mode::FilterInput;
17253
17254        app.handle_action(Action::CloseMenu);
17255
17256        assert_eq!(app.cfn_state.table.selected, 0);
17257        assert_eq!(app.mode, Mode::Normal);
17258    }
17259
17260    #[test]
17261    fn test_lambda_filter_resets_selection() {
17262        let mut app = test_app();
17263        app.service_selected = true;
17264        app.current_service = Service::LambdaFunctions;
17265        app.lambda_state.table.selected = 8;
17266        app.mode = Mode::FilterInput;
17267
17268        app.handle_action(Action::CloseMenu);
17269
17270        assert_eq!(app.lambda_state.table.selected, 0);
17271        assert_eq!(app.mode, Mode::Normal);
17272    }
17273
17274    #[test]
17275    fn test_sqs_filter_resets_selection() {
17276        let mut app = test_app();
17277        app.service_selected = true;
17278        app.current_service = Service::SqsQueues;
17279        app.sqs_state.queues.selected = 7;
17280        app.mode = Mode::FilterInput;
17281
17282        app.handle_action(Action::CloseMenu);
17283
17284        assert_eq!(app.sqs_state.queues.selected, 0);
17285        assert_eq!(app.mode, Mode::Normal);
17286    }
17287
17288    #[test]
17289    fn test_sqs_queues_list_shows_preferences() {
17290        let mut app = test_app();
17291        app.service_selected = true;
17292        app.current_service = Service::SqsQueues;
17293        app.mode = Mode::Normal;
17294
17295        app.handle_action(Action::OpenColumnSelector);
17296
17297        assert_eq!(app.mode, Mode::ColumnSelector);
17298    }
17299
17300    #[test]
17301    fn test_sqs_queue_policies_tab_no_preferences() {
17302        let mut app = test_app();
17303        app.service_selected = true;
17304        app.current_service = Service::SqsQueues;
17305        app.sqs_state.current_queue = Some("test-queue".to_string());
17306        app.sqs_state.detail_tab = SqsQueueDetailTab::QueuePolicies;
17307        app.mode = Mode::Normal;
17308
17309        app.handle_action(Action::OpenColumnSelector);
17310
17311        assert_eq!(app.mode, Mode::Normal);
17312    }
17313
17314    #[test]
17315    fn test_sqs_sns_subscriptions_tab_shows_preferences() {
17316        let mut app = test_app();
17317        app.service_selected = true;
17318        app.current_service = Service::SqsQueues;
17319        app.sqs_state.current_queue = Some("test-queue".to_string());
17320        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
17321        app.mode = Mode::Normal;
17322
17323        app.handle_action(Action::OpenColumnSelector);
17324
17325        assert_eq!(app.mode, Mode::ColumnSelector);
17326    }
17327
17328    #[test]
17329    fn test_sqs_monitoring_tab_no_preferences() {
17330        let mut app = test_app();
17331        app.service_selected = true;
17332        app.current_service = Service::SqsQueues;
17333        app.sqs_state.current_queue = Some("test-queue".to_string());
17334        app.sqs_state.detail_tab = SqsQueueDetailTab::Monitoring;
17335        app.mode = Mode::Normal;
17336
17337        app.handle_action(Action::OpenColumnSelector);
17338
17339        assert_eq!(app.mode, Mode::Normal);
17340    }
17341
17342    #[test]
17343    fn test_cfn_status_filter_change_resets_selection() {
17344        use crate::ui::cfn::STATUS_FILTER;
17345        let mut app = test_app();
17346        app.service_selected = true;
17347        app.current_service = Service::CloudFormationStacks;
17348        app.mode = Mode::FilterInput;
17349        app.cfn_state.input_focus = STATUS_FILTER;
17350        app.cfn_state.status_filter = CfnStatusFilter::All;
17351        app.cfn_state.table.items = vec![
17352            CfnStack {
17353                name: "stack1".to_string(),
17354                stack_id: "id1".to_string(),
17355                status: "CREATE_COMPLETE".to_string(),
17356                created_time: "2024-01-01".to_string(),
17357                updated_time: String::new(),
17358                deleted_time: String::new(),
17359                drift_status: String::new(),
17360                last_drift_check_time: String::new(),
17361                status_reason: String::new(),
17362                description: String::new(),
17363                detailed_status: String::new(),
17364                root_stack: String::new(),
17365                parent_stack: String::new(),
17366                termination_protection: false,
17367                iam_role: String::new(),
17368                tags: Vec::new(),
17369                stack_policy: String::new(),
17370                rollback_monitoring_time: String::new(),
17371                rollback_alarms: Vec::new(),
17372                notification_arns: Vec::new(),
17373            },
17374            CfnStack {
17375                name: "stack2".to_string(),
17376                stack_id: "id2".to_string(),
17377                status: "UPDATE_IN_PROGRESS".to_string(),
17378                created_time: "2024-01-02".to_string(),
17379                updated_time: String::new(),
17380                deleted_time: String::new(),
17381                drift_status: String::new(),
17382                last_drift_check_time: String::new(),
17383                status_reason: String::new(),
17384                description: String::new(),
17385                detailed_status: String::new(),
17386                root_stack: String::new(),
17387                parent_stack: String::new(),
17388                termination_protection: false,
17389                iam_role: String::new(),
17390                tags: Vec::new(),
17391                stack_policy: String::new(),
17392                rollback_monitoring_time: String::new(),
17393                rollback_alarms: Vec::new(),
17394                notification_arns: Vec::new(),
17395            },
17396        ];
17397        app.cfn_state.table.selected = 1;
17398
17399        app.handle_action(Action::NextItem);
17400
17401        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
17402        assert_eq!(app.cfn_state.table.selected, 0);
17403    }
17404
17405    #[test]
17406    fn test_cfn_view_nested_toggle_resets_selection() {
17407        use crate::ui::cfn::VIEW_NESTED;
17408        let mut app = test_app();
17409        app.service_selected = true;
17410        app.current_service = Service::CloudFormationStacks;
17411        app.mode = Mode::FilterInput;
17412        app.cfn_state.input_focus = VIEW_NESTED;
17413        app.cfn_state.view_nested = false;
17414        app.cfn_state.table.items = vec![CfnStack {
17415            name: "stack1".to_string(),
17416            stack_id: "id1".to_string(),
17417            status: "CREATE_COMPLETE".to_string(),
17418            created_time: "2024-01-01".to_string(),
17419            updated_time: String::new(),
17420            deleted_time: String::new(),
17421            drift_status: String::new(),
17422            last_drift_check_time: String::new(),
17423            status_reason: String::new(),
17424            description: String::new(),
17425            detailed_status: String::new(),
17426            root_stack: String::new(),
17427            parent_stack: String::new(),
17428            termination_protection: false,
17429            iam_role: String::new(),
17430            tags: Vec::new(),
17431            stack_policy: String::new(),
17432            rollback_monitoring_time: String::new(),
17433            rollback_alarms: Vec::new(),
17434            notification_arns: Vec::new(),
17435        }];
17436        app.cfn_state.table.selected = 5;
17437
17438        app.handle_action(Action::ToggleFilterCheckbox);
17439
17440        assert!(app.cfn_state.view_nested);
17441        assert_eq!(app.cfn_state.table.selected, 0);
17442    }
17443
17444    #[test]
17445    fn test_cfn_template_scroll_up() {
17446        let mut app = test_app();
17447        app.service_selected = true;
17448        app.current_service = Service::CloudFormationStacks;
17449        app.cfn_state.current_stack = Some("test-stack".to_string());
17450        app.cfn_state.detail_tab = CfnDetailTab::Template;
17451        app.cfn_state.template_scroll = 20;
17452
17453        app.page_up();
17454
17455        assert_eq!(app.cfn_state.template_scroll, 10);
17456    }
17457
17458    #[test]
17459    fn test_cfn_template_scroll_down() {
17460        let mut app = test_app();
17461        app.service_selected = true;
17462        app.current_service = Service::CloudFormationStacks;
17463        app.cfn_state.current_stack = Some("test-stack".to_string());
17464        app.cfn_state.detail_tab = CfnDetailTab::Template;
17465        app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
17466        app.cfn_state.template_scroll = 0;
17467
17468        app.page_down();
17469
17470        assert_eq!(app.cfn_state.template_scroll, 10);
17471    }
17472
17473    #[test]
17474    fn test_cfn_template_scroll_down_respects_max() {
17475        let mut app = test_app();
17476        app.service_selected = true;
17477        app.current_service = Service::CloudFormationStacks;
17478        app.cfn_state.current_stack = Some("test-stack".to_string());
17479        app.cfn_state.detail_tab = CfnDetailTab::Template;
17480        app.cfn_state.template_body = "line1\nline2\nline3".to_string();
17481        app.cfn_state.template_scroll = 0;
17482
17483        app.page_down();
17484
17485        // Should not scroll past the last line (3 lines = max scroll of 2)
17486        assert_eq!(app.cfn_state.template_scroll, 2);
17487    }
17488
17489    #[test]
17490    fn test_cfn_template_arrow_up() {
17491        let mut app = test_app();
17492        app.service_selected = true;
17493        app.current_service = Service::CloudFormationStacks;
17494        app.mode = Mode::Normal;
17495        app.cfn_state.current_stack = Some("test-stack".to_string());
17496        app.cfn_state.detail_tab = CfnDetailTab::Template;
17497        app.cfn_state.template_scroll = 5;
17498
17499        app.prev_item();
17500
17501        assert_eq!(app.cfn_state.template_scroll, 4);
17502    }
17503
17504    #[test]
17505    fn test_cfn_template_arrow_down() {
17506        let mut app = test_app();
17507        app.service_selected = true;
17508        app.current_service = Service::CloudFormationStacks;
17509        app.mode = Mode::Normal;
17510        app.cfn_state.current_stack = Some("test-stack".to_string());
17511        app.cfn_state.detail_tab = CfnDetailTab::Template;
17512        app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
17513        app.cfn_state.template_scroll = 2;
17514
17515        app.next_item();
17516
17517        assert_eq!(app.cfn_state.template_scroll, 3);
17518    }
17519
17520    #[test]
17521    fn test_cfn_template_arrow_down_respects_max() {
17522        let mut app = test_app();
17523        app.service_selected = true;
17524        app.current_service = Service::CloudFormationStacks;
17525        app.mode = Mode::Normal;
17526        app.cfn_state.current_stack = Some("test-stack".to_string());
17527        app.cfn_state.detail_tab = CfnDetailTab::Template;
17528        app.cfn_state.template_body = "line1\nline2".to_string();
17529        app.cfn_state.template_scroll = 1;
17530
17531        app.next_item();
17532
17533        // Should stay at max (2 lines = max scroll of 1)
17534        assert_eq!(app.cfn_state.template_scroll, 1);
17535    }
17536}
17537
17538#[cfg(test)]
17539mod lambda_version_tab_tests {
17540    use super::*;
17541    use crate::ui::iam::POLICY_TYPE_DROPDOWN;
17542    use test_helpers::*;
17543
17544    #[test]
17545    fn test_lambda_version_tab_cycling_next() {
17546        let mut app = test_app();
17547        app.current_service = Service::LambdaFunctions;
17548        app.lambda_state.current_function = Some("test-function".to_string());
17549        app.lambda_state.current_version = Some("1".to_string());
17550        app.lambda_state.detail_tab = LambdaDetailTab::Code;
17551
17552        // Code -> Monitor
17553        app.handle_action(Action::NextDetailTab);
17554        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
17555        assert!(app.lambda_state.metrics_loading);
17556
17557        // Monitor -> Configuration
17558        app.lambda_state.metrics_loading = false;
17559        app.handle_action(Action::NextDetailTab);
17560        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
17561
17562        // Configuration -> Code
17563        app.handle_action(Action::NextDetailTab);
17564        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
17565    }
17566
17567    #[test]
17568    fn test_lambda_version_tab_cycling_prev() {
17569        let mut app = test_app();
17570        app.current_service = Service::LambdaFunctions;
17571        app.lambda_state.current_function = Some("test-function".to_string());
17572        app.lambda_state.current_version = Some("1".to_string());
17573        app.lambda_state.detail_tab = LambdaDetailTab::Code;
17574
17575        // Code -> Configuration
17576        app.handle_action(Action::PrevDetailTab);
17577        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
17578
17579        // Configuration -> Monitor
17580        app.handle_action(Action::PrevDetailTab);
17581        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
17582        assert!(app.lambda_state.metrics_loading);
17583
17584        // Monitor -> Code
17585        app.lambda_state.metrics_loading = false;
17586        app.handle_action(Action::PrevDetailTab);
17587        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
17588    }
17589
17590    #[test]
17591    fn test_lambda_version_monitor_clears_metrics() {
17592        let mut app = test_app();
17593        app.current_service = Service::LambdaFunctions;
17594        app.lambda_state.current_function = Some("test-function".to_string());
17595        app.lambda_state.current_version = Some("1".to_string());
17596        app.lambda_state.detail_tab = LambdaDetailTab::Code;
17597
17598        // Add some fake metric data
17599        app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
17600        app.lambda_state.monitoring_scroll = 5;
17601
17602        // Switch to Monitor tab
17603        app.handle_action(Action::NextDetailTab);
17604
17605        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
17606        assert!(app.lambda_state.metrics_loading);
17607        assert_eq!(app.lambda_state.monitoring_scroll, 0);
17608        assert!(app.lambda_state.metric_data_invocations.is_empty());
17609    }
17610
17611    #[test]
17612    fn test_cfn_parameters_expand_collapse() {
17613        let mut app = test_app();
17614        app.current_service = Service::CloudFormationStacks;
17615        app.service_selected = true;
17616        app.cfn_state.current_stack = Some("test-stack".to_string());
17617        app.cfn_state.detail_tab = CfnDetailTab::Parameters;
17618        app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
17619            key: "Param1".to_string(),
17620            value: "Value1".to_string(),
17621            resolved_value: "Resolved1".to_string(),
17622        }];
17623        app.cfn_state.parameters.reset();
17624
17625        assert_eq!(app.cfn_state.parameters.expanded_item, None);
17626
17627        // Right arrow expands
17628        app.handle_action(Action::NextPane);
17629        assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
17630
17631        // Left arrow collapses
17632        app.handle_action(Action::PrevPane);
17633        assert_eq!(app.cfn_state.parameters.expanded_item, None);
17634    }
17635
17636    #[test]
17637    fn test_cfn_parameters_filter_resets_selection() {
17638        let mut app = test_app();
17639        app.current_service = Service::CloudFormationStacks;
17640        app.service_selected = true;
17641        app.cfn_state.current_stack = Some("test-stack".to_string());
17642        app.cfn_state.detail_tab = CfnDetailTab::Parameters;
17643        app.cfn_state.parameters.items = vec![
17644            rusticity_core::cfn::StackParameter {
17645                key: "DatabaseName".to_string(),
17646                value: "mydb".to_string(),
17647                resolved_value: "mydb".to_string(),
17648            },
17649            rusticity_core::cfn::StackParameter {
17650                key: "InstanceType".to_string(),
17651                value: "t2.micro".to_string(),
17652                resolved_value: "t2.micro".to_string(),
17653            },
17654            rusticity_core::cfn::StackParameter {
17655                key: "Environment".to_string(),
17656                value: "production".to_string(),
17657                resolved_value: "production".to_string(),
17658            },
17659        ];
17660        app.cfn_state.parameters.selected = 2; // Select third item
17661        app.mode = Mode::FilterInput;
17662        app.cfn_state.parameters_input_focus = InputFocus::Filter;
17663
17664        // Type a filter character - should reset selection
17665        app.handle_action(Action::FilterInput('D'));
17666        assert_eq!(app.cfn_state.parameters.selected, 0);
17667        assert_eq!(app.cfn_state.parameters.filter, "D");
17668
17669        // Select another item
17670        app.cfn_state.parameters.selected = 1;
17671
17672        // Type another character - should reset again
17673        app.handle_action(Action::FilterInput('a'));
17674        assert_eq!(app.cfn_state.parameters.selected, 0);
17675        assert_eq!(app.cfn_state.parameters.filter, "Da");
17676
17677        // Select another item
17678        app.cfn_state.parameters.selected = 1;
17679
17680        // Backspace - should also reset
17681        app.handle_action(Action::FilterBackspace);
17682        assert_eq!(app.cfn_state.parameters.selected, 0);
17683        assert_eq!(app.cfn_state.parameters.filter, "D");
17684    }
17685
17686    #[test]
17687    fn test_cfn_template_tab_no_preferences() {
17688        let mut app = test_app();
17689        app.current_service = Service::CloudFormationStacks;
17690        app.service_selected = true;
17691        app.cfn_state.current_stack = Some("test-stack".to_string());
17692        app.cfn_state.detail_tab = CfnDetailTab::Template;
17693        app.mode = Mode::Normal;
17694
17695        // Try to open preferences - should be ignored
17696        app.handle_action(Action::OpenColumnSelector);
17697        assert_eq!(app.mode, Mode::Normal); // Should stay in Normal mode
17698
17699        // GitSync tab should also not allow preferences
17700        app.cfn_state.detail_tab = CfnDetailTab::GitSync;
17701        app.handle_action(Action::OpenColumnSelector);
17702        assert_eq!(app.mode, Mode::Normal); // Should stay in Normal mode
17703
17704        // Parameters tab should allow preferences
17705        app.cfn_state.detail_tab = CfnDetailTab::Parameters;
17706        app.handle_action(Action::OpenColumnSelector);
17707        assert_eq!(app.mode, Mode::ColumnSelector); // Should open preferences
17708
17709        // Outputs tab should allow preferences
17710        app.mode = Mode::Normal;
17711        app.cfn_state.detail_tab = CfnDetailTab::Outputs;
17712        app.handle_action(Action::OpenColumnSelector);
17713        assert_eq!(app.mode, Mode::ColumnSelector); // Should open preferences
17714    }
17715
17716    #[test]
17717    fn test_iam_user_groups_tab_shows_preferences() {
17718        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17719        app.current_service = Service::IamUsers;
17720        app.service_selected = true;
17721        app.mode = Mode::Normal;
17722        app.iam_state.current_user = Some("test-user".to_string());
17723        app.iam_state.user_tab = UserTab::Groups;
17724
17725        // Should allow opening preferences
17726        app.handle_action(Action::OpenColumnSelector);
17727        assert_eq!(app.mode, Mode::ColumnSelector);
17728    }
17729
17730    #[test]
17731    fn test_iam_user_tags_tab_shows_preferences() {
17732        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17733        app.current_service = Service::IamUsers;
17734        app.service_selected = true;
17735        app.mode = Mode::Normal;
17736        app.iam_state.current_user = Some("test-user".to_string());
17737        app.iam_state.user_tab = UserTab::Tags;
17738
17739        // Should allow opening preferences
17740        app.handle_action(Action::OpenColumnSelector);
17741        assert_eq!(app.mode, Mode::ColumnSelector);
17742    }
17743
17744    #[test]
17745    fn test_iam_user_last_accessed_tab_shows_preferences() {
17746        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17747        app.current_service = Service::IamUsers;
17748        app.service_selected = true;
17749        app.mode = Mode::Normal;
17750        app.iam_state.current_user = Some("test-user".to_string());
17751        app.iam_state.user_tab = UserTab::LastAccessed;
17752
17753        // Should allow opening preferences
17754        app.handle_action(Action::OpenColumnSelector);
17755        assert_eq!(app.mode, Mode::ColumnSelector);
17756    }
17757
17758    #[test]
17759    fn test_iam_user_security_credentials_tab_no_preferences() {
17760        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
17761        app.current_service = Service::IamUsers;
17762        app.service_selected = true;
17763        app.mode = Mode::Normal;
17764        app.iam_state.current_user = Some("test-user".to_string());
17765        app.iam_state.user_tab = UserTab::SecurityCredentials;
17766
17767        // Should NOT allow opening preferences
17768        app.handle_action(Action::OpenColumnSelector);
17769        assert_eq!(app.mode, Mode::Normal);
17770    }
17771
17772    #[test]
17773    fn test_iam_user_tabs_without_column_preferences() {
17774        let mut app = test_app();
17775        app.current_service = Service::IamUsers;
17776        app.service_selected = true;
17777        app.iam_state.current_user = Some("test-user".to_string());
17778        app.mode = Mode::Normal;
17779
17780        // Groups tab should allow preferences (page size)
17781        app.iam_state.user_tab = UserTab::Groups;
17782        app.handle_action(Action::OpenColumnSelector);
17783        assert_eq!(app.mode, Mode::ColumnSelector);
17784        app.mode = Mode::Normal;
17785
17786        // Tags tab should allow preferences (page size)
17787        app.iam_state.user_tab = UserTab::Tags;
17788        app.handle_action(Action::OpenColumnSelector);
17789        assert_eq!(app.mode, Mode::ColumnSelector);
17790        app.mode = Mode::Normal;
17791
17792        // SecurityCredentials tab should not allow preferences
17793        app.iam_state.user_tab = UserTab::SecurityCredentials;
17794        app.handle_action(Action::OpenColumnSelector);
17795        assert_eq!(app.mode, Mode::Normal);
17796
17797        // LastAccessed tab should allow preferences (page size)
17798        app.iam_state.user_tab = UserTab::LastAccessed;
17799        app.handle_action(Action::OpenColumnSelector);
17800        assert_eq!(app.mode, Mode::ColumnSelector);
17801        app.mode = Mode::Normal;
17802
17803        // Permissions tab should allow preferences
17804        app.iam_state.user_tab = UserTab::Permissions;
17805        app.handle_action(Action::OpenColumnSelector);
17806        assert_eq!(app.mode, Mode::ColumnSelector);
17807
17808        // User list (no current_user) should allow preferences
17809        app.mode = Mode::Normal;
17810        app.iam_state.current_user = None;
17811        app.handle_action(Action::OpenColumnSelector);
17812        assert_eq!(app.mode, Mode::ColumnSelector);
17813    }
17814
17815    #[test]
17816    fn test_iam_role_policies_dropdown_cycling() {
17817        let mut app = test_app();
17818        app.current_service = Service::IamRoles;
17819        app.service_selected = true;
17820        app.iam_state.current_role = Some("test-role".to_string());
17821        app.iam_state.role_tab = RoleTab::Permissions;
17822        app.mode = Mode::FilterInput;
17823        app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
17824        app.iam_state.policy_type_filter = "All types".to_string();
17825
17826        // Test next cycling
17827        app.next_item();
17828        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17829        app.next_item();
17830        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17831        app.next_item();
17832        assert_eq!(app.iam_state.policy_type_filter, "All types");
17833
17834        // Test prev cycling
17835        app.prev_item();
17836        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17837        app.prev_item();
17838        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17839        app.prev_item();
17840        assert_eq!(app.iam_state.policy_type_filter, "All types");
17841    }
17842
17843    #[test]
17844    fn test_iam_user_policies_dropdown_cycling() {
17845        let mut app = test_app();
17846        app.current_service = Service::IamUsers;
17847        app.service_selected = true;
17848        app.iam_state.current_user = Some("test-user".to_string());
17849        app.iam_state.user_tab = UserTab::Permissions;
17850        app.mode = Mode::FilterInput;
17851        app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
17852        app.iam_state.policy_type_filter = "All types".to_string();
17853
17854        // Test next cycling
17855        app.next_item();
17856        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17857        app.next_item();
17858        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17859        app.next_item();
17860        assert_eq!(app.iam_state.policy_type_filter, "All types");
17861
17862        // Test prev cycling
17863        app.prev_item();
17864        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
17865        app.prev_item();
17866        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
17867        app.prev_item();
17868        assert_eq!(app.iam_state.policy_type_filter, "All types");
17869    }
17870
17871    #[test]
17872    fn test_iam_role_tabs_without_column_preferences() {
17873        let mut app = test_app();
17874        app.current_service = Service::IamRoles;
17875        app.service_selected = true;
17876        app.iam_state.current_role = Some("test-role".to_string());
17877        app.mode = Mode::Normal;
17878
17879        // TrustRelationships tab should not allow preferences
17880        app.iam_state.role_tab = RoleTab::TrustRelationships;
17881        app.handle_action(Action::OpenColumnSelector);
17882        assert_eq!(app.mode, Mode::Normal);
17883
17884        // RevokeSessions tab should not allow preferences
17885        app.iam_state.role_tab = RoleTab::RevokeSessions;
17886        app.handle_action(Action::OpenColumnSelector);
17887        assert_eq!(app.mode, Mode::Normal);
17888
17889        // LastAccessed tab should allow preferences
17890        app.iam_state.role_tab = RoleTab::LastAccessed;
17891        app.handle_action(Action::OpenColumnSelector);
17892        assert_eq!(app.mode, Mode::ColumnSelector);
17893
17894        // Permissions tab should allow preferences
17895        app.mode = Mode::Normal;
17896        app.iam_state.role_tab = RoleTab::Permissions;
17897        app.handle_action(Action::OpenColumnSelector);
17898        assert_eq!(app.mode, Mode::ColumnSelector);
17899
17900        // Tags tab should allow preferences
17901        app.mode = Mode::Normal;
17902        app.iam_state.role_tab = RoleTab::Tags;
17903        app.handle_action(Action::OpenColumnSelector);
17904        assert_eq!(app.mode, Mode::ColumnSelector);
17905
17906        // Role list (no current_role) should allow preferences
17907        app.mode = Mode::Normal;
17908        app.iam_state.current_role = None;
17909        app.handle_action(Action::OpenColumnSelector);
17910        assert_eq!(app.mode, Mode::ColumnSelector);
17911    }
17912
17913    #[test]
17914    fn test_iam_role_tags_tab_cycling() {
17915        let mut app = test_app();
17916        app.current_service = Service::IamRoles;
17917        app.service_selected = true;
17918        app.iam_state.current_role = Some("test-role".to_string());
17919        app.iam_state.role_tab = RoleTab::Tags;
17920        app.mode = Mode::ColumnSelector;
17921        app.column_selector_index = 0;
17922
17923        // NextPreferences from column section -> page size
17924        app.handle_action(Action::NextPreferences);
17925        assert_eq!(app.column_selector_index, 4);
17926
17927        // NextPreferences from page size -> column section
17928        app.handle_action(Action::NextPreferences);
17929        assert_eq!(app.column_selector_index, 0);
17930
17931        // PrevPreferences from column section -> page size
17932        app.handle_action(Action::PrevPreferences);
17933        assert_eq!(app.column_selector_index, 4);
17934
17935        // PrevPreferences from page size -> column section
17936        app.handle_action(Action::PrevPreferences);
17937        assert_eq!(app.column_selector_index, 0);
17938    }
17939
17940    #[test]
17941    fn test_cfn_outputs_expand_collapse() {
17942        let mut app = test_app();
17943        app.current_service = Service::CloudFormationStacks;
17944        app.service_selected = true;
17945        app.cfn_state.current_stack = Some("test-stack".to_string());
17946        app.cfn_state.detail_tab = CfnDetailTab::Outputs;
17947        app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
17948            key: "Output1".to_string(),
17949            value: "Value1".to_string(),
17950            description: "Description1".to_string(),
17951            export_name: "Export1".to_string(),
17952        }];
17953        app.cfn_state.outputs.reset();
17954
17955        assert_eq!(app.cfn_state.outputs.expanded_item, None);
17956
17957        // Right arrow expands
17958        app.handle_action(Action::NextPane);
17959        assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
17960
17961        // Left arrow collapses
17962        app.handle_action(Action::PrevPane);
17963        assert_eq!(app.cfn_state.outputs.expanded_item, None);
17964    }
17965
17966    #[test]
17967    fn test_cfn_outputs_filter_resets_selection() {
17968        let mut app = test_app();
17969        app.current_service = Service::CloudFormationStacks;
17970        app.service_selected = true;
17971        app.cfn_state.current_stack = Some("test-stack".to_string());
17972        app.cfn_state.detail_tab = CfnDetailTab::Outputs;
17973        app.cfn_state.outputs.items = vec![
17974            rusticity_core::cfn::StackOutput {
17975                key: "ApiUrl".to_string(),
17976                value: "https://api.example.com".to_string(),
17977                description: "API endpoint".to_string(),
17978                export_name: "MyApiUrl".to_string(),
17979            },
17980            rusticity_core::cfn::StackOutput {
17981                key: "BucketName".to_string(),
17982                value: "my-bucket".to_string(),
17983                description: "S3 bucket".to_string(),
17984                export_name: "MyBucket".to_string(),
17985            },
17986        ];
17987        app.cfn_state.outputs.reset();
17988        app.cfn_state.outputs.selected = 1;
17989
17990        // Start filter mode
17991        app.handle_action(Action::StartFilter);
17992        assert_eq!(app.mode, Mode::FilterInput);
17993
17994        // Type a character - should reset selection
17995        app.handle_action(Action::FilterInput('A'));
17996        assert_eq!(app.cfn_state.outputs.selected, 0);
17997        assert_eq!(app.cfn_state.outputs.filter, "A");
17998
17999        // Type more
18000        app.cfn_state.outputs.selected = 1;
18001        app.handle_action(Action::FilterInput('p'));
18002        assert_eq!(app.cfn_state.outputs.selected, 0);
18003
18004        // Backspace should also reset selection
18005        app.cfn_state.outputs.selected = 1;
18006        app.handle_action(Action::FilterBackspace);
18007        assert_eq!(app.cfn_state.outputs.selected, 0);
18008    }
18009
18010    #[test]
18011    fn test_ec2_service_in_picker() {
18012        let app = test_app();
18013        assert!(app.service_picker.services.contains(&"EC2 › Instances"));
18014    }
18015
18016    #[test]
18017    fn test_ec2_state_filter_cycles() {
18018        let mut app = test_app();
18019        app.current_service = Service::Ec2Instances;
18020        app.service_selected = true;
18021        app.mode = Mode::FilterInput;
18022        app.ec2_state.input_focus = EC2_STATE_FILTER;
18023
18024        let initial = app.ec2_state.state_filter;
18025        assert_eq!(initial, Ec2StateFilter::AllStates);
18026
18027        // Cycle through filters using ToggleFilterCheckbox
18028        app.handle_action(Action::ToggleFilterCheckbox);
18029        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
18030
18031        app.handle_action(Action::ToggleFilterCheckbox);
18032        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
18033
18034        app.handle_action(Action::ToggleFilterCheckbox);
18035        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
18036
18037        app.handle_action(Action::ToggleFilterCheckbox);
18038        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
18039
18040        app.handle_action(Action::ToggleFilterCheckbox);
18041        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
18042
18043        app.handle_action(Action::ToggleFilterCheckbox);
18044        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
18045
18046        app.handle_action(Action::ToggleFilterCheckbox);
18047        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
18048    }
18049
18050    #[test]
18051    fn test_ec2_filter_resets_table() {
18052        let mut app = test_app();
18053        app.current_service = Service::Ec2Instances;
18054        app.service_selected = true;
18055        app.mode = Mode::FilterInput;
18056        app.ec2_state.input_focus = EC2_STATE_FILTER;
18057        app.ec2_state.table.selected = 5;
18058
18059        app.handle_action(Action::ToggleFilterCheckbox);
18060        assert_eq!(app.ec2_state.table.selected, 0);
18061    }
18062
18063    #[test]
18064    fn test_ec2_columns_visible() {
18065        let app = test_app();
18066        assert_eq!(app.ec2_visible_column_ids.len(), 16); // Default visible columns
18067        assert_eq!(app.ec2_column_ids.len(), 52); // Total available columns
18068    }
18069
18070    #[test]
18071    fn test_ec2_breadcrumbs() {
18072        let mut app = test_app();
18073        app.current_service = Service::Ec2Instances;
18074        app.service_selected = true;
18075        let breadcrumb = app.breadcrumbs();
18076        assert_eq!(breadcrumb, "EC2 > Instances");
18077    }
18078
18079    #[test]
18080    fn test_ec2_console_url() {
18081        let mut app = test_app();
18082        app.current_service = Service::Ec2Instances;
18083        app.service_selected = true;
18084        let url = app.get_console_url();
18085        assert!(url.contains("ec2"));
18086        assert!(url.contains("Instances"));
18087    }
18088
18089    #[test]
18090    fn test_ec2_filter_handling() {
18091        let mut app = test_app();
18092        app.current_service = Service::Ec2Instances;
18093        app.service_selected = true;
18094        app.mode = Mode::FilterInput;
18095
18096        app.handle_action(Action::FilterInput('t'));
18097        app.handle_action(Action::FilterInput('e'));
18098        app.handle_action(Action::FilterInput('s'));
18099        app.handle_action(Action::FilterInput('t'));
18100
18101        assert_eq!(app.ec2_state.table.filter, "test");
18102
18103        app.handle_action(Action::FilterBackspace);
18104        assert_eq!(app.ec2_state.table.filter, "tes");
18105    }
18106
18107    #[test]
18108    fn test_column_selector_page_down_ec2() {
18109        let mut app = test_app();
18110        app.current_service = Service::Ec2Instances;
18111        app.service_selected = true;
18112        app.mode = Mode::ColumnSelector;
18113        app.column_selector_index = 0;
18114
18115        app.handle_action(Action::PageDown);
18116        assert_eq!(app.column_selector_index, 10);
18117
18118        app.handle_action(Action::PageDown);
18119        assert_eq!(app.column_selector_index, 20);
18120    }
18121
18122    #[test]
18123    fn test_column_selector_page_up_ec2() {
18124        let mut app = test_app();
18125        app.current_service = Service::Ec2Instances;
18126        app.service_selected = true;
18127        app.mode = Mode::ColumnSelector;
18128        app.column_selector_index = 30;
18129
18130        app.handle_action(Action::PageUp);
18131        assert_eq!(app.column_selector_index, 20);
18132
18133        app.handle_action(Action::PageUp);
18134        assert_eq!(app.column_selector_index, 10);
18135    }
18136
18137    #[test]
18138    fn test_ec2_state_filter_dropdown_focus() {
18139        let mut app = test_app();
18140        app.current_service = Service::Ec2Instances;
18141        app.service_selected = true;
18142        app.mode = Mode::FilterInput;
18143
18144        // Tab to state filter
18145        app.handle_action(Action::NextFilterFocus);
18146        assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
18147
18148        // Dropdown should show when focused (tested in render)
18149        // Verify we can cycle the filter
18150        app.handle_action(Action::ToggleFilterCheckbox);
18151        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
18152    }
18153
18154    #[test]
18155    fn test_column_selector_ctrl_d_scrolling() {
18156        let mut app = test_app();
18157        app.current_service = Service::LambdaFunctions;
18158        app.mode = Mode::ColumnSelector;
18159        app.column_selector_index = 0;
18160
18161        // PageDown from 0 by 10 lands on blank row (10), skips to 11
18162        app.handle_action(Action::PageDown);
18163        assert_eq!(app.column_selector_index, 11);
18164
18165        // Second PageDown should be capped at max
18166        let max = app.get_column_selector_max();
18167        app.handle_action(Action::PageDown);
18168        assert_eq!(app.column_selector_index, max);
18169    }
18170
18171    #[test]
18172    fn test_column_selector_ctrl_u_scrolling() {
18173        let mut app = test_app();
18174        app.current_service = Service::CloudFormationStacks;
18175        app.mode = Mode::ColumnSelector;
18176        app.column_selector_index = 25;
18177
18178        app.handle_action(Action::PageUp);
18179        assert_eq!(app.column_selector_index, 15);
18180
18181        app.handle_action(Action::PageUp);
18182        assert_eq!(app.column_selector_index, 5);
18183    }
18184
18185    #[test]
18186    fn test_prev_preferences_lambda() {
18187        let mut app = test_app();
18188        app.current_service = Service::LambdaFunctions;
18189        app.mode = Mode::ColumnSelector;
18190        let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
18191        app.column_selector_index = page_size_idx;
18192
18193        app.handle_action(Action::PrevPreferences);
18194        assert_eq!(app.column_selector_index, 0);
18195
18196        app.handle_action(Action::PrevPreferences);
18197        assert_eq!(app.column_selector_index, page_size_idx);
18198    }
18199
18200    #[test]
18201    fn test_prev_preferences_cloudformation() {
18202        let mut app = test_app();
18203        app.current_service = Service::CloudFormationStacks;
18204        app.mode = Mode::ColumnSelector;
18205        let page_size_idx = app.cfn_column_ids.len() + 2;
18206        app.column_selector_index = page_size_idx;
18207
18208        app.handle_action(Action::PrevPreferences);
18209        assert_eq!(app.column_selector_index, 0);
18210
18211        app.handle_action(Action::PrevPreferences);
18212        assert_eq!(app.column_selector_index, page_size_idx);
18213    }
18214
18215    #[test]
18216    fn test_prev_preferences_alarms() {
18217        let mut app = test_app();
18218        app.current_service = Service::CloudWatchAlarms;
18219        app.mode = Mode::ColumnSelector;
18220        app.column_selector_index = 28; // WrapLines
18221
18222        app.handle_action(Action::PrevPreferences);
18223        assert_eq!(app.column_selector_index, 22); // PageSize
18224
18225        app.handle_action(Action::PrevPreferences);
18226        assert_eq!(app.column_selector_index, 18); // ViewAs
18227
18228        app.handle_action(Action::PrevPreferences);
18229        assert_eq!(app.column_selector_index, 0); // Columns
18230
18231        app.handle_action(Action::PrevPreferences);
18232        assert_eq!(app.column_selector_index, 28); // Wrap to WrapLines
18233    }
18234
18235    #[test]
18236    fn test_ec2_page_size_in_preferences() {
18237        let mut app = test_app();
18238        app.current_service = Service::Ec2Instances;
18239        app.mode = Mode::ColumnSelector;
18240        app.ec2_state.table.page_size = PageSize::Fifty;
18241
18242        // Navigate to page size section
18243        let page_size_idx = app.ec2_column_ids.len() + 3; // First page size option (10)
18244        app.column_selector_index = page_size_idx;
18245        app.handle_action(Action::ToggleColumn);
18246
18247        assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
18248    }
18249
18250    #[test]
18251    fn test_ec2_next_preferences_with_page_size() {
18252        let mut app = test_app();
18253        app.current_service = Service::Ec2Instances;
18254        app.mode = Mode::ColumnSelector;
18255        app.column_selector_index = 0;
18256
18257        let page_size_idx = app.ec2_column_ids.len() + 2;
18258        app.handle_action(Action::NextPreferences);
18259        assert_eq!(app.column_selector_index, page_size_idx);
18260
18261        app.handle_action(Action::NextPreferences);
18262        assert_eq!(app.column_selector_index, 0);
18263    }
18264
18265    #[test]
18266    fn test_ec2_dropdown_next_item() {
18267        let mut app = test_app();
18268        app.current_service = Service::Ec2Instances;
18269        app.mode = Mode::FilterInput;
18270        app.ec2_state.input_focus = EC2_STATE_FILTER;
18271        app.ec2_state.state_filter = Ec2StateFilter::AllStates;
18272
18273        app.handle_action(Action::NextItem);
18274        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
18275
18276        app.handle_action(Action::NextItem);
18277        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
18278    }
18279
18280    #[test]
18281    fn test_ec2_dropdown_prev_item() {
18282        let mut app = test_app();
18283        app.current_service = Service::Ec2Instances;
18284        app.mode = Mode::FilterInput;
18285        app.ec2_state.input_focus = EC2_STATE_FILTER;
18286        app.ec2_state.state_filter = Ec2StateFilter::Stopped;
18287
18288        app.handle_action(Action::PrevItem);
18289        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
18290
18291        app.handle_action(Action::PrevItem);
18292        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
18293    }
18294
18295    #[test]
18296    fn test_ec2_dropdown_cycles_with_arrows() {
18297        let mut app = test_app();
18298        app.current_service = Service::Ec2Instances;
18299        app.mode = Mode::FilterInput;
18300        app.ec2_state.input_focus = EC2_STATE_FILTER;
18301        app.ec2_state.state_filter = Ec2StateFilter::Stopping;
18302
18303        // Next wraps to AllStates
18304        app.handle_action(Action::NextItem);
18305        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
18306
18307        // Prev wraps to Stopping
18308        app.handle_action(Action::PrevItem);
18309        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
18310    }
18311
18312    #[test]
18313    fn test_collapse_row_ec2_instances() {
18314        let mut app = test_app();
18315        app.current_service = Service::Ec2Instances;
18316        app.ec2_state.table.expanded_item = Some(0);
18317
18318        app.handle_action(Action::CollapseRow);
18319        assert_eq!(app.ec2_state.table.expanded_item, None);
18320    }
18321
18322    #[test]
18323    fn test_collapse_row_ec2_tags() {
18324        let mut app = test_app();
18325        app.current_service = Service::Ec2Instances;
18326        app.ec2_state.current_instance = Some("i-123".to_string());
18327        app.ec2_state.detail_tab = Ec2DetailTab::Tags;
18328        app.ec2_state.tags.expanded_item = Some(1);
18329
18330        app.handle_action(Action::CollapseRow);
18331        assert_eq!(app.ec2_state.tags.expanded_item, None);
18332    }
18333
18334    #[test]
18335    fn test_collapse_row_cloudwatch_log_groups() {
18336        let mut app = test_app();
18337        app.current_service = Service::CloudWatchLogGroups;
18338        app.log_groups_state.log_groups.expanded_item = Some(2);
18339
18340        app.handle_action(Action::CollapseRow);
18341        assert_eq!(app.log_groups_state.log_groups.expanded_item, None);
18342    }
18343
18344    #[test]
18345    fn test_collapse_row_cloudwatch_alarms() {
18346        let mut app = test_app();
18347        app.current_service = Service::CloudWatchAlarms;
18348        app.alarms_state.table.expanded_item = Some(0);
18349
18350        app.handle_action(Action::CollapseRow);
18351        assert_eq!(app.alarms_state.table.expanded_item, None);
18352    }
18353
18354    #[test]
18355    fn test_collapse_row_lambda_functions() {
18356        let mut app = test_app();
18357        app.current_service = Service::LambdaFunctions;
18358        app.lambda_state.table.expanded_item = Some(1);
18359
18360        app.handle_action(Action::CollapseRow);
18361        assert_eq!(app.lambda_state.table.expanded_item, None);
18362    }
18363
18364    #[test]
18365    fn test_collapse_row_cfn_stacks() {
18366        let mut app = test_app();
18367        app.current_service = Service::CloudFormationStacks;
18368        app.cfn_state.table.expanded_item = Some(0);
18369
18370        app.handle_action(Action::CollapseRow);
18371        assert_eq!(app.cfn_state.table.expanded_item, None);
18372    }
18373
18374    #[test]
18375    fn test_collapse_row_cfn_resources() {
18376        let mut app = test_app();
18377        app.current_service = Service::CloudFormationStacks;
18378        app.cfn_state.current_stack = Some("test-stack".to_string());
18379        app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Resources;
18380        app.cfn_state.resources.expanded_item = Some(2);
18381
18382        app.handle_action(Action::CollapseRow);
18383        assert_eq!(app.cfn_state.resources.expanded_item, None);
18384    }
18385
18386    #[test]
18387    fn test_collapse_row_iam_users() {
18388        let mut app = test_app();
18389        app.current_service = Service::IamUsers;
18390        app.iam_state.users.expanded_item = Some(1);
18391
18392        app.handle_action(Action::CollapseRow);
18393        assert_eq!(app.iam_state.users.expanded_item, None);
18394    }
18395
18396    #[test]
18397    fn test_collapse_row_does_nothing_when_not_expanded() {
18398        let mut app = test_app();
18399        app.current_service = Service::Ec2Instances;
18400        app.ec2_state.table.expanded_item = None;
18401
18402        app.handle_action(Action::CollapseRow);
18403        assert_eq!(app.ec2_state.table.expanded_item, None);
18404    }
18405
18406    #[test]
18407    fn test_s3_collapse_expanded_folder_moves_to_parent() {
18408        let mut app = test_app();
18409        app.current_service = Service::S3Buckets;
18410        app.service_selected = true;
18411        app.mode = Mode::Normal;
18412
18413        // Add bucket with folder
18414        app.s3_state.buckets.items = vec![S3Bucket {
18415            name: "bucket1".to_string(),
18416            region: "us-east-1".to_string(),
18417            creation_date: "2024-01-01T00:00:00Z".to_string(),
18418        }];
18419
18420        // Expand bucket
18421        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18422        app.s3_state.bucket_preview.insert(
18423            "bucket1".to_string(),
18424            vec![S3Object {
18425                key: "folder1/".to_string(),
18426                size: 0,
18427                last_modified: "2024-01-01T00:00:00Z".to_string(),
18428                is_prefix: true,
18429                storage_class: String::new(),
18430            }],
18431        );
18432
18433        // Expand folder
18434        app.s3_state
18435            .expanded_prefixes
18436            .insert("folder1/".to_string());
18437        app.s3_state.prefix_preview.insert(
18438            "folder1/".to_string(),
18439            vec![S3Object {
18440                key: "folder1/file.txt".to_string(),
18441                size: 0,
18442                last_modified: "2024-01-01T00:00:00Z".to_string(),
18443                is_prefix: false,
18444                storage_class: String::new(),
18445            }],
18446        );
18447
18448        // Select the expanded folder (row 1)
18449        app.s3_state.selected_row = 1;
18450
18451        // Press Left to collapse
18452        app.handle_action(Action::PrevPane);
18453
18454        // Folder should be collapsed
18455        assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
18456        // Selection should move to parent (bucket at row 0)
18457        assert_eq!(app.s3_state.selected_row, 0);
18458    }
18459
18460    #[test]
18461    fn test_s3_collapse_hierarchy_level_by_level() {
18462        let mut app = test_app();
18463        app.current_service = Service::S3Buckets;
18464        app.service_selected = true;
18465        app.mode = Mode::Normal;
18466
18467        // Add bucket with 3-level hierarchy
18468        app.s3_state.buckets.items = vec![S3Bucket {
18469            name: "bucket1".to_string(),
18470            region: "us-east-1".to_string(),
18471            creation_date: "2024-01-01T00:00:00Z".to_string(),
18472        }];
18473
18474        // Level 1: bucket -> level1/
18475        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18476        app.s3_state.bucket_preview.insert(
18477            "bucket1".to_string(),
18478            vec![S3Object {
18479                key: "level1/".to_string(),
18480                size: 0,
18481                last_modified: "2024-01-01T00:00:00Z".to_string(),
18482                is_prefix: true,
18483                storage_class: String::new(),
18484            }],
18485        );
18486
18487        // Level 2: level1/ -> level2/
18488        app.s3_state.expanded_prefixes.insert("level1/".to_string());
18489        app.s3_state.prefix_preview.insert(
18490            "level1/".to_string(),
18491            vec![S3Object {
18492                key: "level1/level2/".to_string(),
18493                size: 0,
18494                last_modified: "2024-01-01T00:00:00Z".to_string(),
18495                is_prefix: true,
18496                storage_class: String::new(),
18497            }],
18498        );
18499
18500        // Level 3: level2/ -> file
18501        app.s3_state
18502            .expanded_prefixes
18503            .insert("level1/level2/".to_string());
18504        app.s3_state.prefix_preview.insert(
18505            "level1/level2/".to_string(),
18506            vec![S3Object {
18507                key: "level1/level2/file.txt".to_string(),
18508                size: 100,
18509                last_modified: "2024-01-01T00:00:00Z".to_string(),
18510                is_prefix: false,
18511                storage_class: String::new(),
18512            }],
18513        );
18514
18515        // Select deepest level (row 3: file)
18516        app.s3_state.selected_row = 3;
18517
18518        // First Left: move to parent (level2/ at row 2)
18519        app.handle_action(Action::PrevPane);
18520        assert_eq!(app.s3_state.selected_row, 2);
18521
18522        // Second Left: collapse level2/ and move to parent (level1/ at row 1)
18523        app.handle_action(Action::PrevPane);
18524        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
18525        assert_eq!(app.s3_state.selected_row, 1);
18526
18527        // Third Left: collapse level1/ and move to parent (bucket at row 0)
18528        app.handle_action(Action::PrevPane);
18529        assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
18530        assert_eq!(app.s3_state.selected_row, 0);
18531
18532        // Fourth Left: collapse bucket (stays at row 0)
18533        app.handle_action(Action::PrevPane);
18534        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18535        assert_eq!(app.s3_state.selected_row, 0);
18536    }
18537
18538    #[test]
18539    fn test_ec2_instance_detail_tabs_no_preferences() {
18540        let mut app = test_app();
18541        app.current_service = Service::Ec2Instances;
18542        app.service_selected = true;
18543        app.ec2_state.table.expanded_item = Some(0);
18544        app.mode = Mode::Normal;
18545
18546        // Details tab should NOT allow preferences
18547        app.ec2_state.detail_tab = Ec2DetailTab::Details;
18548        app.handle_action(Action::OpenColumnSelector);
18549        assert_eq!(app.mode, Mode::Normal);
18550
18551        // StatusAndAlarms tab should NOT allow preferences
18552        app.ec2_state.detail_tab = Ec2DetailTab::StatusAndAlarms;
18553        app.handle_action(Action::OpenColumnSelector);
18554        assert_eq!(app.mode, Mode::Normal);
18555
18556        // Monitoring tab should NOT allow preferences
18557        app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
18558        app.handle_action(Action::OpenColumnSelector);
18559        assert_eq!(app.mode, Mode::Normal);
18560
18561        // Security tab should NOT allow preferences
18562        app.ec2_state.detail_tab = Ec2DetailTab::Security;
18563        app.handle_action(Action::OpenColumnSelector);
18564        assert_eq!(app.mode, Mode::Normal);
18565
18566        // Networking tab should NOT allow preferences
18567        app.ec2_state.detail_tab = Ec2DetailTab::Networking;
18568        app.handle_action(Action::OpenColumnSelector);
18569        assert_eq!(app.mode, Mode::Normal);
18570
18571        // Storage tab should NOT allow preferences
18572        app.ec2_state.detail_tab = Ec2DetailTab::Storage;
18573        app.handle_action(Action::OpenColumnSelector);
18574        assert_eq!(app.mode, Mode::Normal);
18575
18576        // Tags tab SHOULD allow preferences
18577        app.ec2_state.detail_tab = Ec2DetailTab::Tags;
18578        app.handle_action(Action::OpenColumnSelector);
18579        assert_eq!(app.mode, Mode::ColumnSelector);
18580    }
18581
18582    #[test]
18583    fn test_log_streams_filter_only_updates_when_focused() {
18584        let mut app = test_app();
18585        app.current_service = Service::CloudWatchLogGroups;
18586        app.service_selected = true;
18587        app.view_mode = ViewMode::Detail;
18588        app.mode = Mode::FilterInput;
18589        app.log_groups_state.stream_filter = "test".to_string();
18590
18591        // When filter is focused, typing should update filter
18592        app.log_groups_state.input_focus = InputFocus::Filter;
18593        app.handle_action(Action::FilterInput('x'));
18594        assert_eq!(app.log_groups_state.stream_filter, "testx");
18595
18596        // When pagination is focused, typing should NOT update filter
18597        app.log_groups_state.input_focus = InputFocus::Pagination;
18598        app.handle_action(Action::FilterInput('y'));
18599        assert_eq!(app.log_groups_state.stream_filter, "testx"); // unchanged
18600    }
18601
18602    #[test]
18603    fn test_log_streams_backspace_only_updates_when_focused() {
18604        let mut app = test_app();
18605        app.current_service = Service::CloudWatchLogGroups;
18606        app.service_selected = true;
18607        app.view_mode = ViewMode::Detail;
18608        app.mode = Mode::FilterInput;
18609        app.log_groups_state.stream_filter = "test".to_string();
18610
18611        // When filter is focused, backspace should update filter
18612        app.log_groups_state.input_focus = InputFocus::Filter;
18613        app.handle_action(Action::FilterBackspace);
18614        assert_eq!(app.log_groups_state.stream_filter, "tes");
18615
18616        // When pagination is focused, backspace should NOT update filter
18617        app.log_groups_state.input_focus = InputFocus::Pagination;
18618        app.handle_action(Action::FilterBackspace);
18619        assert_eq!(app.log_groups_state.stream_filter, "tes"); // unchanged
18620    }
18621
18622    #[test]
18623    fn test_log_groups_filter_only_updates_when_focused() {
18624        let mut app = test_app();
18625        app.current_service = Service::CloudWatchLogGroups;
18626        app.service_selected = true;
18627        app.view_mode = ViewMode::List;
18628        app.mode = Mode::FilterInput;
18629        app.log_groups_state.log_groups.filter = "test".to_string();
18630
18631        // When filter is focused, typing should update filter
18632        app.log_groups_state.input_focus = InputFocus::Filter;
18633        app.handle_action(Action::FilterInput('x'));
18634        assert_eq!(app.log_groups_state.log_groups.filter, "testx");
18635
18636        // When pagination is focused, typing should NOT update filter
18637        app.log_groups_state.input_focus = InputFocus::Pagination;
18638        app.handle_action(Action::FilterInput('y'));
18639        assert_eq!(app.log_groups_state.log_groups.filter, "testx"); // unchanged
18640    }
18641
18642    #[test]
18643    fn test_s3_bucket_collapse_nested_prefix_jumps_to_parent() {
18644        use S3Bucket;
18645        use S3Object;
18646
18647        let mut app = test_app();
18648        app.current_service = Service::S3Buckets;
18649        app.service_selected = true;
18650
18651        // Create bucket with nested prefixes
18652        app.s3_state.buckets.items = vec![S3Bucket {
18653            name: "test-bucket".to_string(),
18654            region: "us-east-1".to_string(),
18655            creation_date: String::new(),
18656        }];
18657
18658        // Expand bucket with folder1/
18659        app.s3_state
18660            .expanded_prefixes
18661            .insert("test-bucket".to_string());
18662        app.s3_state.bucket_preview.insert(
18663            "test-bucket".to_string(),
18664            vec![S3Object {
18665                key: "folder1/".to_string(),
18666                is_prefix: true,
18667                size: 0,
18668                last_modified: String::new(),
18669                storage_class: String::new(),
18670            }],
18671        );
18672
18673        // Expand folder1/ with folder2/
18674        app.s3_state
18675            .expanded_prefixes
18676            .insert("folder1/".to_string());
18677        app.s3_state.prefix_preview.insert(
18678            "folder1/".to_string(),
18679            vec![S3Object {
18680                key: "folder1/folder2/".to_string(),
18681                is_prefix: true,
18682                size: 0,
18683                last_modified: String::new(),
18684                storage_class: String::new(),
18685            }],
18686        );
18687
18688        // Select folder2/ (row 0: bucket, row 1: folder1, row 2: folder2)
18689        app.s3_state.selected_row = 2;
18690
18691        // Press left arrow - should collapse folder2 and jump to folder1
18692        app.handle_action(Action::CollapseRow);
18693
18694        // folder2 should be collapsed
18695        assert!(!app.s3_state.expanded_prefixes.contains("folder1/folder2/"));
18696        // Selection should move to folder1 (row 1)
18697        assert_eq!(app.s3_state.selected_row, 1);
18698    }
18699
18700    #[test]
18701    fn test_s3_bucket_collapse_expanded_folder_moves_to_parent() {
18702        use S3Bucket;
18703        use S3Object;
18704
18705        let mut app = test_app();
18706        app.current_service = Service::S3Buckets;
18707        app.service_selected = true;
18708
18709        // Create bucket with folder
18710        app.s3_state.buckets.items = vec![S3Bucket {
18711            name: "test-bucket".to_string(),
18712            region: "us-east-1".to_string(),
18713            creation_date: String::new(),
18714        }];
18715
18716        // Expand bucket with folder1/
18717        app.s3_state
18718            .expanded_prefixes
18719            .insert("test-bucket".to_string());
18720        app.s3_state.bucket_preview.insert(
18721            "test-bucket".to_string(),
18722            vec![S3Object {
18723                key: "folder1/".to_string(),
18724                is_prefix: true,
18725                size: 0,
18726                last_modified: String::new(),
18727                storage_class: String::new(),
18728            }],
18729        );
18730
18731        // Expand folder1/
18732        app.s3_state
18733            .expanded_prefixes
18734            .insert("folder1/".to_string());
18735        app.s3_state.prefix_preview.insert(
18736            "folder1/".to_string(),
18737            vec![S3Object {
18738                key: "folder1/file.txt".to_string(),
18739                is_prefix: false,
18740                size: 100,
18741                last_modified: String::new(),
18742                storage_class: String::new(),
18743            }],
18744        );
18745
18746        // Select folder1/ (row 0: bucket, row 1: folder1)
18747        app.s3_state.selected_row = 1;
18748
18749        // Press left arrow - should collapse folder1 and jump to bucket
18750        app.handle_action(Action::CollapseRow);
18751
18752        // folder1 should be collapsed
18753        assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
18754        // Selection should move to bucket (row 0)
18755        assert_eq!(app.s3_state.selected_row, 0);
18756    }
18757
18758    #[test]
18759    fn test_log_streams_pagination_limits_table_content() {
18760        let mut app = test_app();
18761        app.current_service = Service::CloudWatchLogGroups;
18762        app.service_selected = true;
18763        app.view_mode = ViewMode::Detail;
18764
18765        // Create 50 log streams
18766        app.log_groups_state.log_streams = (0..50)
18767            .map(|i| rusticity_core::LogStream {
18768                name: format!("stream-{}", i),
18769                creation_time: None,
18770                last_event_time: None,
18771            })
18772            .collect();
18773
18774        // Set page size to 10
18775        app.log_groups_state.stream_page_size = 10;
18776        app.log_groups_state.stream_current_page = 0;
18777
18778        // First page should show streams 0-9
18779        // (This is tested by rendering, but we can verify pagination logic)
18780        assert_eq!(app.log_groups_state.stream_page_size, 10);
18781        assert_eq!(app.log_groups_state.stream_current_page, 0);
18782
18783        // Navigate to page 2
18784        app.log_groups_state.stream_current_page = 1;
18785        assert_eq!(app.log_groups_state.stream_current_page, 1);
18786    }
18787
18788    #[test]
18789    fn test_log_streams_page_size_change_resets_page() {
18790        let mut app = test_app();
18791        app.current_service = Service::CloudWatchLogGroups;
18792        app.service_selected = true;
18793        app.view_mode = ViewMode::Detail;
18794        app.mode = Mode::ColumnSelector;
18795
18796        app.log_groups_state.stream_page_size = 10;
18797        app.log_groups_state.stream_current_page = 3;
18798
18799        // Change page size - should reset to page 0
18800        app.column_selector_index = app.cw_log_stream_column_ids.len() + 4; // 25 items
18801        app.handle_action(Action::ToggleColumn);
18802
18803        assert_eq!(app.log_groups_state.stream_page_size, 25);
18804        assert_eq!(app.log_groups_state.stream_current_page, 0);
18805    }
18806
18807    #[test]
18808    fn test_s3_objects_expanded_rows_stay_visible() {
18809        use S3Object;
18810
18811        let mut app = test_app();
18812        app.current_service = Service::S3Buckets;
18813        app.service_selected = true;
18814        app.mode = Mode::Normal;
18815        app.s3_state.current_bucket = Some("test-bucket".to_string());
18816
18817        // Create a folder with many nested items
18818        app.s3_state.objects = vec![S3Object {
18819            key: "folder1/".to_string(),
18820            is_prefix: true,
18821            size: 0,
18822            last_modified: String::new(),
18823            storage_class: String::new(),
18824        }];
18825
18826        // Expand folder1 with 20 files
18827        app.s3_state
18828            .expanded_prefixes
18829            .insert("folder1/".to_string());
18830        app.s3_state.prefix_preview.insert(
18831            "folder1/".to_string(),
18832            (0..20)
18833                .map(|i| S3Object {
18834                    key: format!("folder1/file{}.txt", i),
18835                    is_prefix: false,
18836                    size: 100,
18837                    last_modified: String::new(),
18838                    storage_class: String::new(),
18839                })
18840                .collect(),
18841        );
18842
18843        // Set viewport to show 10 rows
18844        app.s3_state.object_visible_rows.set(10);
18845        app.s3_state.object_scroll_offset = 0;
18846        app.s3_state.selected_object = 0; // folder1
18847
18848        // Navigate down through all items
18849        for i in 1..=20 {
18850            app.handle_action(Action::NextItem);
18851            assert_eq!(app.s3_state.selected_object, i);
18852
18853            // Check that selection is within visible range
18854            let visible_start = app.s3_state.object_scroll_offset;
18855            let visible_end = visible_start + app.s3_state.object_visible_rows.get();
18856            assert!(
18857                app.s3_state.selected_object >= visible_start
18858                    && app.s3_state.selected_object < visible_end,
18859                "Selection {} should be visible in range [{}, {})",
18860                app.s3_state.selected_object,
18861                visible_start,
18862                visible_end
18863            );
18864        }
18865    }
18866
18867    #[test]
18868    fn test_s3_bucket_error_rows_counted_in_total() {
18869        use S3Bucket;
18870
18871        let mut app = test_app();
18872        app.current_service = Service::S3Buckets;
18873        app.service_selected = true;
18874
18875        // Create buckets
18876        app.s3_state.buckets.items = vec![
18877            S3Bucket {
18878                name: "bucket1".to_string(),
18879                region: "us-east-1".to_string(),
18880                creation_date: String::new(),
18881            },
18882            S3Bucket {
18883                name: "bucket2".to_string(),
18884                region: "us-east-1".to_string(),
18885                creation_date: String::new(),
18886            },
18887        ];
18888
18889        // Expand bucket1 with error (long error message that will wrap)
18890        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18891        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();
18892        app.s3_state
18893            .bucket_errors
18894            .insert("bucket1".to_string(), long_error.clone());
18895
18896        // Calculate total rows
18897        let total = app.calculate_total_bucket_rows();
18898
18899        // Should be: 2 buckets + error rows (long_error.len() / 120 rounded up)
18900        let error_rows = long_error.len().div_ceil(120);
18901        assert_eq!(total, 2 + error_rows);
18902    }
18903
18904    #[test]
18905    fn test_s3_bucket_with_error_can_be_collapsed() {
18906        use S3Bucket;
18907
18908        let mut app = test_app();
18909        app.current_service = Service::S3Buckets;
18910        app.service_selected = true;
18911        app.mode = Mode::Normal;
18912
18913        // Create bucket
18914        app.s3_state.buckets.items = vec![S3Bucket {
18915            name: "bucket1".to_string(),
18916            region: "us-east-1".to_string(),
18917            creation_date: String::new(),
18918        }];
18919
18920        // Expand bucket with error
18921        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18922        let error = "service error: PermanentRedirect".to_string();
18923        app.s3_state
18924            .bucket_errors
18925            .insert("bucket1".to_string(), error);
18926
18927        // Select the bucket row (row 0) - error rows are not selectable
18928        app.s3_state.selected_row = 0;
18929
18930        // Press left arrow to collapse
18931        app.handle_action(Action::CollapseRow);
18932
18933        // Bucket should be collapsed
18934        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18935        // Selection should stay on bucket
18936        assert_eq!(app.s3_state.selected_row, 0);
18937    }
18938
18939    #[test]
18940    fn test_s3_bucket_collapse_on_bucket_row() {
18941        use S3Bucket;
18942
18943        let mut app = test_app();
18944        app.current_service = Service::S3Buckets;
18945        app.service_selected = true;
18946        app.mode = Mode::Normal;
18947
18948        // Create bucket
18949        app.s3_state.buckets.items = vec![S3Bucket {
18950            name: "bucket1".to_string(),
18951            region: "us-east-1".to_string(),
18952            creation_date: String::new(),
18953        }];
18954
18955        // Expand bucket with error
18956        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
18957        let error = "service error: PermanentRedirect".to_string();
18958        app.s3_state
18959            .bucket_errors
18960            .insert("bucket1".to_string(), error);
18961
18962        // Select the bucket row itself (row 0)
18963        app.s3_state.selected_row = 0;
18964
18965        // Press left arrow to collapse
18966        app.handle_action(Action::CollapseRow);
18967
18968        // Bucket should be collapsed
18969        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
18970        // Selection should stay on bucket
18971        assert_eq!(app.s3_state.selected_row, 0);
18972    }
18973
18974    #[test]
18975    fn test_s3_bucket_collapse_adjusts_scroll_offset() {
18976        use S3Bucket;
18977
18978        let mut app = test_app();
18979        app.current_service = Service::S3Buckets;
18980        app.service_selected = true;
18981        app.mode = Mode::Normal;
18982
18983        // Create multiple buckets
18984        app.s3_state.buckets.items = (0..20)
18985            .map(|i| S3Bucket {
18986                name: format!("bucket{}", i),
18987                region: "us-east-1".to_string(),
18988                creation_date: String::new(),
18989            })
18990            .collect();
18991
18992        // Expand bucket 10 with a long error
18993        app.s3_state
18994            .expanded_prefixes
18995            .insert("bucket10".to_string());
18996        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();
18997        app.s3_state
18998            .bucket_errors
18999            .insert("bucket10".to_string(), long_error.clone());
19000
19001        // Set viewport to 10 rows and scroll so bucket10 is at top
19002        app.s3_state.bucket_visible_rows.set(10);
19003        app.s3_state.bucket_scroll_offset = 10; // bucket10 is at row 10
19004
19005        // Select bucket10 (row 10) - error rows are not selectable
19006        app.s3_state.selected_row = 10;
19007
19008        // Press left arrow to collapse
19009        app.handle_action(Action::CollapseRow);
19010
19011        // Bucket should be collapsed
19012        assert!(!app.s3_state.expanded_prefixes.contains("bucket10"));
19013        // Selection should stay on bucket10 (row 10)
19014        assert_eq!(app.s3_state.selected_row, 10);
19015        // Scroll offset should be adjusted to show bucket10
19016        assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
19017        assert!(
19018            app.s3_state.selected_row
19019                < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
19020        );
19021    }
19022
19023    #[test]
19024    fn test_s3_collapse_second_to_last_bucket_with_last_having_error() {
19025        use S3Bucket;
19026
19027        let mut app = test_app();
19028        app.current_service = Service::S3Buckets;
19029        app.service_selected = true;
19030        app.mode = Mode::Normal;
19031
19032        // Create 3 buckets
19033        app.s3_state.buckets.items = vec![
19034            S3Bucket {
19035                name: "bucket1".to_string(),
19036                region: "us-east-1".to_string(),
19037                creation_date: String::new(),
19038            },
19039            S3Bucket {
19040                name: "bucket2".to_string(),
19041                region: "us-east-1".to_string(),
19042                creation_date: String::new(),
19043            },
19044            S3Bucket {
19045                name: "bucket3".to_string(),
19046                region: "us-east-1".to_string(),
19047                creation_date: String::new(),
19048            },
19049        ];
19050
19051        // Expand bucket2 (second to last) with preview
19052        app.s3_state.expanded_prefixes.insert("bucket2".to_string());
19053        app.s3_state.bucket_preview.insert(
19054            "bucket2".to_string(),
19055            vec![
19056                S3Object {
19057                    key: "folder1/".to_string(),
19058                    is_prefix: true,
19059                    size: 0,
19060                    last_modified: String::new(),
19061                    storage_class: String::new(),
19062                },
19063                S3Object {
19064                    key: "file1.txt".to_string(),
19065                    is_prefix: false,
19066                    size: 100,
19067                    last_modified: String::new(),
19068                    storage_class: String::new(),
19069                },
19070            ],
19071        );
19072
19073        // Expand bucket3 (last) with error
19074        app.s3_state.expanded_prefixes.insert("bucket3".to_string());
19075        let error = "service error: PermanentRedirect".to_string();
19076        app.s3_state
19077            .bucket_errors
19078            .insert("bucket3".to_string(), error);
19079
19080        // Set viewport
19081        app.s3_state.bucket_visible_rows.set(10);
19082        app.s3_state.bucket_scroll_offset = 0;
19083
19084        // Select last item in bucket2 (row 3: bucket1, bucket2, folder1, file1)
19085        app.s3_state.selected_row = 3;
19086
19087        // Collapse - should move to parent (bucket2)
19088        app.handle_action(Action::CollapseRow);
19089
19090        // bucket2 should still be expanded (we only moved to parent, didn't collapse)
19091        assert!(app.s3_state.expanded_prefixes.contains("bucket2"));
19092        // Selection should move to bucket2
19093        assert_eq!(app.s3_state.selected_row, 1);
19094        // Selection should be visible
19095        assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
19096        assert!(
19097            app.s3_state.selected_row
19098                < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
19099        );
19100    }
19101
19102    #[test]
19103    fn test_s3_collapse_bucket_with_error() {
19104        use S3Bucket;
19105
19106        let mut app = test_app();
19107        app.current_service = Service::S3Buckets;
19108        app.service_selected = true;
19109        app.mode = Mode::Normal;
19110
19111        app.s3_state.buckets.items = vec![
19112            S3Bucket {
19113                name: "bucket1".to_string(),
19114                region: "us-east-1".to_string(),
19115                creation_date: String::new(),
19116            },
19117            S3Bucket {
19118                name: "bucket2".to_string(),
19119                region: "us-east-1".to_string(),
19120                creation_date: String::new(),
19121            },
19122        ];
19123
19124        let error = "service error: unhandled error (PermanentRedirect)".to_string();
19125        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19126        app.s3_state
19127            .bucket_errors
19128            .insert("bucket1".to_string(), error);
19129
19130        app.s3_state.bucket_visible_rows.set(10);
19131        app.s3_state.bucket_scroll_offset = 0;
19132
19133        // Select bucket1 (row 0) - error rows are not selectable
19134        app.s3_state.selected_row = 0;
19135
19136        // Collapse
19137        app.handle_action(Action::CollapseRow);
19138
19139        // bucket1 should be collapsed
19140        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
19141        assert_eq!(app.s3_state.selected_row, 0);
19142    }
19143
19144    #[test]
19145    fn test_s3_collapse_row_with_multiple_error_buckets() {
19146        use S3Bucket;
19147
19148        let mut app = test_app();
19149        app.current_service = Service::S3Buckets;
19150        app.service_selected = true;
19151        app.mode = Mode::Normal;
19152
19153        app.s3_state.buckets.items = vec![
19154            S3Bucket {
19155                name: "bucket1".to_string(),
19156                region: "us-east-1".to_string(),
19157                creation_date: String::new(),
19158            },
19159            S3Bucket {
19160                name: "bucket2".to_string(),
19161                region: "us-east-1".to_string(),
19162                creation_date: String::new(),
19163            },
19164            S3Bucket {
19165                name: "bucket3".to_string(),
19166                region: "us-east-1".to_string(),
19167                creation_date: String::new(),
19168            },
19169        ];
19170
19171        let error = "service error: unhandled error (PermanentRedirect)".to_string();
19172
19173        // Expand bucket1 with error
19174        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19175        app.s3_state
19176            .bucket_errors
19177            .insert("bucket1".to_string(), error.clone());
19178
19179        // Expand bucket3 with error
19180        app.s3_state.expanded_prefixes.insert("bucket3".to_string());
19181        app.s3_state
19182            .bucket_errors
19183            .insert("bucket3".to_string(), error.clone());
19184
19185        app.s3_state.bucket_visible_rows.set(30);
19186        app.s3_state.bucket_scroll_offset = 0;
19187
19188        // Row 0: bucket1 (expanded with error - error rows not selectable)
19189        // Row 1: bucket2
19190        // Row 2: bucket3 (expanded with error - error rows not selectable)
19191        // Select bucket3
19192        app.s3_state.selected_row = 2;
19193
19194        app.handle_action(Action::CollapseRow);
19195
19196        // bucket3 should be collapsed, NOT bucket1
19197        assert!(
19198            !app.s3_state.expanded_prefixes.contains("bucket3"),
19199            "bucket3 should be collapsed"
19200        );
19201        assert!(
19202            app.s3_state.expanded_prefixes.contains("bucket1"),
19203            "bucket1 should still be expanded"
19204        );
19205        assert_eq!(app.s3_state.selected_row, 2);
19206    }
19207
19208    #[test]
19209    fn test_s3_collapse_row_nested_only_collapses_one_level() {
19210        use S3Bucket;
19211
19212        let mut app = test_app();
19213        app.current_service = Service::S3Buckets;
19214        app.service_selected = true;
19215        app.mode = Mode::Normal;
19216
19217        app.s3_state.buckets.items = vec![S3Bucket {
19218            name: "bucket1".to_string(),
19219            region: "us-east-1".to_string(),
19220            creation_date: String::new(),
19221        }];
19222
19223        // Level 1: bucket -> level1/
19224        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19225        app.s3_state.bucket_preview.insert(
19226            "bucket1".to_string(),
19227            vec![S3Object {
19228                key: "level1/".to_string(),
19229                size: 0,
19230                last_modified: "2024-01-01T00:00:00Z".to_string(),
19231                is_prefix: true,
19232                storage_class: String::new(),
19233            }],
19234        );
19235
19236        // Level 2: level1/ -> level2/
19237        app.s3_state.expanded_prefixes.insert("level1/".to_string());
19238        app.s3_state.prefix_preview.insert(
19239            "level1/".to_string(),
19240            vec![S3Object {
19241                key: "level1/level2/".to_string(),
19242                size: 0,
19243                last_modified: "2024-01-01T00:00:00Z".to_string(),
19244                is_prefix: true,
19245                storage_class: String::new(),
19246            }],
19247        );
19248
19249        // Level 3: level2/ -> file
19250        app.s3_state
19251            .expanded_prefixes
19252            .insert("level1/level2/".to_string());
19253        app.s3_state.prefix_preview.insert(
19254            "level1/level2/".to_string(),
19255            vec![S3Object {
19256                key: "level1/level2/file.txt".to_string(),
19257                size: 100,
19258                last_modified: "2024-01-01T00:00:00Z".to_string(),
19259                is_prefix: false,
19260                storage_class: String::new(),
19261            }],
19262        );
19263
19264        app.s3_state.bucket_visible_rows.set(10);
19265
19266        // Select level2/ (row 2: bucket, level1, level2)
19267        app.s3_state.selected_row = 2;
19268
19269        // Collapse - should only collapse level2/, not the entire bucket
19270        app.handle_action(Action::CollapseRow);
19271
19272        // level2/ should be collapsed
19273        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
19274        // level1/ should still be expanded
19275        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
19276        // bucket should still be expanded
19277        assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
19278        // Selection should move to parent (level1/ at row 1)
19279        assert_eq!(app.s3_state.selected_row, 1);
19280    }
19281
19282    #[test]
19283    fn test_s3_collapse_row_deeply_nested_file() {
19284        use S3Bucket;
19285
19286        let mut app = test_app();
19287        app.current_service = Service::S3Buckets;
19288        app.service_selected = true;
19289        app.mode = Mode::Normal;
19290
19291        app.s3_state.buckets.items = vec![S3Bucket {
19292            name: "bucket1".to_string(),
19293            region: "us-east-1".to_string(),
19294            creation_date: String::new(),
19295        }];
19296
19297        // Level 1: bucket -> level1/
19298        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
19299        app.s3_state.bucket_preview.insert(
19300            "bucket1".to_string(),
19301            vec![S3Object {
19302                key: "level1/".to_string(),
19303                size: 0,
19304                last_modified: "2024-01-01T00:00:00Z".to_string(),
19305                is_prefix: true,
19306                storage_class: String::new(),
19307            }],
19308        );
19309
19310        // Level 2: level1/ -> level2/
19311        app.s3_state.expanded_prefixes.insert("level1/".to_string());
19312        app.s3_state.prefix_preview.insert(
19313            "level1/".to_string(),
19314            vec![S3Object {
19315                key: "level1/level2/".to_string(),
19316                size: 0,
19317                last_modified: "2024-01-01T00:00:00Z".to_string(),
19318                is_prefix: true,
19319                storage_class: String::new(),
19320            }],
19321        );
19322
19323        // Level 3: level2/ -> file
19324        app.s3_state
19325            .expanded_prefixes
19326            .insert("level1/level2/".to_string());
19327        app.s3_state.prefix_preview.insert(
19328            "level1/level2/".to_string(),
19329            vec![S3Object {
19330                key: "level1/level2/file.txt".to_string(),
19331                size: 100,
19332                last_modified: "2024-01-01T00:00:00Z".to_string(),
19333                is_prefix: false,
19334                storage_class: String::new(),
19335            }],
19336        );
19337
19338        app.s3_state.bucket_visible_rows.set(10);
19339
19340        // Select file (row 3: bucket, level1, level2, file)
19341        app.s3_state.selected_row = 3;
19342
19343        // Collapse - should move to parent (level2/)
19344        app.handle_action(Action::CollapseRow);
19345
19346        // All levels should still be expanded
19347        assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
19348        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
19349        assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
19350        // Selection should move to parent (level2/ at row 2)
19351        assert_eq!(app.s3_state.selected_row, 2);
19352    }
19353
19354    #[test]
19355    fn test_s3_bucket_pagination_adjusts_scroll() {
19356        use S3Bucket;
19357
19358        let mut app = test_app();
19359        app.current_service = Service::S3Buckets;
19360        app.service_selected = true;
19361        app.mode = Mode::Normal;
19362
19363        // Create 150 buckets (3 pages with page size 50)
19364        app.s3_state.buckets.items = (0..150)
19365            .map(|i| S3Bucket {
19366                name: format!("bucket{:03}", i),
19367                region: "us-east-1".to_string(),
19368                creation_date: String::new(),
19369            })
19370            .collect();
19371
19372        app.s3_state.bucket_visible_rows.set(20);
19373        app.s3_state.selected_row = 0;
19374        app.s3_state.bucket_scroll_offset = 0;
19375
19376        // Go to page 2 (should select row 50 and scroll to show it)
19377        app.go_to_page(2);
19378
19379        assert_eq!(app.s3_state.selected_row, 50);
19380        // Scroll offset should be adjusted to show page 2
19381        assert_eq!(app.s3_state.bucket_scroll_offset, 50);
19382
19383        // Go to page 3 (should select row 100 and scroll to show it)
19384        app.go_to_page(3);
19385
19386        assert_eq!(app.s3_state.selected_row, 100);
19387        assert_eq!(app.s3_state.bucket_scroll_offset, 100);
19388
19389        // Go to page 1 (should select row 0 and scroll to top)
19390        app.go_to_page(1);
19391
19392        assert_eq!(app.s3_state.selected_row, 0);
19393        assert_eq!(app.s3_state.bucket_scroll_offset, 0);
19394    }
19395
19396    #[test]
19397    fn test_s3_bucket_pagination_uses_page_size() {
19398        use S3Bucket;
19399
19400        let mut app = test_app();
19401        app.current_service = Service::S3Buckets;
19402        app.service_selected = true;
19403        app.mode = Mode::Normal;
19404
19405        // Create 100 buckets
19406        app.s3_state.buckets.items = (0..100)
19407            .map(|i| S3Bucket {
19408                name: format!("bucket{:03}", i),
19409                region: "us-east-1".to_string(),
19410                creation_date: String::new(),
19411            })
19412            .collect();
19413
19414        app.s3_state.bucket_visible_rows.set(20);
19415        app.s3_state.selected_row = 0;
19416
19417        // Default page size is 50
19418        assert_eq!(app.s3_state.buckets.page_size.value(), 50);
19419
19420        // Go to page 2 with default page size (50)
19421        app.go_to_page(2);
19422        assert_eq!(app.s3_state.selected_row, 50);
19423        assert_eq!(app.s3_state.bucket_scroll_offset, 50);
19424
19425        // Change page size to 25
19426        app.s3_state.buckets.page_size = crate::common::PageSize::TwentyFive;
19427        assert_eq!(app.s3_state.buckets.page_size.value(), 25);
19428
19429        // Go to page 2 with new page size (25)
19430        app.go_to_page(2);
19431        assert_eq!(app.s3_state.selected_row, 25);
19432        assert_eq!(app.s3_state.bucket_scroll_offset, 25);
19433    }
19434
19435    #[test]
19436    fn test_s3_bucket_page_size_limits_visible_rows() {
19437        use S3Bucket;
19438
19439        let mut app = test_app();
19440        app.current_service = Service::S3Buckets;
19441        app.service_selected = true;
19442        app.mode = Mode::Normal;
19443
19444        // Create 100 buckets
19445        app.s3_state.buckets.items = (0..100)
19446            .map(|i| S3Bucket {
19447                name: format!("bucket{:03}", i),
19448                region: "us-east-1".to_string(),
19449                creation_date: String::new(),
19450            })
19451            .collect();
19452
19453        // Set page size to 10
19454        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19455        assert_eq!(app.s3_state.buckets.page_size.value(), 10);
19456
19457        // Calculate total rows - should only count buckets on current page
19458        let total_rows = app.calculate_total_bucket_rows();
19459        // With 100 buckets and page size 10, we should see 10 buckets per page
19460        // But calculate_total_bucket_rows returns ALL rows, not just current page
19461        // This is the issue - we need to paginate the display
19462        assert!(total_rows >= 10, "Should have at least 10 rows");
19463    }
19464
19465    #[test]
19466    fn test_s3_bucket_tab_cycling_in_filter() {
19467        use crate::common::InputFocus;
19468
19469        let mut app = test_app();
19470        app.current_service = Service::S3Buckets;
19471        app.mode = Mode::FilterInput;
19472
19473        // Start at Filter
19474        assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
19475
19476        // Tab to Pagination
19477        app.handle_action(Action::NextFilterFocus);
19478        assert_eq!(app.s3_state.input_focus, InputFocus::Pagination);
19479
19480        // Tab back to Filter
19481        app.handle_action(Action::NextFilterFocus);
19482        assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
19483    }
19484
19485    #[test]
19486    fn test_s3_bucket_pagination_navigation_with_arrows() {
19487        use S3Bucket;
19488
19489        let mut app = test_app();
19490        app.current_service = Service::S3Buckets;
19491        app.mode = Mode::FilterInput;
19492
19493        // Create 100 buckets
19494        app.s3_state.buckets.items = (0..100)
19495            .map(|i| S3Bucket {
19496                name: format!("bucket{:03}", i),
19497                region: "us-east-1".to_string(),
19498                creation_date: String::new(),
19499            })
19500            .collect();
19501
19502        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19503        app.s3_state.selected_row = 0;
19504
19505        // Focus pagination
19506        app.s3_state.input_focus = crate::common::InputFocus::Pagination;
19507
19508        // Right arrow should go to next page (row 10)
19509        app.handle_action(Action::NextItem);
19510        assert_eq!(app.s3_state.selected_row, 10);
19511
19512        // Right arrow again (row 20)
19513        app.handle_action(Action::NextItem);
19514        assert_eq!(app.s3_state.selected_row, 20);
19515
19516        // Left arrow should go back (row 10)
19517        app.handle_action(Action::PrevItem);
19518        assert_eq!(app.s3_state.selected_row, 10);
19519    }
19520
19521    #[test]
19522    fn test_s3_bucket_go_to_page_shows_correct_buckets() {
19523        use S3Bucket;
19524
19525        let mut app = test_app();
19526        app.current_service = Service::S3Buckets;
19527        app.service_selected = true;
19528        app.mode = Mode::Normal;
19529
19530        // Create 100 buckets
19531        app.s3_state.buckets.items = (0..100)
19532            .map(|i| S3Bucket {
19533                name: format!("bucket{:03}", i),
19534                region: "us-east-1".to_string(),
19535                creation_date: String::new(),
19536            })
19537            .collect();
19538
19539        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19540
19541        // Go to page 2 (should show buckets 10-19)
19542        app.go_to_page(2);
19543        assert_eq!(app.s3_state.selected_row, 10);
19544
19545        // Go to page 5 (should show buckets 40-49)
19546        app.go_to_page(5);
19547        assert_eq!(app.s3_state.selected_row, 40);
19548    }
19549
19550    #[test]
19551    fn test_s3_bucket_left_right_arrows_change_pages() {
19552        use S3Bucket;
19553
19554        let mut app = test_app();
19555        app.current_service = Service::S3Buckets;
19556        app.mode = Mode::FilterInput;
19557
19558        // Create 100 buckets
19559        app.s3_state.buckets.items = (0..100)
19560            .map(|i| S3Bucket {
19561                name: format!("bucket{:03}", i),
19562                region: "us-east-1".to_string(),
19563                creation_date: String::new(),
19564            })
19565            .collect();
19566
19567        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
19568        app.s3_state.selected_row = 0;
19569        app.s3_state.input_focus = crate::common::InputFocus::Pagination;
19570
19571        // Right arrow (PageDown) should go to next page
19572        app.handle_action(Action::PageDown);
19573        assert_eq!(app.s3_state.selected_row, 10);
19574
19575        // Right arrow again
19576        app.handle_action(Action::PageDown);
19577        assert_eq!(app.s3_state.selected_row, 20);
19578
19579        // Left arrow (PageUp) should go back
19580        app.handle_action(Action::PageUp);
19581        assert_eq!(app.s3_state.selected_row, 10);
19582    }
19583
19584    #[test]
19585    fn test_s3_bucket_preview_uses_bucket_region_not_config_region() {
19586        // Regression: load_bucket_preview was falling back to self.config.region when
19587        // the bucket's stored region was empty (which it always is from list_buckets).
19588        // This caused IllegalLocationConstraintException for cross-region buckets.
19589        // The fix: resolve the actual region via get_bucket_location when region is empty.
19590        //
19591        // This test verifies the region stored in the bucket is used when available,
19592        // not the app's configured region.
19593        use S3Bucket;
19594        let mut app = test_app();
19595        app.config.region = "ap-southeast-3".to_string(); // user's configured region
19596
19597        // Bucket is actually in us-east-1 — different from app region
19598        app.s3_state.buckets.items = vec![S3Bucket {
19599            name: "my-bucket".to_string(),
19600            region: "us-east-1".to_string(), // correct bucket region stored
19601            creation_date: String::new(),
19602        }];
19603
19604        // The stored region must take precedence over config.region
19605        let bucket = app
19606            .s3_state
19607            .buckets
19608            .items
19609            .iter()
19610            .find(|b| b.name == "my-bucket")
19611            .unwrap();
19612        let effective_region = if bucket.region.is_empty() {
19613            app.config.region.as_str()
19614        } else {
19615            bucket.region.as_str()
19616        };
19617        assert_eq!(
19618            effective_region, "us-east-1",
19619            "load_bucket_preview must use bucket's region, not app config region"
19620        );
19621        assert_ne!(
19622            effective_region, "ap-southeast-3",
19623            "Must NOT fall back to configured region for cross-region buckets"
19624        );
19625    }
19626
19627    #[test]
19628    fn test_s3_scroll_follows_selection_after_bucket_expand() {
19629        use S3Bucket;
19630        use S3Object;
19631        // Regression: expanding a bucket below the visible area didn't update bucket_scroll_offset,
19632        // so the selection (now pointing to the first child row) left the visible area.
19633        let mut app = test_app();
19634        app.current_service = Service::S3Buckets;
19635        app.service_selected = true;
19636        app.mode = Mode::Normal;
19637
19638        // 5 buckets, visible area is 3 rows (bucket_visible_rows = 3)
19639        app.s3_state.buckets.items = (0..5)
19640            .map(|i| S3Bucket {
19641                name: format!("bucket{}", i),
19642                region: "us-east-1".to_string(),
19643                creation_date: String::new(),
19644            })
19645            .collect();
19646
19647        app.s3_state.bucket_visible_rows = std::cell::Cell::new(3);
19648
19649        // Select row 2 (third bucket), scroll offset = 0 → row 2 is visible
19650        app.s3_state.selected_row = 2;
19651        app.s3_state.bucket_scroll_offset = 0;
19652
19653        // Expand bucket2 — it gets 2 children, selected_row moves to 3
19654        app.s3_state.bucket_preview.insert(
19655            "bucket2".to_string(),
19656            vec![
19657                S3Object {
19658                    key: "file1.txt".to_string(),
19659                    is_prefix: false,
19660                    size: 100,
19661                    last_modified: String::new(),
19662                    storage_class: String::new(),
19663                },
19664                S3Object {
19665                    key: "file2.txt".to_string(),
19666                    is_prefix: false,
19667                    size: 200,
19668                    last_modified: String::new(),
19669                    storage_class: String::new(),
19670                },
19671            ],
19672        );
19673
19674        // Simulate expand: selected_row moves to first child (row 3)
19675        // After expand, row 3 must be within [scroll_offset, scroll_offset + visible_rows)
19676        // i.e. bucket_scroll_offset must be adjusted so row 3 is visible
19677        app.handle_action(Action::CollapseRow); // first collapse to reset
19678        app.handle_action(Action::NextPane); // expand
19679
19680        // selected_row should now be 3 (first child), scroll_offset must keep it visible
19681        let visible = app.s3_state.bucket_visible_rows.get();
19682        let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
19683            && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
19684        assert!(
19685            in_view,
19686            "selected_row {} must be visible in [{}, {})",
19687            app.s3_state.selected_row,
19688            app.s3_state.bucket_scroll_offset,
19689            app.s3_state.bucket_scroll_offset + visible
19690        );
19691    }
19692
19693    #[test]
19694    fn test_s3_expand_without_preview_does_not_move_selection() {
19695        // Regression: when a bucket preview is not yet loaded (still loading),
19696        // right arrow must NOT move selection to "first child" (row_idx + 1).
19697        // Selection must stay on the bucket row until preview arrives.
19698        use S3Bucket;
19699        let mut app = test_app();
19700        app.current_service = Service::S3Buckets;
19701        app.service_selected = true;
19702        app.mode = Mode::Normal;
19703
19704        app.s3_state.buckets.items = vec![
19705            S3Bucket {
19706                name: "bucket0".to_string(),
19707                region: "us-east-1".to_string(),
19708                creation_date: String::new(),
19709            },
19710            S3Bucket {
19711                name: "bucket1".to_string(),
19712                region: "us-east-1".to_string(),
19713                creation_date: String::new(),
19714            },
19715        ];
19716        app.s3_state.selected_row = 0; // on bucket0
19717                                       // No preview loaded yet for bucket0
19718
19719        app.handle_action(Action::NextPane); // right arrow — expand, triggers loading
19720
19721        // Must stay on row 0 until preview arrives
19722        assert_eq!(
19723            app.s3_state.selected_row, 0,
19724            "Selection must not move before preview is loaded"
19725        );
19726        // Bucket must be marked as expanded and loading
19727        assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
19728        assert!(app.s3_state.buckets.loading);
19729    }
19730
19731    #[test]
19732    fn test_s3_expand_with_preview_moves_to_first_child() {
19733        // After preview is loaded, right arrow must move selection to first child immediately.
19734        use crate::s3::Object as S3Object;
19735        use S3Bucket;
19736        let mut app = test_app();
19737        app.current_service = Service::S3Buckets;
19738        app.service_selected = true;
19739        app.mode = Mode::Normal;
19740
19741        app.s3_state.buckets.items = vec![
19742            S3Bucket {
19743                name: "bucket0".to_string(),
19744                region: "us-east-1".to_string(),
19745                creation_date: String::new(),
19746            },
19747            S3Bucket {
19748                name: "bucket1".to_string(),
19749                region: "us-east-1".to_string(),
19750                creation_date: String::new(),
19751            },
19752        ];
19753        app.s3_state.selected_row = 0;
19754        app.s3_state.bucket_preview.insert(
19755            "bucket0".to_string(),
19756            vec![S3Object {
19757                key: "file.txt".to_string(),
19758                is_prefix: false,
19759                size: 0,
19760                last_modified: String::new(),
19761                storage_class: String::new(),
19762            }],
19763        );
19764        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19765
19766        // First right arrow: expands and immediately moves to first child (preview already loaded)
19767        app.handle_action(Action::NextPane);
19768        assert_eq!(app.s3_state.selected_row, 1, "Should move to first child");
19769    }
19770
19771    #[test]
19772    fn test_s3_expand_then_load_then_right_goes_to_first_child_not_sibling() {
19773        // Repro: right arrow → loading starts (selection stays on bucket) →
19774        // preview arrives → right arrow again → must go to first child (row 1),
19775        // NOT to next sibling bucket (row 2 if no children counted, or higher).
19776        use crate::s3::Object as S3Object;
19777        use S3Bucket;
19778        let mut app = test_app();
19779        app.current_service = Service::S3Buckets;
19780        app.service_selected = true;
19781        app.mode = Mode::Normal;
19782        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19783
19784        app.s3_state.buckets.items = vec![
19785            S3Bucket {
19786                name: "bucket0".to_string(),
19787                region: "us-east-1".to_string(),
19788                creation_date: String::new(),
19789            },
19790            S3Bucket {
19791                name: "bucket1".to_string(),
19792                region: "us-east-1".to_string(),
19793                creation_date: String::new(),
19794            },
19795        ];
19796        app.s3_state.selected_row = 0;
19797
19798        // Step 1: right arrow — no preview yet, expands + loading
19799        app.handle_action(Action::NextPane);
19800        assert_eq!(
19801            app.s3_state.selected_row, 0,
19802            "Must stay on bucket0 while loading"
19803        );
19804        assert!(app.s3_state.buckets.loading);
19805
19806        // Step 2: simulate preview arriving (like main.rs does after load_bucket_preview)
19807        app.s3_state.bucket_preview.insert(
19808            "bucket0".to_string(),
19809            vec![S3Object {
19810                key: "file.txt".to_string(),
19811                is_prefix: false,
19812                size: 0,
19813                last_modified: String::new(),
19814                storage_class: String::new(),
19815            }],
19816        );
19817        app.s3_state.buckets.loading = false;
19818
19819        // Step 3: right arrow again — must go to first child (row 1), not sibling (row 2)
19820        app.handle_action(Action::NextPane);
19821        assert_eq!(
19822            app.s3_state.selected_row, 1,
19823            "After preview loads, right arrow must select first child (row 1), not sibling (row 2)"
19824        );
19825    }
19826
19827    #[test]
19828    fn test_s3_first_expand_never_moves_selection_before_children_visible() {
19829        // Rule: selection must NEVER move to a new row until the children rows
19830        // are actually present in the data (preview loaded). This prevents
19831        // showing a "selected next sibling" before children appear.
19832        use S3Bucket;
19833        let mut app = test_app();
19834        app.current_service = Service::S3Buckets;
19835        app.service_selected = true;
19836        app.mode = Mode::Normal;
19837        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19838
19839        app.s3_state.buckets.items = vec![
19840            S3Bucket {
19841                name: "bucket0".to_string(),
19842                region: "us-east-1".to_string(),
19843                creation_date: String::new(),
19844            },
19845            S3Bucket {
19846                name: "bucket1".to_string(),
19847                region: "us-east-1".to_string(),
19848                creation_date: String::new(),
19849            },
19850        ];
19851        // No preview for bucket0
19852        app.s3_state.selected_row = 0;
19853
19854        // First expand (right arrow = ExpandRow): no preview → must NOT move selection
19855        app.handle_action(Action::ExpandRow);
19856        assert_eq!(
19857            app.s3_state.selected_row, 0,
19858            "Selection must stay on bucket0 until children are visible"
19859        );
19860        assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
19861        assert!(app.s3_state.buckets.loading);
19862    }
19863
19864    #[test]
19865    fn test_s3_after_preview_loads_selection_moves_to_first_child() {
19866        // After first expand triggers loading, once preview arrives the selection
19867        // must automatically advance to the first child.
19868        use crate::s3::Object as S3Object;
19869        use S3Bucket;
19870        let mut app = test_app();
19871        app.current_service = Service::S3Buckets;
19872        app.service_selected = true;
19873        app.mode = Mode::Normal;
19874        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19875        app.s3_state.buckets.items = vec![
19876            S3Bucket {
19877                name: "b0".to_string(),
19878                region: "us-east-1".to_string(),
19879                creation_date: String::new(),
19880            },
19881            S3Bucket {
19882                name: "b1".to_string(),
19883                region: "us-east-1".to_string(),
19884                creation_date: String::new(),
19885            },
19886        ];
19887        app.s3_state.selected_row = 0;
19888
19889        // Step 1: right arrow — no preview, expand + loading, selection stays
19890        app.handle_action(Action::ExpandRow);
19891        assert_eq!(
19892            app.s3_state.selected_row, 0,
19893            "Must stay on bucket while loading"
19894        );
19895
19896        // Step 2: simulate preview arriving (main.rs calls load_bucket_preview)
19897        app.s3_state.bucket_preview.insert(
19898            "b0".to_string(),
19899            vec![S3Object {
19900                key: "f.txt".to_string(),
19901                is_prefix: false,
19902                size: 0,
19903                last_modified: String::new(),
19904                storage_class: String::new(),
19905            }],
19906        );
19907        // After load, app must advance selection to first child
19908        app.after_bucket_preview_loaded("b0");
19909        assert_eq!(
19910            app.s3_state.selected_row, 1,
19911            "After preview loads, selection must advance to first child (row 1)"
19912        );
19913    }
19914
19915    #[test]
19916    fn test_s3_right_arrow_on_expanded_loaded_bucket_enters_first_child() {
19917        // When bucket is already expanded AND preview is loaded, right arrow
19918        // must move selection to first child — NOT collapse the bucket.
19919        use crate::s3::Object as S3Object;
19920        use S3Bucket;
19921        let mut app = test_app();
19922        app.current_service = Service::S3Buckets;
19923        app.service_selected = true;
19924        app.mode = Mode::Normal;
19925        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19926        app.s3_state.buckets.items = vec![
19927            S3Bucket {
19928                name: "b0".to_string(),
19929                region: "us-east-1".to_string(),
19930                creation_date: String::new(),
19931            },
19932            S3Bucket {
19933                name: "b1".to_string(),
19934                region: "us-east-1".to_string(),
19935                creation_date: String::new(),
19936            },
19937        ];
19938        app.s3_state.expanded_prefixes.insert("b0".to_string());
19939        app.s3_state.bucket_preview.insert(
19940            "b0".to_string(),
19941            vec![S3Object {
19942                key: "f.txt".to_string(),
19943                is_prefix: false,
19944                size: 0,
19945                last_modified: String::new(),
19946                storage_class: String::new(),
19947            }],
19948        );
19949        app.s3_state.selected_row = 0;
19950
19951        app.handle_action(Action::ExpandRow);
19952
19953        // Must enter first child, not collapse
19954        assert!(
19955            app.s3_state.expanded_prefixes.contains("b0"),
19956            "Must stay expanded"
19957        );
19958        assert_eq!(app.s3_state.selected_row, 1, "Must move to first child");
19959    }
19960
19961    #[test]
19962    fn test_s3_second_expand_press_collapses_when_already_expanded_with_children() {
19963        // Collapse is done via left arrow (CollapseRow), not right arrow.
19964        // Right arrow on expanded+loaded bucket enters first child.
19965        use crate::s3::Object as S3Object;
19966        use S3Bucket;
19967        let mut app = test_app();
19968        app.current_service = Service::S3Buckets;
19969        app.service_selected = true;
19970        app.mode = Mode::Normal;
19971        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
19972        app.s3_state.buckets.items = vec![S3Bucket {
19973            name: "bucket0".to_string(),
19974            region: "us-east-1".to_string(),
19975            creation_date: String::new(),
19976        }];
19977        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
19978        app.s3_state.bucket_preview.insert(
19979            "bucket0".to_string(),
19980            vec![S3Object {
19981                key: "file.txt".to_string(),
19982                is_prefix: false,
19983                size: 0,
19984                last_modified: String::new(),
19985                storage_class: String::new(),
19986            }],
19987        );
19988        app.s3_state.selected_row = 0;
19989
19990        // Left arrow collapses
19991        app.handle_action(Action::CollapseRow);
19992        assert!(
19993            !app.s3_state.expanded_prefixes.contains("bucket0"),
19994            "Left arrow must collapse the bucket"
19995        );
19996        assert_eq!(app.s3_state.selected_row, 0);
19997    }
19998
19999    #[test]
20000    fn test_s3_expand_row_index_correct_when_prior_bucket_has_error() {
20001        // Repro: bucket0 is expanded and has an error message (1+ error rows).
20002        // next_pane uses `continue` for error buckets, skipping their error rows.
20003        // But calculate_filtered_bucket_rows counts error rows.
20004        // This causes selected_row=2 to map to bucket2 in next_pane
20005        // but to the error row of bucket0 visually — wrong expansion target.
20006        use S3Bucket;
20007        let mut app = test_app();
20008        app.current_service = Service::S3Buckets;
20009        app.service_selected = true;
20010        app.mode = Mode::Normal;
20011        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20012
20013        app.s3_state.buckets.items = vec![
20014            S3Bucket {
20015                name: "bucket0".to_string(),
20016                region: "us-east-1".to_string(),
20017                creation_date: String::new(),
20018            },
20019            S3Bucket {
20020                name: "bucket1".to_string(),
20021                region: "us-east-1".to_string(),
20022                creation_date: String::new(),
20023            },
20024            S3Bucket {
20025                name: "bucket2".to_string(),
20026                region: "us-east-1".to_string(),
20027                creation_date: String::new(),
20028            },
20029        ];
20030
20031        // bucket0 is expanded with an error (1 error row)
20032        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
20033        app.s3_state
20034            .bucket_errors
20035            .insert("bucket0".to_string(), "AccessDenied".to_string());
20036
20037        // Visual rows: 0=bucket0, 1=⚠️error, 2=bucket1, 3=bucket2
20038        // calculate_filtered_bucket_rows = 4
20039        let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
20040        assert_eq!(
20041            total, 4,
20042            "Should be 4 rows: bucket0 + error + bucket1 + bucket2"
20043        );
20044
20045        // User selects bucket2 (visual row 3)
20046        app.s3_state.selected_row = 3;
20047
20048        // bucket2 has a preview
20049        use crate::s3::Object as S3Object;
20050        app.s3_state.bucket_preview.insert(
20051            "bucket2".to_string(),
20052            vec![S3Object {
20053                key: "file.txt".to_string(),
20054                is_prefix: false,
20055                size: 0,
20056                last_modified: String::new(),
20057                storage_class: String::new(),
20058            }],
20059        );
20060
20061        app.handle_action(Action::NextPane);
20062
20063        // Must expand bucket2 (row 3), select its first child (row 4)
20064        assert!(
20065            app.s3_state.expanded_prefixes.contains("bucket2"),
20066            "Must expand bucket2, not bucket1"
20067        );
20068        assert_eq!(
20069            app.s3_state.selected_row, 4,
20070            "First child of bucket2 must be row 4"
20071        );
20072    }
20073
20074    #[test]
20075    fn test_s3_next_pane_row_count_matches_calculate_filtered_bucket_rows() {
20076        // The row index next_pane uses to find the selected bucket must match
20077        // the row index that calculate_filtered_bucket_rows produces.
20078        // If they diverge, selecting row N expands a different bucket than expected.
20079        use crate::s3::Object as S3Object;
20080        use S3Bucket;
20081        let mut app = test_app();
20082        app.current_service = Service::S3Buckets;
20083        app.service_selected = true;
20084        app.mode = Mode::Normal;
20085        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20086
20087        app.s3_state.buckets.items = vec![
20088            S3Bucket {
20089                name: "bucket0".to_string(),
20090                region: "us-east-1".to_string(),
20091                creation_date: String::new(),
20092            },
20093            S3Bucket {
20094                name: "bucket1".to_string(),
20095                region: "us-east-1".to_string(),
20096                creation_date: String::new(),
20097            },
20098            S3Bucket {
20099                name: "bucket2".to_string(),
20100                region: "us-east-1".to_string(),
20101                creation_date: String::new(),
20102            },
20103        ];
20104
20105        // bucket0 expanded + loaded with 3 children
20106        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
20107        app.s3_state.bucket_preview.insert(
20108            "bucket0".to_string(),
20109            vec![
20110                S3Object {
20111                    key: "a.txt".to_string(),
20112                    is_prefix: false,
20113                    size: 0,
20114                    last_modified: String::new(),
20115                    storage_class: String::new(),
20116                },
20117                S3Object {
20118                    key: "b.txt".to_string(),
20119                    is_prefix: false,
20120                    size: 0,
20121                    last_modified: String::new(),
20122                    storage_class: String::new(),
20123                },
20124                S3Object {
20125                    key: "c.txt".to_string(),
20126                    is_prefix: false,
20127                    size: 0,
20128                    last_modified: String::new(),
20129                    storage_class: String::new(),
20130                },
20131            ],
20132        );
20133
20134        // bucket1 expanded + loading (no preview yet)
20135        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
20136
20137        // calculate_filtered_bucket_rows: 3 buckets + 3 children of bucket0 + 0 loading = 6
20138        let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
20139        assert_eq!(
20140            total, 6,
20141            "Total rows: bucket0(1)+3children+bucket1(1)+bucket2(1)=6"
20142        );
20143
20144        // Visual rows: 0=bucket0, 1=a, 2=b, 3=c, 4=bucket1(loading), 5=bucket2
20145        // Select bucket2 (row 5)
20146        app.s3_state.selected_row = 5;
20147
20148        // bucket2 has preview
20149        app.s3_state.bucket_preview.insert(
20150            "bucket2".to_string(),
20151            vec![S3Object {
20152                key: "x.txt".to_string(),
20153                is_prefix: false,
20154                size: 0,
20155                last_modified: String::new(),
20156                storage_class: String::new(),
20157            }],
20158        );
20159
20160        app.handle_action(Action::NextPane);
20161
20162        assert!(
20163            app.s3_state.expanded_prefixes.contains("bucket2"),
20164            "Must expand bucket2, not bucket1"
20165        );
20166        assert_eq!(
20167            app.s3_state.selected_row, 6,
20168            "First child of bucket2 must be row 6"
20169        );
20170    }
20171
20172    #[test]
20173    fn test_s3_expand_row_index_correct_when_prior_bucket_has_loaded_children() {
20174        // Repro: bucket0 is expanded AND has loaded children (e.g. 2 objects).
20175        // This means visual rows are: 0=bucket0, 1=child0, 2=child1, 3=bucket1, 4=bucket2
20176        // If user selects bucket2 (visual row 4) and presses right, next_pane must
20177        // expand bucket2 — NOT a child of bucket0 (which is at row_idx 4 in old logic).
20178        use crate::s3::Object as S3Object;
20179        use S3Bucket;
20180        let mut app = test_app();
20181        app.current_service = Service::S3Buckets;
20182        app.service_selected = true;
20183        app.mode = Mode::Normal;
20184        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20185
20186        app.s3_state.buckets.items = vec![
20187            S3Bucket {
20188                name: "bucket0".to_string(),
20189                region: "us-east-1".to_string(),
20190                creation_date: String::new(),
20191            },
20192            S3Bucket {
20193                name: "bucket1".to_string(),
20194                region: "us-east-1".to_string(),
20195                creation_date: String::new(),
20196            },
20197            S3Bucket {
20198                name: "bucket2".to_string(),
20199                region: "us-east-1".to_string(),
20200                creation_date: String::new(),
20201            },
20202        ];
20203
20204        // bucket0 is expanded AND has 2 children loaded
20205        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
20206        app.s3_state.bucket_preview.insert(
20207            "bucket0".to_string(),
20208            vec![
20209                S3Object {
20210                    key: "a.txt".to_string(),
20211                    is_prefix: false,
20212                    size: 0,
20213                    last_modified: String::new(),
20214                    storage_class: String::new(),
20215                },
20216                S3Object {
20217                    key: "b.txt".to_string(),
20218                    is_prefix: false,
20219                    size: 0,
20220                    last_modified: String::new(),
20221                    storage_class: String::new(),
20222                },
20223            ],
20224        );
20225
20226        // Visual rows: 0=bucket0, 1=a.txt, 2=b.txt, 3=bucket1, 4=bucket2
20227        // User selects bucket2 (visual row 4)
20228        app.s3_state.selected_row = 4;
20229
20230        // bucket2 has preview ready
20231        app.s3_state.bucket_preview.insert(
20232            "bucket2".to_string(),
20233            vec![S3Object {
20234                key: "c.txt".to_string(),
20235                is_prefix: false,
20236                size: 0,
20237                last_modified: String::new(),
20238                storage_class: String::new(),
20239            }],
20240        );
20241
20242        app.handle_action(Action::NextPane);
20243
20244        // Must expand bucket2 (row 4) and move to its first child (row 5)
20245        assert!(
20246            app.s3_state.expanded_prefixes.contains("bucket2"),
20247            "bucket2 must be expanded"
20248        );
20249        assert_eq!(
20250            app.s3_state.selected_row, 5,
20251            "Must select first child of bucket2 (row 5), not some wrong row"
20252        );
20253        assert!(
20254            !app.s3_state.expanded_prefixes.contains("bucket1"),
20255            "bucket1 must NOT be expanded"
20256        );
20257    }
20258
20259    #[test]
20260    fn test_s3_expand_with_empty_preview_stays_on_bucket() {
20261        // Repro: bucket has a preview but it's EMPTY (no objects at root).
20262        // After expand, selected_row must stay on the bucket row — NOT advance to next sibling.
20263        use S3Bucket;
20264        let mut app = test_app();
20265        app.current_service = Service::S3Buckets;
20266        app.service_selected = true;
20267        app.mode = Mode::Normal;
20268        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20269
20270        app.s3_state.buckets.items = vec![
20271            S3Bucket {
20272                name: "bucket0".to_string(),
20273                region: "us-east-1".to_string(),
20274                creation_date: String::new(),
20275            },
20276            S3Bucket {
20277                name: "bucket1".to_string(),
20278                region: "us-east-1".to_string(),
20279                creation_date: String::new(),
20280            },
20281            S3Bucket {
20282                name: "bucket2".to_string(),
20283                region: "us-east-1".to_string(),
20284                creation_date: String::new(),
20285            },
20286        ];
20287        // bucket1 has empty preview (bucket exists but no root-level objects)
20288        app.s3_state
20289            .bucket_preview
20290            .insert("bucket1".to_string(), vec![]);
20291        app.s3_state.selected_row = 1; // on bucket1
20292
20293        app.handle_action(Action::NextPane);
20294
20295        // Must stay on bucket1 (row 1) — no children to enter
20296        assert_eq!(
20297            app.s3_state.selected_row, 1,
20298            "Empty bucket: selection must stay on bucket row, not advance to next sibling"
20299        );
20300        assert!(
20301            app.s3_state.expanded_prefixes.contains("bucket1"),
20302            "Bucket must be expanded (showing ▼)"
20303        );
20304    }
20305
20306    #[test]
20307    fn test_s3_expand_with_preceding_loading_buckets_on_prior_page() {
20308        // Repro: user is on page 2 (scroll_offset=50). Some buckets on page 1
20309        // are expanded+loading. The next_pane traversal iterates all buckets from 0,
20310        // and each loading bucket on page 1 still adds a phantom row, shifting
20311        // the row where selected_row=55 actually lands.
20312        use crate::s3::Object as S3Object;
20313        use S3Bucket;
20314        let mut app = test_app();
20315        app.current_service = Service::S3Buckets;
20316        app.service_selected = true;
20317        app.mode = Mode::Normal;
20318        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20319
20320        // 60 buckets
20321        app.s3_state.buckets.items = (0..60)
20322            .map(|i| S3Bucket {
20323                name: format!("bucket{:02}", i),
20324                region: "us-east-1".to_string(),
20325                creation_date: String::new(),
20326            })
20327            .collect();
20328
20329        // Buckets 0-2 on page 1 are expanded+loading (no preview)
20330        for i in 0..3usize {
20331            app.s3_state
20332                .expanded_prefixes
20333                .insert(format!("bucket{:02}", i));
20334        }
20335
20336        app.s3_state.bucket_scroll_offset = 50;
20337
20338        // bucket55 has preview loaded
20339        app.s3_state.bucket_preview.insert(
20340            "bucket55".to_string(),
20341            vec![S3Object {
20342                key: "file.txt".to_string(),
20343                is_prefix: false,
20344                size: 0,
20345                last_modified: String::new(),
20346                storage_class: String::new(),
20347            }],
20348        );
20349
20350        // selected_row = 55 (bucket55, visible row 5 on page 2)
20351        app.s3_state.selected_row = 55;
20352
20353        app.handle_action(Action::NextPane);
20354
20355        // Must expand bucket55, select its first child (row 56)
20356        assert!(
20357            app.s3_state.expanded_prefixes.contains("bucket55"),
20358            "bucket55 must be expanded"
20359        );
20360        assert_eq!(
20361            app.s3_state.selected_row, 56,
20362            "Must select first child of bucket55 (row 56)"
20363        );
20364    }
20365
20366    #[test]
20367    fn test_s3_expand_with_multiple_preceding_loading_buckets() {
20368        // Repro: multiple buckets before the selected one are expanded+loading.
20369        // Each was incorrectly adding a phantom row causing selection to point to wrong bucket.
20370        use crate::s3::Object as S3Object;
20371        use S3Bucket;
20372        let mut app = test_app();
20373        app.current_service = Service::S3Buckets;
20374        app.service_selected = true;
20375        app.mode = Mode::Normal;
20376        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20377
20378        app.s3_state.buckets.items = (0..5)
20379            .map(|i| S3Bucket {
20380                name: format!("bucket{}", i),
20381                region: "us-east-1".to_string(),
20382                creation_date: String::new(),
20383            })
20384            .collect();
20385
20386        // Buckets 0, 1, 2 are expanded+loading (no preview)
20387        for i in 0..3 {
20388            app.s3_state
20389                .expanded_prefixes
20390                .insert(format!("bucket{}", i));
20391        }
20392
20393        // bucket3 has preview loaded
20394        app.s3_state.bucket_preview.insert(
20395            "bucket3".to_string(),
20396            vec![S3Object {
20397                key: "file.txt".to_string(),
20398                is_prefix: false,
20399                size: 0,
20400                last_modified: String::new(),
20401                storage_class: String::new(),
20402            }],
20403        );
20404
20405        // Visual: bucket0 (row 0), bucket1 (row 1), bucket2 (row 2), bucket3 (row 3), bucket4 (row 4)
20406        // No loading rows visible since previews not loaded
20407        app.s3_state.selected_row = 3; // bucket3
20408
20409        app.handle_action(Action::NextPane);
20410
20411        // Must expand bucket3, move to its first child (row 4)
20412        assert!(
20413            app.s3_state.expanded_prefixes.contains("bucket3"),
20414            "bucket3 must be expanded"
20415        );
20416        assert_eq!(
20417            app.s3_state.selected_row, 4,
20418            "Must select first child of bucket3 (row 4)"
20419        );
20420    }
20421
20422    #[test]
20423    fn test_s3_expand_with_preceding_loading_bucket_selects_correct_row() {
20424        // Repro: bucket0 is expanded+loading (in expanded_prefixes, no preview).
20425        // next_pane's traversal adds a phantom row for bucket0's loading state.
20426        // This shifts row_idx so that selecting bucket1 (visual row 1) actually
20427        // points to bucket2 (visual row 2) in the traversal — wrong expansion target.
20428        use crate::s3::Object as S3Object;
20429        use S3Bucket;
20430        let mut app = test_app();
20431        app.current_service = Service::S3Buckets;
20432        app.service_selected = true;
20433        app.mode = Mode::Normal;
20434        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
20435
20436        app.s3_state.buckets.items = vec![
20437            S3Bucket {
20438                name: "bucket0".to_string(),
20439                region: "us-east-1".to_string(),
20440                creation_date: String::new(),
20441            },
20442            S3Bucket {
20443                name: "bucket1".to_string(),
20444                region: "us-east-1".to_string(),
20445                creation_date: String::new(),
20446            },
20447            S3Bucket {
20448                name: "bucket2".to_string(),
20449                region: "us-east-1".to_string(),
20450                creation_date: String::new(),
20451            },
20452        ];
20453
20454        // bucket0 is expanded+loading (in expanded_prefixes but NO preview)
20455        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
20456        app.s3_state.buckets.loading = true;
20457
20458        // bucket1's preview is loaded
20459        app.s3_state.bucket_preview.insert(
20460            "bucket1".to_string(),
20461            vec![S3Object {
20462                key: "file.txt".to_string(),
20463                is_prefix: false,
20464                size: 0,
20465                last_modified: String::new(),
20466                storage_class: String::new(),
20467            }],
20468        );
20469
20470        // User selects bucket1 (visual row 1 = bucket row, bucket0 has no visible children)
20471        app.s3_state.selected_row = 1;
20472
20473        // Expand bucket1
20474        app.handle_action(Action::NextPane);
20475
20476        // Must expand bucket1 (not bucket2), so selected_row = 2 (first child of bucket1)
20477        assert_eq!(
20478            app.s3_state.selected_row, 2,
20479            "Must expand bucket1's first child (row 2), not bucket2 (row 3)"
20480        );
20481        assert!(
20482            app.s3_state.expanded_prefixes.contains("bucket1"),
20483            "bucket1 must be in expanded_prefixes"
20484        );
20485    }
20486
20487    #[test]
20488    fn test_s3_expand_last_row_on_page_shows_first_child_not_next_page() {
20489        // Repro: user selects the last bucket on a page (e.g. row 49 with page size 50),
20490        // expands it. selected_row becomes 50 (first child). But scroll_offset is still 0,
20491        // so row 50 is off screen — visually the next bucket (row 50 on next page) appears selected.
20492        // Fix: after expanding the last row on a visible page, scroll must advance to show first child.
20493        use crate::s3::Object as S3Object;
20494        use S3Bucket;
20495        let mut app = test_app();
20496        app.current_service = Service::S3Buckets;
20497        app.service_selected = true;
20498        app.mode = Mode::Normal;
20499
20500        // 3 buckets, visible area = 2 rows (tight to force the issue)
20501        app.s3_state.buckets.items = vec![
20502            S3Bucket {
20503                name: "bucket0".to_string(),
20504                region: "us-east-1".to_string(),
20505                creation_date: String::new(),
20506            },
20507            S3Bucket {
20508                name: "bucket1".to_string(),
20509                region: "us-east-1".to_string(),
20510                creation_date: String::new(),
20511            },
20512            S3Bucket {
20513                name: "bucket2".to_string(),
20514                region: "us-east-1".to_string(),
20515                creation_date: String::new(),
20516            },
20517        ];
20518        app.s3_state.bucket_visible_rows = std::cell::Cell::new(2);
20519        // Select the last visible bucket (row 1), scroll_offset = 0 → rows 0 and 1 visible
20520        app.s3_state.selected_row = 1;
20521        app.s3_state.bucket_scroll_offset = 0;
20522
20523        // Preview already loaded
20524        app.s3_state.bucket_preview.insert(
20525            "bucket1".to_string(),
20526            vec![S3Object {
20527                key: "file.txt".to_string(),
20528                is_prefix: false,
20529                size: 0,
20530                last_modified: String::new(),
20531                storage_class: String::new(),
20532            }],
20533        );
20534
20535        app.handle_action(Action::NextPane); // right arrow — expand
20536
20537        // selected_row must be 2 (first child of bucket1)
20538        assert_eq!(app.s3_state.selected_row, 2, "Must select first child");
20539
20540        // scroll_offset must have advanced so row 2 is visible
20541        let visible = app.s3_state.bucket_visible_rows.get();
20542        let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
20543            && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
20544        assert!(
20545            in_view,
20546            "First child row {} must be visible in [{}, {})",
20547            app.s3_state.selected_row,
20548            app.s3_state.bucket_scroll_offset,
20549            app.s3_state.bucket_scroll_offset + visible
20550        );
20551    }
20552
20553    #[test]
20554    fn test_apig_detail_tab_navigation() {
20555        use crate::apig::api::RestApi;
20556        use crate::ui::apig::ApiDetailTab;
20557
20558        let mut app = test_app();
20559        app.current_service = Service::ApiGatewayApis;
20560        app.apig_state.current_api = Some(RestApi {
20561            id: "test123".to_string(),
20562            name: "Test API".to_string(),
20563            description: "Test".to_string(),
20564            created_date: "2024-01-01".to_string(),
20565            api_key_source: "HEADER".to_string(),
20566            endpoint_configuration: "REGIONAL".to_string(),
20567            protocol_type: "REST".to_string(),
20568            disable_execute_api_endpoint: false,
20569            status: "AVAILABLE".to_string(),
20570        });
20571
20572        // Should start on Routes tab
20573        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20574
20575        // Next tab - should stay on Routes (only tab)
20576        app.handle_action(Action::NextDetailTab);
20577        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20578
20579        // Prev tab - should stay on Routes (only tab)
20580        app.handle_action(Action::PrevDetailTab);
20581        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20582
20583        // Another next
20584        app.handle_action(Action::NextDetailTab);
20585        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20586
20587        // Prev tab should stay on Routes (only tab)
20588        app.handle_action(Action::PrevDetailTab);
20589        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20590
20591        // Next tab should stay on Routes (only tab)
20592        app.handle_action(Action::NextDetailTab);
20593        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
20594    }
20595
20596    #[test]
20597    fn test_apig_routes_expand_collapse() {
20598        use crate::apig::api::RestApi;
20599        use crate::apig::route::Route;
20600        use crate::ui::apig::ApiDetailTab;
20601
20602        let mut app = test_app();
20603        app.current_service = Service::ApiGatewayApis;
20604        app.apig_state.current_api = Some(RestApi {
20605            id: "test123".to_string(),
20606            name: "Test API".to_string(),
20607            description: "Test".to_string(),
20608            created_date: "2024-01-01".to_string(),
20609            api_key_source: "HEADER".to_string(),
20610            endpoint_configuration: "REGIONAL".to_string(),
20611            protocol_type: "HTTP".to_string(),
20612            disable_execute_api_endpoint: false,
20613            status: "AVAILABLE".to_string(),
20614        });
20615        app.apig_state.detail_tab = ApiDetailTab::Routes;
20616
20617        // Create hierarchy with virtual parent
20618        let virtual_parent = Route {
20619            route_id: "virtual_/api".to_string(),
20620            route_key: "/api".to_string(),
20621            target: String::new(), // Empty target = virtual parent
20622            authorization_type: String::new(),
20623            api_key_required: false,
20624            display_name: String::new(),
20625            arn: String::new(),
20626        };
20627        let child_route = Route {
20628            route_id: "1".to_string(),
20629            route_key: "/api/users".to_string(),
20630            target: "integration1".to_string(),
20631            authorization_type: "NONE".to_string(),
20632            api_key_required: false,
20633            display_name: String::new(),
20634            arn: String::new(),
20635        };
20636
20637        app.apig_state.routes.items = vec![virtual_parent];
20638        app.apig_state
20639            .route_children
20640            .insert("/api".to_string(), vec![child_route]);
20641
20642        // Initially no routes expanded
20643        assert!(app.apig_state.expanded_routes.is_empty());
20644
20645        // Expand virtual parent
20646        app.apig_state.routes.selected = 0;
20647        app.handle_action(Action::ExpandRow);
20648        assert!(app.apig_state.expanded_routes.contains("/api"));
20649
20650        // Collapse virtual parent
20651        app.handle_action(Action::CollapseRow);
20652        assert!(!app.apig_state.expanded_routes.contains("/api"));
20653
20654        // Toggle expand again
20655        app.handle_action(Action::ExpandRow);
20656        assert!(app.apig_state.expanded_routes.contains("/api"));
20657    }
20658
20659    #[test]
20660    fn test_apig_routes_navigation() {
20661        use crate::apig::api::RestApi;
20662        use crate::apig::route::Route;
20663        use crate::ui::apig::ApiDetailTab;
20664
20665        let mut app = test_app();
20666        app.mode = Mode::Normal;
20667        app.service_selected = true;
20668        app.current_service = Service::ApiGatewayApis;
20669        app.apig_state.current_api = Some(RestApi {
20670            id: "test123".to_string(),
20671            name: "Test API".to_string(),
20672            description: "Test".to_string(),
20673            created_date: "2024-01-01".to_string(),
20674            api_key_source: "HEADER".to_string(),
20675            endpoint_configuration: "REGIONAL".to_string(),
20676            protocol_type: "HTTP".to_string(),
20677            disable_execute_api_endpoint: false,
20678            status: "AVAILABLE".to_string(),
20679        });
20680        app.apig_state.detail_tab = ApiDetailTab::Routes;
20681        app.apig_state.routes.items = vec![
20682            Route {
20683                route_id: "1".to_string(),
20684                route_key: "/api/users".to_string(),
20685                target: "integration1".to_string(),
20686                authorization_type: "NONE".to_string(),
20687                api_key_required: false,
20688                display_name: String::new(),
20689                arn: String::new(),
20690            },
20691            Route {
20692                route_id: "2".to_string(),
20693                route_key: "/health".to_string(),
20694                target: "integration2".to_string(),
20695                authorization_type: "NONE".to_string(),
20696                api_key_required: false,
20697                display_name: String::new(),
20698                arn: String::new(),
20699            },
20700        ];
20701
20702        // Start at first route
20703        assert_eq!(app.apig_state.routes.selected, 0);
20704
20705        // Navigate down
20706        app.handle_action(Action::NextItem);
20707        assert_eq!(app.apig_state.routes.selected, 1);
20708
20709        // Navigate down (should stay at last)
20710        app.handle_action(Action::NextItem);
20711        assert_eq!(app.apig_state.routes.selected, 1);
20712
20713        // Navigate up
20714        app.handle_action(Action::PrevItem);
20715        assert_eq!(app.apig_state.routes.selected, 0);
20716
20717        // Navigate up (should stay at first)
20718        app.handle_action(Action::PrevItem);
20719        assert_eq!(app.apig_state.routes.selected, 0);
20720    }
20721
20722    #[test]
20723    fn test_apig_routes_expand_jumps_to_child() {
20724        use crate::apig::api::RestApi;
20725        use crate::apig::route::Route;
20726        use crate::ui::apig::ApiDetailTab;
20727        use std::collections::HashMap;
20728
20729        let mut app = test_app();
20730        app.mode = Mode::Normal;
20731        app.service_selected = true;
20732        app.current_service = Service::ApiGatewayApis;
20733        app.apig_state.current_api = Some(RestApi {
20734            id: "test123".to_string(),
20735            name: "Test API".to_string(),
20736            description: "Test".to_string(),
20737            created_date: "2024-01-01".to_string(),
20738            api_key_source: "HEADER".to_string(),
20739            endpoint_configuration: "REGIONAL".to_string(),
20740            protocol_type: "HTTP".to_string(),
20741            disable_execute_api_endpoint: false,
20742            status: "AVAILABLE".to_string(),
20743        });
20744        app.apig_state.detail_tab = ApiDetailTab::Routes;
20745
20746        // Set up hierarchy: /api with child /api/users
20747        app.apig_state.routes.items = vec![Route {
20748            route_id: "virtual_/api".to_string(),
20749            route_key: "/api".to_string(),
20750            target: String::new(),
20751            authorization_type: String::new(),
20752            api_key_required: false,
20753            display_name: String::new(),
20754            arn: String::new(),
20755        }];
20756
20757        let mut children = HashMap::new();
20758        children.insert(
20759            "/api".to_string(),
20760            vec![Route {
20761                route_id: "1".to_string(),
20762                route_key: "/api/users".to_string(),
20763                target: "integration1".to_string(),
20764                authorization_type: "NONE".to_string(),
20765                api_key_required: false,
20766                display_name: String::new(),
20767                arn: String::new(),
20768            }],
20769        );
20770        app.apig_state.route_children = children;
20771
20772        // Start at /api (row 0)
20773        assert_eq!(app.apig_state.routes.selected, 0);
20774        assert!(!app.apig_state.expanded_routes.contains("/api"));
20775
20776        // First expand - should expand /api
20777        app.handle_action(Action::ExpandRow);
20778        assert!(app.apig_state.expanded_routes.contains("/api"));
20779        assert_eq!(app.apig_state.routes.selected, 0);
20780
20781        // Second expand - should jump to first child (row 1)
20782        app.handle_action(Action::ExpandRow);
20783        assert_eq!(app.apig_state.routes.selected, 1);
20784    }
20785
20786    #[test]
20787    fn test_apig_filter_only_when_focused() {
20788        use crate::apig::api::RestApi;
20789        use crate::ui::apig::ApiDetailTab;
20790
20791        let mut app = test_app();
20792        app.current_service = Service::ApiGatewayApis;
20793        app.apig_state.current_api = Some(RestApi {
20794            id: "test".to_string(),
20795            name: "Test API".to_string(),
20796            description: "Test".to_string(),
20797            created_date: "2024-01-01".to_string(),
20798            api_key_source: "HEADER".to_string(),
20799            endpoint_configuration: "REGIONAL".to_string(),
20800            protocol_type: "HTTP".to_string(),
20801            disable_execute_api_endpoint: false,
20802            status: "AVAILABLE".to_string(),
20803        });
20804        app.apig_state.detail_tab = ApiDetailTab::Routes;
20805        app.mode = Mode::FilterInput;
20806
20807        // When input_focus is NOT on Filter, typing should not update filter
20808        app.apig_state.input_focus = InputFocus::Pagination;
20809        app.handle_action(Action::FilterInput('x'));
20810        assert_eq!(app.apig_state.route_filter, "");
20811
20812        // When input_focus IS on Filter, typing should update filter
20813        app.apig_state.input_focus = InputFocus::Filter;
20814        app.handle_action(Action::FilterInput('x'));
20815        assert_eq!(app.apig_state.route_filter, "x");
20816    }
20817
20818    #[test]
20819    fn test_apig_routes_and_resources_use_same_render_function() {
20820        // Both routes and resources must call crate::ui::table::render_tree_table
20821        let source = include_str!("ui/apig.rs");
20822        let render_calls: Vec<_> = source
20823            .match_indices("crate::ui::table::render_tree_table")
20824            .collect();
20825
20826        // Should have exactly 2 calls: one for resources, one for routes
20827        assert_eq!(
20828            render_calls.len(),
20829            2,
20830            "Both routes and resources must use render_tree_table"
20831        );
20832    }
20833
20834    #[test]
20835    fn test_s3_uses_same_render_function() {
20836        // S3 objects must also call crate::ui::table::render_tree_table
20837        let source = include_str!("ui/s3.rs");
20838        let render_calls: Vec<_> = source
20839            .match_indices("crate::ui::table::render_tree_table")
20840            .collect();
20841
20842        // Should have at least 1 call for objects table
20843        assert!(!render_calls.is_empty(), "S3 must use render_tree_table");
20844    }
20845
20846    #[test]
20847    fn test_search_icon_has_proper_border_spacing() {
20848        // SEARCH_ICON should have dashes on both sides for proper border rendering
20849        // Should be "─ 🔍 ─" not "─ 🔍 " to avoid "╭N" appearance
20850        use crate::ui::SEARCH_ICON;
20851
20852        assert!(SEARCH_ICON.starts_with("─"), "Should start with dash");
20853        assert!(
20854            SEARCH_ICON.ends_with("─"),
20855            "Should end with dash for proper border spacing"
20856        );
20857        assert!(SEARCH_ICON.contains("🔍"), "Should contain search icon");
20858    }
20859
20860    #[test]
20861    fn test_apig_expand_with_filter() {
20862        use crate::apig::api::RestApi;
20863        use crate::apig::route::Route;
20864        use crate::ui::apig::ApiDetailTab;
20865        use std::collections::HashMap;
20866
20867        let mut app = test_app();
20868        app.current_service = Service::ApiGatewayApis;
20869        app.apig_state.current_api = Some(RestApi {
20870            id: "test123".to_string(),
20871            name: "Test API".to_string(),
20872            description: "Test".to_string(),
20873            created_date: "2024-01-01".to_string(),
20874            api_key_source: "HEADER".to_string(),
20875            endpoint_configuration: "REGIONAL".to_string(),
20876            protocol_type: "HTTP".to_string(),
20877            disable_execute_api_endpoint: false,
20878            status: "AVAILABLE".to_string(),
20879        });
20880        app.apig_state.detail_tab = ApiDetailTab::Routes;
20881
20882        // Setup routes with parent and child
20883        app.apig_state.routes.items = vec![Route {
20884            route_id: "0".to_string(),
20885            route_key: "/api".to_string(),
20886            target: "".to_string(),
20887            authorization_type: "NONE".to_string(),
20888            api_key_required: false,
20889            display_name: "/api".to_string(),
20890            arn: String::new(),
20891        }];
20892
20893        let mut children = HashMap::new();
20894        children.insert(
20895            "/api".to_string(),
20896            vec![Route {
20897                route_id: "1".to_string(),
20898                route_key: "GET".to_string(),
20899                target: "integration1".to_string(),
20900                authorization_type: "NONE".to_string(),
20901                api_key_required: false,
20902                display_name: "GET".to_string(),
20903                arn: String::new(),
20904            }],
20905        );
20906        app.apig_state.route_children = children;
20907
20908        // Apply filter that matches child
20909        app.apig_state.route_filter = "GET".to_string();
20910
20911        // Parent should be visible (because child matches)
20912        // Expand should work on filtered parent
20913        assert_eq!(app.apig_state.routes.selected, 0);
20914        assert!(!app.apig_state.expanded_routes.contains("/api"));
20915
20916        app.handle_action(Action::ExpandRow);
20917
20918        // Should expand the parent
20919        assert!(app.apig_state.expanded_routes.contains("/api"));
20920    }
20921
20922    #[test]
20923    fn test_apig_console_url_routes() {
20924        use crate::apig::api::RestApi;
20925        use crate::apig::route::Route;
20926        use crate::ui::apig::ApiDetailTab;
20927
20928        let mut app = test_app();
20929        app.current_service = Service::ApiGatewayApis;
20930        app.config.region = "us-east-1".to_string();
20931
20932        // Test API list URL
20933        let url = app.get_console_url();
20934        assert!(url.contains("apigateway/main/apis"));
20935        assert!(url.contains("region=us-east-1"));
20936
20937        // Test routes URL with selection
20938        app.apig_state.current_api = Some(RestApi {
20939            id: "2todvod3n0".to_string(),
20940            name: "Test API".to_string(),
20941            description: "Test".to_string(),
20942            created_date: "2024-01-01".to_string(),
20943            api_key_source: "HEADER".to_string(),
20944            endpoint_configuration: "REGIONAL".to_string(),
20945            protocol_type: "HTTP".to_string(),
20946            disable_execute_api_endpoint: false,
20947            status: "AVAILABLE".to_string(),
20948        });
20949        app.apig_state.detail_tab = ApiDetailTab::Routes;
20950        app.apig_state.routes.items = vec![Route {
20951            route_id: "eizmisr".to_string(),
20952            route_key: "GET /test".to_string(),
20953            target: "integration1".to_string(),
20954            authorization_type: "NONE".to_string(),
20955            api_key_required: false,
20956            display_name: "GET /test".to_string(),
20957            arn: String::new(),
20958        }];
20959        app.apig_state.routes.selected = 0;
20960
20961        let url = app.get_console_url();
20962        assert!(url.contains("apigateway/main/develop/routes"));
20963        assert!(url.contains("api=2todvod3n0"));
20964        assert!(url.contains("routes=eizmisr"));
20965        assert!(url.contains("region=us-east-1"));
20966    }
20967
20968    #[test]
20969    fn test_apig_console_url_resources() {
20970        use crate::apig::api::RestApi;
20971        use crate::apig::resource::Resource;
20972        use crate::ui::apig::ApiDetailTab;
20973
20974        let mut app = test_app();
20975        app.current_service = Service::ApiGatewayApis;
20976        app.config.region = "us-east-1".to_string();
20977
20978        // Test resources URL with selection
20979        app.apig_state.current_api = Some(RestApi {
20980            id: "2j9j50ze47".to_string(),
20981            name: "Test API".to_string(),
20982            description: "Test".to_string(),
20983            created_date: "2024-01-01".to_string(),
20984            api_key_source: "HEADER".to_string(),
20985            endpoint_configuration: "REGIONAL".to_string(),
20986            protocol_type: "REST".to_string(),
20987            disable_execute_api_endpoint: false,
20988            status: "AVAILABLE".to_string(),
20989        });
20990        app.apig_state.detail_tab = ApiDetailTab::Routes;
20991        app.apig_state.resources.items = vec![Resource {
20992            id: "abc123".to_string(),
20993            path: "/test".to_string(),
20994            parent_id: None,
20995            methods: vec![],
20996            display_name: "/test".to_string(),
20997            arn: String::new(),
20998        }];
20999        app.apig_state.resources.selected = 0;
21000
21001        let url = app.get_console_url();
21002        assert!(url.contains("apigateway/main/apis/2j9j50ze47/resources"));
21003        assert!(url.contains("api=2j9j50ze47"));
21004        assert!(url.contains("#abc123"));
21005        assert!(url.contains("region=us-east-1"));
21006    }
21007
21008    #[test]
21009    fn test_apig_console_url_routes_parent_vs_leaf() {
21010        use crate::apig::api::RestApi;
21011        use crate::apig::route::Route;
21012        use crate::ui::apig::ApiDetailTab;
21013        use std::collections::HashMap;
21014
21015        let mut app = test_app();
21016        app.current_service = Service::ApiGatewayApis;
21017        app.config.region = "us-east-1".to_string();
21018
21019        app.apig_state.current_api = Some(RestApi {
21020            id: "2todvod3n0".to_string(),
21021            name: "Test API".to_string(),
21022            description: "Test".to_string(),
21023            created_date: "2024-01-01".to_string(),
21024            api_key_source: "HEADER".to_string(),
21025            endpoint_configuration: "REGIONAL".to_string(),
21026            protocol_type: "HTTP".to_string(),
21027            disable_execute_api_endpoint: false,
21028            status: "AVAILABLE".to_string(),
21029        });
21030        app.apig_state.detail_tab = ApiDetailTab::Routes;
21031
21032        // Parent node (virtual path, no target)
21033        app.apig_state.routes.items = vec![Route {
21034            route_id: "parent".to_string(),
21035            route_key: "/v1/get/jobs".to_string(),
21036            target: "".to_string(), // Empty target = parent node
21037            authorization_type: "NONE".to_string(),
21038            api_key_required: false,
21039            display_name: "/v1/get/jobs".to_string(),
21040            arn: String::new(),
21041        }];
21042
21043        // Child leaf node (actual route with target)
21044        let mut children = HashMap::new();
21045        children.insert(
21046            "/v1/get/jobs".to_string(),
21047            vec![Route {
21048                route_id: "1iz9vtl".to_string(),
21049                route_key: "GET".to_string(),
21050                target: "integration1".to_string(), // Has target = leaf node
21051                authorization_type: "NONE".to_string(),
21052                api_key_required: false,
21053                display_name: "GET".to_string(),
21054                arn: String::new(),
21055            }],
21056        );
21057        app.apig_state.route_children = children;
21058
21059        // Select parent - should NOT include routes parameter
21060        app.apig_state.routes.selected = 0;
21061        let url = app.get_console_url();
21062        assert!(url.contains("apigateway/main/develop/routes"));
21063        assert!(url.contains("api=2todvod3n0"));
21064        assert!(
21065            !url.contains("routes="),
21066            "Parent node should not include routes parameter"
21067        );
21068
21069        // Expand parent and select child leaf
21070        app.apig_state
21071            .expanded_routes
21072            .insert("/v1/get/jobs".to_string());
21073        app.apig_state.routes.selected = 1;
21074        let url = app.get_console_url();
21075        assert!(
21076            url.contains("routes=1iz9vtl"),
21077            "Leaf node should include routes parameter: {}",
21078            url
21079        );
21080    }
21081
21082    #[test]
21083    fn test_apig_preferences_context() {
21084        use crate::apig::api::RestApi;
21085        use crate::ui::apig::ApiDetailTab;
21086
21087        let mut app = test_app();
21088        app.current_service = Service::ApiGatewayApis;
21089
21090        // In API list view - current_api is None
21091        assert!(
21092            app.apig_state.current_api.is_none(),
21093            "Should be in list view"
21094        );
21095
21096        // In detail view - current_api is Some
21097        app.apig_state.current_api = Some(RestApi {
21098            id: "test123".to_string(),
21099            name: "Test API".to_string(),
21100            description: "Test".to_string(),
21101            created_date: "2024-01-01".to_string(),
21102            api_key_source: "HEADER".to_string(),
21103            endpoint_configuration: "REGIONAL".to_string(),
21104            protocol_type: "HTTP".to_string(),
21105            disable_execute_api_endpoint: false,
21106            status: "AVAILABLE".to_string(),
21107        });
21108        app.apig_state.detail_tab = ApiDetailTab::Routes;
21109
21110        assert!(
21111            app.apig_state.current_api.is_some(),
21112            "Should be in detail view"
21113        );
21114        // Preferences rendering will check current_api.is_none() to decide what to show
21115    }
21116
21117    #[test]
21118    fn test_apig_route_columns() {
21119        use crate::apig::route::Column as RouteColumn;
21120
21121        // Verify all columns exist
21122        let cols = RouteColumn::all();
21123        assert_eq!(cols.len(), 5);
21124
21125        // Verify column IDs
21126        assert_eq!(RouteColumn::RouteKey.id(), "route_key");
21127        assert_eq!(RouteColumn::RouteId.id(), "route_id");
21128        assert_eq!(RouteColumn::Arn.id(), "arn");
21129        assert_eq!(RouteColumn::AuthorizationType.id(), "authorization_type");
21130        assert_eq!(RouteColumn::Target.id(), "target");
21131
21132        // Verify from_id
21133        assert_eq!(
21134            RouteColumn::from_id("route_key"),
21135            Some(RouteColumn::RouteKey)
21136        );
21137        assert_eq!(RouteColumn::from_id("arn"), Some(RouteColumn::Arn));
21138        assert_eq!(RouteColumn::from_id("invalid"), None);
21139    }
21140
21141    #[test]
21142    fn test_apig_yank_copies_route_arn() {
21143        use crate::apig::api::RestApi;
21144        use crate::ui::apig::ApiDetailTab;
21145
21146        let mut app = test_app();
21147        app.current_service = Service::ApiGatewayApis;
21148        app.apig_state.current_api = Some(RestApi {
21149            id: "test123".to_string(),
21150            name: "Test API".to_string(),
21151            description: "Test".to_string(),
21152            created_date: "2024-01-01".to_string(),
21153            api_key_source: "HEADER".to_string(),
21154            endpoint_configuration: "REGIONAL".to_string(),
21155            protocol_type: "HTTP".to_string(),
21156            disable_execute_api_endpoint: false,
21157            status: "AVAILABLE".to_string(),
21158        });
21159        app.apig_state.detail_tab = ApiDetailTab::Routes;
21160
21161        // Add routes with ARNs
21162        app.apig_state.routes.items = vec![
21163            Route {
21164                route_id: "route1".to_string(),
21165                route_key: "GET /users".to_string(),
21166                target: "integrations/abc".to_string(),
21167                authorization_type: "NONE".to_string(),
21168                api_key_required: false,
21169                display_name: "GET /users".to_string(),
21170                arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1".to_string(),
21171            },
21172            Route {
21173                route_id: "route2".to_string(),
21174                route_key: "POST /users".to_string(),
21175                target: "integrations/def".to_string(),
21176                authorization_type: "AWS_IAM".to_string(),
21177                api_key_required: true,
21178                display_name: "POST /users".to_string(),
21179                arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route2".to_string(),
21180            },
21181        ];
21182
21183        // Select first route
21184        app.apig_state.routes.selected = 0;
21185
21186        // Yank should copy ARN (we can't test clipboard directly, but we can verify the logic path)
21187        assert_eq!(
21188            app.apig_state.routes.items[0].arn,
21189            "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1"
21190        );
21191    }
21192
21193    #[test]
21194    fn test_apig_yank_ignores_empty_arn() {
21195        use crate::apig::api::RestApi;
21196        use crate::ui::apig::ApiDetailTab;
21197
21198        let mut app = test_app();
21199        app.current_service = Service::ApiGatewayApis;
21200        app.apig_state.current_api = Some(RestApi {
21201            id: "test123".to_string(),
21202            name: "Test API".to_string(),
21203            description: "Test".to_string(),
21204            created_date: "2024-01-01".to_string(),
21205            api_key_source: "HEADER".to_string(),
21206            endpoint_configuration: "REGIONAL".to_string(),
21207            protocol_type: "HTTP".to_string(),
21208            disable_execute_api_endpoint: false,
21209            status: "AVAILABLE".to_string(),
21210        });
21211        app.apig_state.detail_tab = ApiDetailTab::Routes;
21212
21213        // Add virtual parent node with empty ARN
21214        app.apig_state.routes.items = vec![Route {
21215            route_id: String::new(),
21216            route_key: "/users".to_string(),
21217            target: String::new(), // Virtual parent
21218            authorization_type: String::new(),
21219            api_key_required: false,
21220            display_name: "/users".to_string(),
21221            arn: String::new(), // Empty ARN
21222        }];
21223
21224        app.apig_state.routes.selected = 0;
21225
21226        // Verify empty ARN won't be copied
21227        assert!(
21228            app.apig_state.routes.items[0].arn.is_empty(),
21229            "Virtual parent should have empty ARN"
21230        );
21231    }
21232
21233    #[test]
21234    fn test_apig_route_column_toggle() {
21235        use crate::apig::api::RestApi;
21236        use crate::apig::route::Column as RouteColumn;
21237        use crate::keymap::Action;
21238        use crate::ui::apig::ApiDetailTab;
21239
21240        let mut app = test_app();
21241        app.current_service = Service::ApiGatewayApis;
21242        app.mode = Mode::ColumnSelector;
21243        app.apig_state.current_api = Some(RestApi {
21244            id: "test123".to_string(),
21245            name: "Test API".to_string(),
21246            description: "Test".to_string(),
21247            created_date: "2024-01-01".to_string(),
21248            api_key_source: "HEADER".to_string(),
21249            endpoint_configuration: "REGIONAL".to_string(),
21250            protocol_type: "HTTP".to_string(),
21251            disable_execute_api_endpoint: false,
21252            status: "AVAILABLE".to_string(),
21253        });
21254        app.apig_state.detail_tab = ApiDetailTab::Routes;
21255
21256        // Initially all columns visible
21257        assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21258
21259        // Try to toggle first column (Route) - should be locked, no effect
21260        app.column_selector_index = 1;
21261        app.handle_action(Action::ToggleColumn);
21262        assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21263        assert!(app
21264            .apig_route_visible_column_ids
21265            .contains(&RouteColumn::RouteKey.id()));
21266
21267        // Select ARN column (index 3 in UI)
21268        app.column_selector_index = 3;
21269        app.handle_action(Action::ToggleColumn);
21270
21271        // ARN should be removed
21272        assert_eq!(app.apig_route_visible_column_ids.len(), 4);
21273        assert!(!app
21274            .apig_route_visible_column_ids
21275            .contains(&RouteColumn::Arn.id()));
21276
21277        // Toggle again to add it back
21278        app.handle_action(Action::ToggleColumn);
21279        assert_eq!(app.apig_route_visible_column_ids.len(), 5);
21280        assert!(app
21281            .apig_route_visible_column_ids
21282            .contains(&RouteColumn::Arn.id()));
21283    }
21284
21285    #[test]
21286    fn test_apig_resource_column_toggle() {
21287        use crate::apig::api::RestApi;
21288        use crate::apig::resource::Column as ResourceColumn;
21289        use crate::keymap::Action;
21290        use crate::ui::apig::ApiDetailTab;
21291
21292        let mut app = test_app();
21293        app.current_service = Service::ApiGatewayApis;
21294        app.mode = Mode::ColumnSelector;
21295        app.apig_state.current_api = Some(RestApi {
21296            id: "test123".to_string(),
21297            name: "Test API".to_string(),
21298            description: "Test".to_string(),
21299            created_date: "2024-01-01".to_string(),
21300            api_key_source: "HEADER".to_string(),
21301            endpoint_configuration: "REGIONAL".to_string(),
21302            protocol_type: "REST".to_string(), // REST API shows resources
21303            disable_execute_api_endpoint: false,
21304            status: "AVAILABLE".to_string(),
21305        });
21306        app.apig_state.detail_tab = ApiDetailTab::Routes; // Resources shown in Routes tab for REST APIs
21307
21308        // Initially all columns visible
21309        assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21310
21311        // Try to toggle first column (Resource) - should be locked, no effect
21312        app.column_selector_index = 1;
21313        app.handle_action(Action::ToggleColumn);
21314        assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21315        assert!(app
21316            .apig_resource_visible_column_ids
21317            .contains(&ResourceColumn::Path.id()));
21318
21319        // Select ARN column (index 3 in UI)
21320        app.column_selector_index = 3;
21321        app.handle_action(Action::ToggleColumn);
21322
21323        // ARN should be removed
21324        assert_eq!(app.apig_resource_visible_column_ids.len(), 2);
21325        assert!(!app
21326            .apig_resource_visible_column_ids
21327            .contains(&ResourceColumn::Arn.id()));
21328
21329        // Toggle again to add it back
21330        app.handle_action(Action::ToggleColumn);
21331        assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
21332        assert!(app
21333            .apig_resource_visible_column_ids
21334            .contains(&ResourceColumn::Arn.id()));
21335    }
21336
21337    #[test]
21338    fn test_cloudtrail_filter_input() {
21339        let mut app = App::new_without_client("default".to_string(), None);
21340        app.service_selected = true;
21341        app.current_service = Service::CloudTrailEvents;
21342
21343        app.handle_action(Action::StartFilter);
21344        assert_eq!(app.mode, Mode::FilterInput);
21345
21346        app.cloudtrail_state.table.filter = "test".to_string();
21347
21348        assert_eq!(app.cloudtrail_state.table.filter, "test");
21349    }
21350
21351    #[test]
21352    fn test_cloudtrail_row_expansion() {
21353        let mut app = App::new_without_client("default".to_string(), None);
21354        app.service_selected = true;
21355        app.current_service = Service::CloudTrailEvents;
21356        app.cloudtrail_state.table.items = vec![CloudTrailEvent {
21357            event_name: "Event1".to_string(),
21358            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21359            username: "user1".to_string(),
21360            event_source: "s3.amazonaws.com".to_string(),
21361            resource_type: "Bucket".to_string(),
21362            resource_name: "my-bucket".to_string(),
21363            read_only: "false".to_string(),
21364            aws_region: "us-east-1".to_string(),
21365            event_id: "abc123".to_string(),
21366            access_key_id: "AKIA...".to_string(),
21367            source_ip_address: "1.2.3.4".to_string(),
21368            error_code: "".to_string(),
21369            request_id: "req-123".to_string(),
21370            event_type: "AwsApiCall".to_string(),
21371            cloud_trail_event_json: "{}".to_string(),
21372        }];
21373
21374        assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21375
21376        app.expand_row();
21377        assert_eq!(app.cloudtrail_state.table.expanded_item, Some(0));
21378
21379        app.collapse_row();
21380        assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21381    }
21382
21383    #[test]
21384    fn test_cloudtrail_service_initialization() {
21385        let app = App::new_without_client("default".to_string(), None);
21386        assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
21387        assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
21388    }
21389
21390    #[test]
21391    fn test_cloudtrail_service_name() {
21392        assert_eq!(
21393            Service::CloudTrailEvents.name(),
21394            "CloudTrail › Event History"
21395        );
21396    }
21397
21398    #[test]
21399    fn test_cloudtrail_in_service_picker() {
21400        let app = App::new_without_client("default".to_string(), None);
21401        assert!(app
21402            .service_picker
21403            .services
21404            .contains(&"CloudTrail › Event History"));
21405    }
21406
21407    #[test]
21408    fn test_cloudtrail_service_selection() {
21409        let mut app = App::new_without_client("default".to_string(), None);
21410        app.current_service = Service::CloudTrailEvents;
21411        app.service_selected = true;
21412
21413        assert_eq!(app.current_service, Service::CloudTrailEvents);
21414        assert!(app.service_selected);
21415    }
21416
21417    #[test]
21418    fn test_cloudtrail_filter_resets_selection() {
21419        let mut app = App::new_without_client("default".to_string(), None);
21420        app.service_selected = true;
21421        app.current_service = Service::CloudTrailEvents;
21422        app.cloudtrail_state.table.selected = 5;
21423        app.cloudtrail_state.table.expanded_item = Some(3);
21424
21425        app.handle_action(Action::StartFilter);
21426        app.apply_filter_operation(|_| {});
21427
21428        assert_eq!(app.cloudtrail_state.table.selected, 0);
21429        assert_eq!(app.cloudtrail_state.table.expanded_item, None);
21430    }
21431
21432    #[test]
21433    fn test_cloudtrail_column_toggle() {
21434        let mut app = App::new_without_client("default".to_string(), None);
21435        app.service_selected = true;
21436        app.current_service = Service::CloudTrailEvents;
21437        app.mode = Mode::ColumnSelector;
21438
21439        // Initially 6 columns visible
21440        assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
21441
21442        // Toggle column at index 7 (ReadOnly - 7th column, index 1-14)
21443        app.column_selector_index = 7;
21444        app.handle_action(Action::ToggleColumn);
21445
21446        // Should now have 7 visible columns
21447        assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 7);
21448    }
21449
21450    #[test]
21451    fn test_cloudtrail_tab_cycles_filter_focus() {
21452        let mut app = App::new_without_client("default".to_string(), None);
21453        app.service_selected = true;
21454        app.current_service = Service::CloudTrailEvents;
21455        app.mode = Mode::FilterInput;
21456        app.cloudtrail_state.input_focus = InputFocus::Filter;
21457
21458        // Tab should cycle to Pagination
21459        app.handle_action(Action::NextFilterFocus);
21460        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
21461
21462        // Tab again should cycle back to Filter
21463        app.handle_action(Action::NextFilterFocus);
21464        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
21465    }
21466
21467    #[test]
21468    fn test_cloudtrail_shift_tab_cycles_filter_focus() {
21469        let mut app = App::new_without_client("default".to_string(), None);
21470        app.service_selected = true;
21471        app.current_service = Service::CloudTrailEvents;
21472        app.mode = Mode::FilterInput;
21473        app.cloudtrail_state.input_focus = InputFocus::Filter;
21474
21475        // Shift+Tab should cycle to Pagination
21476        app.handle_action(Action::PrevFilterFocus);
21477        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
21478
21479        // Shift+Tab again should cycle back to Filter
21480        app.handle_action(Action::PrevFilterFocus);
21481        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
21482    }
21483
21484    #[test]
21485    fn test_cloudtrail_detail_view_tab_cycles_focus() {
21486        let mut app = App::new_without_client("default".to_string(), None);
21487        app.service_selected = true;
21488        app.current_service = Service::CloudTrailEvents;
21489        app.mode = Mode::Normal;
21490        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21491            event_name: "PutObject".to_string(),
21492            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21493            username: "user".to_string(),
21494            event_source: "s3.amazonaws.com".to_string(),
21495            resource_type: "AWS::S3::Bucket".to_string(),
21496            resource_name: "my-bucket".to_string(),
21497            read_only: "false".to_string(),
21498            aws_region: "us-east-1".to_string(),
21499            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21500            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21501            source_ip_address: "192.0.2.1".to_string(),
21502            error_code: "".to_string(),
21503            request_id: "req-123".to_string(),
21504            event_type: "AwsApiCall".to_string(),
21505            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21506        });
21507
21508        // Default focus is Resources
21509        assert_eq!(
21510            app.cloudtrail_state.detail_focus,
21511            CloudTrailDetailFocus::Resources
21512        );
21513
21514        // Tab cycles to EventRecord
21515        app.handle_action(Action::NextDetailTab);
21516        assert_eq!(
21517            app.cloudtrail_state.detail_focus,
21518            CloudTrailDetailFocus::EventRecord
21519        );
21520
21521        // Tab cycles back to Resources
21522        app.handle_action(Action::NextDetailTab);
21523        assert_eq!(
21524            app.cloudtrail_state.detail_focus,
21525            CloudTrailDetailFocus::Resources
21526        );
21527    }
21528
21529    #[test]
21530    fn test_cloudtrail_detail_view_shift_tab_cycles_focus() {
21531        let mut app = App::new_without_client("default".to_string(), None);
21532        app.service_selected = true;
21533        app.current_service = Service::CloudTrailEvents;
21534        app.mode = Mode::Normal;
21535        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21536            event_name: "PutObject".to_string(),
21537            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21538            username: "user".to_string(),
21539            event_source: "s3.amazonaws.com".to_string(),
21540            resource_type: "AWS::S3::Bucket".to_string(),
21541            resource_name: "my-bucket".to_string(),
21542            read_only: "false".to_string(),
21543            aws_region: "us-east-1".to_string(),
21544            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21545            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21546            source_ip_address: "192.0.2.1".to_string(),
21547            error_code: "".to_string(),
21548            request_id: "req-123".to_string(),
21549            event_type: "AwsApiCall".to_string(),
21550            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21551        });
21552
21553        // Default focus is Resources
21554        assert_eq!(
21555            app.cloudtrail_state.detail_focus,
21556            CloudTrailDetailFocus::Resources
21557        );
21558
21559        // Shift+Tab cycles to EventRecord
21560        app.handle_action(Action::PrevDetailTab);
21561        assert_eq!(
21562            app.cloudtrail_state.detail_focus,
21563            CloudTrailDetailFocus::EventRecord
21564        );
21565
21566        // Shift+Tab cycles back to Resources
21567        app.handle_action(Action::PrevDetailTab);
21568        assert_eq!(
21569            app.cloudtrail_state.detail_focus,
21570            CloudTrailDetailFocus::Resources
21571        );
21572    }
21573
21574    #[test]
21575    fn test_cloudtrail_json_scroll_with_arrow_keys() {
21576        let mut app = App::new_without_client("default".to_string(), None);
21577        app.service_selected = true;
21578        app.current_service = Service::CloudTrailEvents;
21579        app.mode = Mode::Normal;
21580        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21581            event_name: "PutObject".to_string(),
21582            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21583            username: "user".to_string(),
21584            event_source: "s3.amazonaws.com".to_string(),
21585            resource_type: "AWS::S3::Bucket".to_string(),
21586            resource_name: "my-bucket".to_string(),
21587            read_only: "false".to_string(),
21588            aws_region: "us-east-1".to_string(),
21589            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21590            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21591            source_ip_address: "192.0.2.1".to_string(),
21592            error_code: "".to_string(),
21593            request_id: "req-123".to_string(),
21594            event_type: "AwsApiCall".to_string(),
21595            cloud_trail_event_json: (0..50)
21596                .map(|i| format!("line {}", i))
21597                .collect::<Vec<_>>()
21598                .join("\n"),
21599        });
21600        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21601        app.cloudtrail_state.event_json_scroll = 0;
21602
21603        // Down arrow scrolls down
21604        app.handle_action(Action::NextItem);
21605        assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
21606
21607        app.handle_action(Action::NextItem);
21608        assert_eq!(app.cloudtrail_state.event_json_scroll, 2);
21609
21610        // Up arrow scrolls up
21611        app.handle_action(Action::PrevItem);
21612        assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
21613
21614        app.handle_action(Action::PrevItem);
21615        assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
21616
21617        // Up at 0 stays at 0
21618        app.handle_action(Action::PrevItem);
21619        assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
21620    }
21621
21622    #[test]
21623    fn test_cloudtrail_tab_works_with_no_resources() {
21624        let mut app = App::new_without_client("default".to_string(), None);
21625        app.service_selected = true;
21626        app.current_service = Service::CloudTrailEvents;
21627        app.mode = Mode::Normal;
21628        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21629            event_name: "PutObject".to_string(),
21630            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21631            username: "user".to_string(),
21632            event_source: "s3.amazonaws.com".to_string(),
21633            resource_type: "".to_string(), // No resource
21634            resource_name: "".to_string(), // No resource
21635            read_only: "false".to_string(),
21636            aws_region: "us-east-1".to_string(),
21637            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21638            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21639            source_ip_address: "192.0.2.1".to_string(),
21640            error_code: "".to_string(),
21641            request_id: "req-123".to_string(),
21642            event_type: "AwsApiCall".to_string(),
21643            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21644        });
21645        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21646
21647        // Tab cycles to Resources even with no resources
21648        app.handle_action(Action::NextDetailTab);
21649        assert_eq!(
21650            app.cloudtrail_state.detail_focus,
21651            CloudTrailDetailFocus::Resources
21652        );
21653
21654        // Tab cycles back to EventRecord
21655        app.handle_action(Action::NextDetailTab);
21656        assert_eq!(
21657            app.cloudtrail_state.detail_focus,
21658            CloudTrailDetailFocus::EventRecord
21659        );
21660    }
21661
21662    #[test]
21663    fn test_cloudtrail_resources_expand_collapse() {
21664        let mut app = App::new_without_client("default".to_string(), None);
21665        app.service_selected = true;
21666        app.current_service = Service::CloudTrailEvents;
21667        app.mode = Mode::Normal;
21668        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21669            event_name: "PutObject".to_string(),
21670            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21671            username: "user".to_string(),
21672            event_source: "s3.amazonaws.com".to_string(),
21673            resource_type: "AWS::S3::Bucket".to_string(),
21674            resource_name: "my-bucket".to_string(),
21675            read_only: "false".to_string(),
21676            aws_region: "us-east-1".to_string(),
21677            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21678            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21679            source_ip_address: "192.0.2.1".to_string(),
21680            error_code: "".to_string(),
21681            request_id: "req-123".to_string(),
21682            event_type: "AwsApiCall".to_string(),
21683            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21684        });
21685        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21686        app.cloudtrail_state.resources_expanded_index = None;
21687
21688        // Right arrow expands
21689        app.handle_action(Action::ExpandRow);
21690        assert_eq!(app.cloudtrail_state.resources_expanded_index, Some(0));
21691
21692        // Left arrow collapses
21693        app.handle_action(Action::CollapseRow);
21694        assert_eq!(app.cloudtrail_state.resources_expanded_index, None);
21695    }
21696
21697    #[test]
21698    fn test_cloudtrail_event_json_no_column_selector() {
21699        let mut app = App::new_without_client("default".to_string(), None);
21700        app.service_selected = true;
21701        app.current_service = Service::CloudTrailEvents;
21702        app.mode = Mode::Normal;
21703        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21704            event_name: "PutObject".to_string(),
21705            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21706            username: "user".to_string(),
21707            event_source: "s3.amazonaws.com".to_string(),
21708            resource_type: "AWS::S3::Bucket".to_string(),
21709            resource_name: "my-bucket".to_string(),
21710            read_only: "false".to_string(),
21711            aws_region: "us-east-1".to_string(),
21712            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21713            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21714            source_ip_address: "192.0.2.1".to_string(),
21715            error_code: "".to_string(),
21716            request_id: "req-123".to_string(),
21717            event_type: "AwsApiCall".to_string(),
21718            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21719        });
21720        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
21721
21722        // 'p' should not open column selector on Event JSON
21723        app.handle_action(Action::OpenColumnSelector);
21724        assert_eq!(app.mode, Mode::Normal);
21725
21726        // But should work on Resources
21727        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21728        app.handle_action(Action::OpenColumnSelector);
21729        assert_eq!(app.mode, Mode::ColumnSelector);
21730    }
21731
21732    #[test]
21733    fn test_cloudtrail_resources_preferences_show_only_3_columns() {
21734        let mut app = App::new_without_client("default".to_string(), None);
21735        app.service_selected = true;
21736        app.current_service = Service::CloudTrailEvents;
21737        app.mode = Mode::ColumnSelector;
21738        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21739            event_name: "PutObject".to_string(),
21740            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21741            username: "user".to_string(),
21742            event_source: "s3.amazonaws.com".to_string(),
21743            resource_type: "AWS::S3::Bucket".to_string(),
21744            resource_name: "my-bucket".to_string(),
21745            read_only: "false".to_string(),
21746            aws_region: "us-east-1".to_string(),
21747            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21748            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21749            source_ip_address: "192.0.2.1".to_string(),
21750            error_code: "".to_string(),
21751            request_id: "req-123".to_string(),
21752            event_type: "AwsApiCall".to_string(),
21753            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21754        });
21755        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21756
21757        // Should show 3 columns
21758        assert_eq!(app.get_column_count(), 3);
21759        assert_eq!(app.get_column_selector_max(), 3);
21760
21761        // All 3 columns should be visible by default
21762        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 3);
21763
21764        // Toggle first column off
21765        app.column_selector_index = 1;
21766        app.handle_action(Action::ToggleColumn);
21767        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
21768
21769        // Toggle second column off
21770        app.column_selector_index = 2;
21771        app.handle_action(Action::ToggleColumn);
21772        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
21773
21774        // Try to toggle last column off - should NOT work (at least 1 must remain)
21775        app.column_selector_index = 3;
21776        app.handle_action(Action::ToggleColumn);
21777        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
21778
21779        // Toggle first column back on
21780        app.column_selector_index = 1;
21781        app.handle_action(Action::ToggleColumn);
21782        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
21783    }
21784
21785    #[test]
21786    fn test_cloudtrail_resources_preferences_tab_cycles() {
21787        let mut app = App::new_without_client("default".to_string(), None);
21788        app.service_selected = true;
21789        app.current_service = Service::CloudTrailEvents;
21790        app.mode = Mode::ColumnSelector;
21791        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
21792            event_name: "PutObject".to_string(),
21793            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21794            username: "user".to_string(),
21795            event_source: "s3.amazonaws.com".to_string(),
21796            resource_type: "AWS::S3::Bucket".to_string(),
21797            resource_name: "my-bucket".to_string(),
21798            read_only: "false".to_string(),
21799            aws_region: "us-east-1".to_string(),
21800            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
21801            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
21802            source_ip_address: "192.0.2.1".to_string(),
21803            error_code: "".to_string(),
21804            request_id: "req-123".to_string(),
21805            event_type: "AwsApiCall".to_string(),
21806            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
21807        });
21808        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
21809        app.column_selector_index = 1;
21810
21811        // Tab wraps to start (no page size section)
21812        app.handle_action(Action::NextPreferences);
21813        assert_eq!(app.column_selector_index, 0);
21814
21815        // Shift+Tab wraps to start (no page size section)
21816        app.column_selector_index = 1;
21817        app.handle_action(Action::PrevPreferences);
21818        assert_eq!(app.column_selector_index, 0);
21819    }
21820
21821    #[test]
21822    fn test_cloudtrail_resources_height_stays_constant_when_expanding() {
21823        // This test verifies that table height is pre-allocated for expansion
21824        // so content doesn't jump around when expanding/collapsing
21825
21826        // Height formula: (n_rows + n_visible_cols - 1) + 1 table header + 2 borders + 1 title
21827        // For 1 row with 3 visible columns: (1 + 3 - 1) + 1 + 2 + 1 = 7
21828
21829        // With 3 visible columns
21830        let visible_cols_3 = 3;
21831        let height_3 = (1 + visible_cols_3 - 1 + 1 + 2 + 1) as u16;
21832        assert_eq!(height_3, 7);
21833
21834        // With 2 visible columns (one toggled off)
21835        let visible_cols_2 = 2;
21836        let height_2 = (1 + visible_cols_2 - 1 + 1 + 2 + 1) as u16;
21837        assert_eq!(height_2, 6);
21838
21839        // With 1 visible column (two toggled off)
21840        let visible_cols_1 = 1;
21841        let height_1 = (1 + visible_cols_1 - 1 + 1 + 2 + 1) as u16;
21842        assert_eq!(height_1, 5);
21843    }
21844
21845    #[test]
21846    fn test_cloudtrail_default_focus_is_resources() {
21847        let app = App::new_without_client("default".to_string(), None);
21848        assert_eq!(
21849            app.cloudtrail_state.detail_focus,
21850            CloudTrailDetailFocus::Resources
21851        );
21852    }
21853
21854    fn setup_cloudtrail_pagination_test() -> App {
21855        let mut app = App::new_without_client("default".to_string(), None);
21856        app.service_selected = true;
21857        app.current_service = Service::CloudTrailEvents;
21858        app.mode = Mode::FilterInput;
21859        app.cloudtrail_state.input_focus = InputFocus::Pagination;
21860        app.cloudtrail_state.table.page_size = PageSize::Ten;
21861        app.cloudtrail_state.table.items = (0..25)
21862            .map(|i| CloudTrailEvent {
21863                event_name: format!("Event{}", i),
21864                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21865                username: "user".to_string(),
21866                event_source: "s3.amazonaws.com".to_string(),
21867                resource_type: "Bucket".to_string(),
21868                resource_name: "bucket".to_string(),
21869                read_only: "false".to_string(),
21870                aws_region: "us-east-1".to_string(),
21871                event_id: "id".to_string(),
21872                access_key_id: "key".to_string(),
21873                source_ip_address: "1.2.3.4".to_string(),
21874                error_code: "".to_string(),
21875                request_id: "req".to_string(),
21876                event_type: "AwsApiCall".to_string(),
21877                cloud_trail_event_json: "{}".to_string(),
21878            })
21879            .collect();
21880        app
21881    }
21882
21883    #[test]
21884    fn test_cloudtrail_pagination_navigation() {
21885        // Test right arrow navigation
21886        let mut app = setup_cloudtrail_pagination_test();
21887        assert_eq!(app.cloudtrail_state.table.selected, 0);
21888        app.handle_action(Action::NextItem);
21889        assert_eq!(app.cloudtrail_state.table.selected, 10);
21890        app.handle_action(Action::NextItem);
21891        assert_eq!(app.cloudtrail_state.table.selected, 20);
21892        app.handle_action(Action::NextItem);
21893        assert_eq!(app.cloudtrail_state.table.selected, 20);
21894
21895        // Test left arrow navigation
21896        app.handle_action(Action::PrevItem);
21897        assert_eq!(app.cloudtrail_state.table.selected, 10);
21898        app.handle_action(Action::PrevItem);
21899        assert_eq!(app.cloudtrail_state.table.selected, 0);
21900        app.handle_action(Action::PrevItem);
21901        assert_eq!(app.cloudtrail_state.table.selected, 0);
21902    }
21903
21904    #[test]
21905    fn test_cloudtrail_pagination_navigation_right_arrow() {
21906        let mut app = setup_cloudtrail_pagination_test();
21907        app.current_service = Service::CloudTrailEvents;
21908        app.mode = Mode::FilterInput;
21909        app.cloudtrail_state.input_focus = InputFocus::Pagination;
21910        app.cloudtrail_state.table.page_size = PageSize::Ten;
21911
21912        // Create 25 items (3 pages)
21913        app.cloudtrail_state.table.items = (0..25)
21914            .map(|i| CloudTrailEvent {
21915                event_name: format!("Event{}", i),
21916                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21917                username: "user".to_string(),
21918                event_source: "s3.amazonaws.com".to_string(),
21919                resource_type: "Bucket".to_string(),
21920                resource_name: "bucket".to_string(),
21921                read_only: "false".to_string(),
21922                aws_region: "us-east-1".to_string(),
21923                event_id: "id".to_string(),
21924                access_key_id: "key".to_string(),
21925                source_ip_address: "1.2.3.4".to_string(),
21926                error_code: "".to_string(),
21927                request_id: "req".to_string(),
21928                event_type: "AwsApiCall".to_string(),
21929                cloud_trail_event_json: "{}".to_string(),
21930            })
21931            .collect();
21932
21933        // Start at page 0
21934        assert_eq!(app.cloudtrail_state.table.selected, 0);
21935
21936        // Right arrow should go to page 1
21937        app.handle_action(Action::NextItem);
21938        assert_eq!(app.cloudtrail_state.table.selected, 10);
21939
21940        // Right arrow should go to page 2
21941        app.handle_action(Action::NextItem);
21942        assert_eq!(app.cloudtrail_state.table.selected, 20);
21943
21944        // Right arrow at last page should stay
21945        app.handle_action(Action::NextItem);
21946        assert_eq!(app.cloudtrail_state.table.selected, 20);
21947    }
21948
21949    #[test]
21950    fn test_cloudtrail_pagination_navigation_left_arrow() {
21951        let mut app = App::new_without_client("default".to_string(), None);
21952        app.service_selected = true;
21953        app.current_service = Service::CloudTrailEvents;
21954        app.mode = Mode::FilterInput;
21955        app.cloudtrail_state.input_focus = InputFocus::Pagination;
21956        app.cloudtrail_state.table.page_size = PageSize::Ten;
21957
21958        // Create 25 items (3 pages)
21959        app.cloudtrail_state.table.items = (0..25)
21960            .map(|i| CloudTrailEvent {
21961                event_name: format!("Event{}", i),
21962                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
21963                username: "user".to_string(),
21964                event_source: "s3.amazonaws.com".to_string(),
21965                resource_type: "Bucket".to_string(),
21966                resource_name: "bucket".to_string(),
21967                read_only: "false".to_string(),
21968                aws_region: "us-east-1".to_string(),
21969                event_id: "id".to_string(),
21970                access_key_id: "key".to_string(),
21971                source_ip_address: "1.2.3.4".to_string(),
21972                error_code: "".to_string(),
21973                request_id: "req".to_string(),
21974                event_type: "AwsApiCall".to_string(),
21975                cloud_trail_event_json: "{}".to_string(),
21976            })
21977            .collect();
21978
21979        // Start at page 2
21980        app.cloudtrail_state.table.selected = 20;
21981
21982        // Left arrow should go to page 1
21983        app.handle_action(Action::PrevItem);
21984        assert_eq!(app.cloudtrail_state.table.selected, 10);
21985
21986        // Left arrow should go to page 0
21987        app.handle_action(Action::PrevItem);
21988        assert_eq!(app.cloudtrail_state.table.selected, 0);
21989
21990        // Left arrow at first page should stay
21991        app.handle_action(Action::PrevItem);
21992        assert_eq!(app.cloudtrail_state.table.selected, 0);
21993    }
21994
21995    #[test]
21996    fn test_cloudtrail_arrow_navigation() {
21997        let mut app = App::new_without_client("default".to_string(), None);
21998        app.service_selected = true;
21999        app.current_service = Service::CloudTrailEvents;
22000        app.mode = Mode::Normal;
22001
22002        // Create 5 items
22003        app.cloudtrail_state.table.items = (0..5)
22004            .map(|i| CloudTrailEvent {
22005                event_name: format!("Event{}", i),
22006                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22007                username: "user".to_string(),
22008                event_source: "s3.amazonaws.com".to_string(),
22009                resource_type: "Bucket".to_string(),
22010                resource_name: "bucket".to_string(),
22011                read_only: "false".to_string(),
22012                aws_region: "us-east-1".to_string(),
22013                event_id: "id".to_string(),
22014                access_key_id: "key".to_string(),
22015                source_ip_address: "1.2.3.4".to_string(),
22016                error_code: "".to_string(),
22017                request_id: "req".to_string(),
22018                event_type: "AwsApiCall".to_string(),
22019                cloud_trail_event_json: "{}".to_string(),
22020            })
22021            .collect();
22022
22023        // Down arrow should move to next item
22024        app.handle_action(Action::NextItem);
22025        assert_eq!(app.cloudtrail_state.table.selected, 1);
22026
22027        app.handle_action(Action::NextItem);
22028        assert_eq!(app.cloudtrail_state.table.selected, 2);
22029
22030        // Up arrow should move to previous item
22031        app.handle_action(Action::PrevItem);
22032        assert_eq!(app.cloudtrail_state.table.selected, 1);
22033    }
22034
22035    #[test]
22036    fn test_cloudtrail_page_down_navigation() {
22037        let mut app = App::new_without_client("default".to_string(), None);
22038        app.service_selected = true;
22039        app.current_service = Service::CloudTrailEvents;
22040        app.mode = Mode::Normal;
22041        app.cloudtrail_state.table.page_size = PageSize::Ten;
22042
22043        // Create 25 items
22044        app.cloudtrail_state.table.items = (0..25)
22045            .map(|i| CloudTrailEvent {
22046                event_name: format!("Event{}", i),
22047                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22048                username: "user".to_string(),
22049                event_source: "s3.amazonaws.com".to_string(),
22050                resource_type: "Bucket".to_string(),
22051                resource_name: "bucket".to_string(),
22052                read_only: "false".to_string(),
22053                aws_region: "us-east-1".to_string(),
22054                event_id: "id".to_string(),
22055                access_key_id: "key".to_string(),
22056                source_ip_address: "1.2.3.4".to_string(),
22057                error_code: "".to_string(),
22058                request_id: "req".to_string(),
22059                event_type: "AwsApiCall".to_string(),
22060                cloud_trail_event_json: "{}".to_string(),
22061            })
22062            .collect();
22063
22064        // Ctrl+D should jump down a page
22065        app.handle_action(Action::PageDown);
22066        assert_eq!(app.cloudtrail_state.table.selected, 10);
22067
22068        app.handle_action(Action::PageDown);
22069        assert_eq!(app.cloudtrail_state.table.selected, 20);
22070    }
22071
22072    #[test]
22073    fn test_cloudtrail_page_up_navigation() {
22074        let mut app = App::new_without_client("default".to_string(), None);
22075        app.service_selected = true;
22076        app.current_service = Service::CloudTrailEvents;
22077        app.mode = Mode::Normal;
22078        app.cloudtrail_state.table.page_size = PageSize::Ten;
22079
22080        // Create 25 items
22081        app.cloudtrail_state.table.items = (0..25)
22082            .map(|i| CloudTrailEvent {
22083                event_name: format!("Event{}", i),
22084                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22085                username: "user".to_string(),
22086                event_source: "s3.amazonaws.com".to_string(),
22087                resource_type: "Bucket".to_string(),
22088                resource_name: "bucket".to_string(),
22089                read_only: "false".to_string(),
22090                aws_region: "us-east-1".to_string(),
22091                event_id: "id".to_string(),
22092                access_key_id: "key".to_string(),
22093                source_ip_address: "1.2.3.4".to_string(),
22094                error_code: "".to_string(),
22095                request_id: "req".to_string(),
22096                event_type: "AwsApiCall".to_string(),
22097                cloud_trail_event_json: "{}".to_string(),
22098            })
22099            .collect();
22100
22101        // Start at page 2
22102        app.cloudtrail_state.table.selected = 20;
22103
22104        // Ctrl+U should jump up a page
22105        app.handle_action(Action::PageUp);
22106        assert_eq!(app.cloudtrail_state.table.selected, 10);
22107
22108        app.handle_action(Action::PageUp);
22109        assert_eq!(app.cloudtrail_state.table.selected, 0);
22110    }
22111
22112    #[test]
22113    fn test_cloudtrail_page_size_change_updates_display() {
22114        let mut app = App::new_without_client("default".to_string(), None);
22115        app.service_selected = true;
22116        app.current_service = Service::CloudTrailEvents;
22117        app.mode = Mode::ColumnSelector;
22118
22119        // Create 50 items
22120        app.cloudtrail_state.table.items = (0..50)
22121            .map(|i| CloudTrailEvent {
22122                event_name: format!("Event{}", i),
22123                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22124                username: "user".to_string(),
22125                event_source: "s3.amazonaws.com".to_string(),
22126                resource_type: "Bucket".to_string(),
22127                resource_name: "bucket".to_string(),
22128                read_only: "false".to_string(),
22129                aws_region: "us-east-1".to_string(),
22130                event_id: "id".to_string(),
22131                access_key_id: "key".to_string(),
22132                source_ip_address: "1.2.3.4".to_string(),
22133                error_code: "".to_string(),
22134                request_id: "req".to_string(),
22135                event_type: "AwsApiCall".to_string(),
22136                cloud_trail_event_json: "{}".to_string(),
22137            })
22138            .collect();
22139
22140        // Default page size is 50
22141        assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Fifty);
22142
22143        // Change to page size 10 (index 17)
22144        app.column_selector_index = 17;
22145        app.handle_action(Action::ToggleColumn);
22146
22147        assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
22148        // snap_to_page should have been called to adjust display
22149    }
22150
22151    #[test]
22152    fn test_cloudtrail_all_columns_toggleable() {
22153        let mut app = App::new_without_client("default".to_string(), None);
22154        app.service_selected = true;
22155        app.current_service = Service::CloudTrailEvents;
22156        app.mode = Mode::ColumnSelector;
22157
22158        // Verify we have 14 columns
22159        assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
22160
22161        // Test toggling each column (indices 1-14)
22162        // Note: Cannot toggle off the last visible column
22163        for idx in 1..=14 {
22164            app.column_selector_index = idx;
22165            let initial_visible = app.cloudtrail_event_visible_column_ids.clone();
22166            let initial_count = initial_visible.len();
22167
22168            // Check if this column is currently visible
22169            let col = app.cloudtrail_event_column_ids.get(idx - 1).unwrap();
22170            let is_visible = initial_visible.contains(col);
22171
22172            app.handle_action(Action::ToggleColumn);
22173
22174            // If it was the last visible column, it should not be removed
22175            if is_visible && initial_count == 1 {
22176                assert_eq!(
22177                    app.cloudtrail_event_visible_column_ids, initial_visible,
22178                    "Last visible column at index {} should not be toggleable",
22179                    idx
22180                );
22181            } else {
22182                // Otherwise it should toggle
22183                assert_ne!(
22184                    app.cloudtrail_event_visible_column_ids, initial_visible,
22185                    "Column at index {} should be toggleable when not the last one",
22186                    idx
22187                );
22188            }
22189        }
22190    }
22191
22192    #[test]
22193    fn test_cloudtrail_readonly_column_toggleable() {
22194        use crate::cloudtrail::events::CloudTrailEventColumn;
22195
22196        let mut app = App::new_without_client("default".to_string(), None);
22197        app.service_selected = true;
22198        app.current_service = Service::CloudTrailEvents;
22199        app.mode = Mode::ColumnSelector;
22200
22201        // ReadOnly is at position 6 (0-indexed) in the all() array
22202        // So it's at index 7 in the column selector (1-indexed)
22203        let readonly_id = CloudTrailEventColumn::ReadOnly.id();
22204
22205        // Verify ReadOnly is in the column list
22206        assert!(app.cloudtrail_event_column_ids.contains(&readonly_id));
22207
22208        // Initially not visible
22209        assert!(!app
22210            .cloudtrail_event_visible_column_ids
22211            .contains(&readonly_id));
22212
22213        // Toggle it on (index 7)
22214        app.column_selector_index = 7;
22215        app.handle_action(Action::ToggleColumn);
22216
22217        // Should now be visible
22218        assert!(
22219            app.cloudtrail_event_visible_column_ids
22220                .contains(&readonly_id),
22221            "ReadOnly column should be toggleable at index 7"
22222        );
22223
22224        // Toggle it off
22225        app.handle_action(Action::ToggleColumn);
22226
22227        // Should be hidden again
22228        assert!(!app
22229            .cloudtrail_event_visible_column_ids
22230            .contains(&readonly_id));
22231    }
22232
22233    #[test]
22234    fn test_cloudtrail_pagination_limits_displayed_items() {
22235        let mut app = App::new_without_client("default".to_string(), None);
22236        app.service_selected = true;
22237        app.current_service = Service::CloudTrailEvents;
22238
22239        // Create 50 items
22240        app.cloudtrail_state.table.items = (0..50)
22241            .map(|i| CloudTrailEvent {
22242                event_name: format!("Event{}", i),
22243                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22244                username: "user".to_string(),
22245                event_source: "s3.amazonaws.com".to_string(),
22246                resource_type: "Bucket".to_string(),
22247                resource_name: "bucket".to_string(),
22248                read_only: "false".to_string(),
22249                aws_region: "us-east-1".to_string(),
22250                event_id: "id".to_string(),
22251                access_key_id: "key".to_string(),
22252                source_ip_address: "1.2.3.4".to_string(),
22253                error_code: "".to_string(),
22254                request_id: "req".to_string(),
22255                event_type: "AwsApiCall".to_string(),
22256                cloud_trail_event_json: "{}".to_string(),
22257            })
22258            .collect();
22259
22260        // Set page size to 10
22261        app.cloudtrail_state.table.page_size = PageSize::Ten;
22262
22263        // Render to a test backend to verify pagination
22264        use ratatui::backend::TestBackend;
22265        use ratatui::Terminal;
22266
22267        let backend = TestBackend::new(100, 30);
22268        let mut terminal = Terminal::new(backend).unwrap();
22269
22270        terminal
22271            .draw(|frame| {
22272                let area = frame.area();
22273                crate::ui::cloudtrail::render_events(frame, &app, area);
22274            })
22275            .unwrap();
22276
22277        // The rendering should only show 10 items per page
22278        // This is verified by the pagination logic in render_events
22279        assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
22280    }
22281
22282    #[test]
22283    fn test_cloudtrail_readonly_column_selectable_in_preferences() {
22284        let mut app = App::new_without_client("default".to_string(), None);
22285        app.service_selected = true;
22286        app.current_service = Service::CloudTrailEvents;
22287        app.mode = Mode::ColumnSelector;
22288
22289        // Verify we have 14 columns
22290        assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
22291
22292        // Navigate to Read-only column (index 7)
22293        app.column_selector_index = 7;
22294
22295        // Verify we can select it by checking the index is valid
22296        let max_index = app.get_column_selector_max();
22297        assert!(
22298            app.column_selector_index <= max_index,
22299            "Read-only column index {} should be <= max {}",
22300            app.column_selector_index,
22301            max_index
22302        );
22303
22304        // Verify the column exists at this index
22305        let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
22306        let col = CloudTrailEventColumn::from_id(col_id);
22307        assert!(col.is_some(), "Column should exist at index 7");
22308        assert_eq!(
22309            col.unwrap().default_name(),
22310            "Read-only",
22311            "Column at index 7 should be Read-only"
22312        );
22313    }
22314
22315    #[test]
22316    fn test_cloudtrail_navigate_to_readonly_column() {
22317        let mut app = App::new_without_client("default".to_string(), None);
22318        app.service_selected = true;
22319        app.current_service = Service::CloudTrailEvents;
22320        app.mode = Mode::ColumnSelector;
22321        app.column_selector_index = 1; // Start at first column
22322
22323        // Check column IDs are initialized
22324        println!(
22325            "cloudtrail_event_column_ids.len() = {}",
22326            app.cloudtrail_event_column_ids.len()
22327        );
22328        assert_eq!(
22329            app.cloudtrail_event_column_ids.len(),
22330            14,
22331            "Should have 14 columns"
22332        );
22333
22334        // Check blank row index
22335        let column_count = app.get_column_count();
22336        println!("Column count from get_column_count(): {}", column_count);
22337        assert_eq!(column_count, 14, "Column count should be 14");
22338
22339        // The blank row should be at index 15 (14 columns + 1)
22340        assert!(!app.is_blank_row_index(7), "Index 7 should NOT be blank");
22341        assert!(app.is_blank_row_index(15), "Index 15 should be blank");
22342
22343        // Navigate down to Read-only column (index 7)
22344        for _ in 0..6 {
22345            app.handle_action(Action::NextItem);
22346        }
22347
22348        assert_eq!(
22349            app.column_selector_index, 7,
22350            "Should navigate to Read-only column at index 7"
22351        );
22352
22353        // Verify it's the Read-only column
22354        let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
22355        let col = CloudTrailEventColumn::from_id(col_id).unwrap();
22356        assert_eq!(col.default_name(), "Read-only");
22357    }
22358
22359    #[test]
22360    fn test_cloudtrail_navigate_beyond_loaded_items() {
22361        let mut app = App::new_without_client("default".to_string(), None);
22362        app.service_selected = true;
22363        app.current_service = Service::CloudTrailEvents;
22364
22365        // Load only 50 items
22366        app.cloudtrail_state.table.items = (0..50)
22367            .map(|i| CloudTrailEvent {
22368                event_name: format!("Event{}", i),
22369                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22370                username: "user".to_string(),
22371                event_source: "s3.amazonaws.com".to_string(),
22372                resource_type: "Bucket".to_string(),
22373                resource_name: "bucket".to_string(),
22374                read_only: "false".to_string(),
22375                aws_region: "us-east-1".to_string(),
22376                event_id: "id".to_string(),
22377                access_key_id: "key".to_string(),
22378                source_ip_address: "1.2.3.4".to_string(),
22379                error_code: "".to_string(),
22380                request_id: "req".to_string(),
22381                event_type: "AwsApiCall".to_string(),
22382                cloud_trail_event_json: "{}".to_string(),
22383            })
22384            .collect();
22385
22386        // Page size is 50 by default, so we have 1 page of data
22387        // Try to navigate to page 10 (beyond loaded data)
22388        let initial_selected = app.cloudtrail_state.table.selected;
22389        app.page_input = "10".to_string();
22390        app.go_to_page(10);
22391
22392        // Should do nothing (stay at initial position)
22393        assert_eq!(
22394            app.cloudtrail_state.table.selected, initial_selected,
22395            "Should ignore navigation to page 10 when only 1 page is loaded"
22396        );
22397
22398        // But page 2 should work (loaded + 1)
22399        app.go_to_page(2);
22400        let page_size = app.cloudtrail_state.table.page_size.value();
22401        assert_eq!(
22402            app.cloudtrail_state.table.selected, page_size,
22403            "Should allow navigation to page 2 (loaded + 1)"
22404        );
22405    }
22406
22407    #[test]
22408    fn test_cloudtrail_page_change_resets_expansion() {
22409        let mut app = App::new_without_client("default".to_string(), None);
22410        app.service_selected = true;
22411        app.current_service = Service::CloudTrailEvents;
22412
22413        // Create 100 items (2 pages with page size 50)
22414        app.cloudtrail_state.table.items = (0..100)
22415            .map(|i| CloudTrailEvent {
22416                event_name: format!("Event{}", i),
22417                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
22418                username: "user".to_string(),
22419                event_source: "s3.amazonaws.com".to_string(),
22420                resource_type: "Bucket".to_string(),
22421                resource_name: "bucket".to_string(),
22422                read_only: "false".to_string(),
22423                aws_region: "us-east-1".to_string(),
22424                event_id: "id".to_string(),
22425                access_key_id: "key".to_string(),
22426                source_ip_address: "1.2.3.4".to_string(),
22427                error_code: "".to_string(),
22428                request_id: "req".to_string(),
22429                event_type: "AwsApiCall".to_string(),
22430                cloud_trail_event_json: "{}".to_string(),
22431            })
22432            .collect();
22433
22434        // Expand an item on page 1
22435        app.cloudtrail_state.table.expanded_item = Some(5);
22436        assert_eq!(app.cloudtrail_state.table.expanded_item, Some(5));
22437
22438        // Navigate to page 2
22439        app.go_to_page(2);
22440
22441        // Expansion should be reset
22442        assert_eq!(
22443            app.cloudtrail_state.table.expanded_item, None,
22444            "Page change should reset expanded_item"
22445        );
22446    }
22447
22448    #[test]
22449    fn test_lambda_application_resources_have_columns() {
22450        let app = test_app();
22451
22452        // Verify lambda resource columns are initialized correctly
22453        assert_eq!(app.lambda_resource_visible_column_ids.len(), 4);
22454        assert_eq!(app.lambda_resource_column_ids.len(), 4);
22455
22456        // Verify they contain the expected Lambda ResourceColumn IDs
22457        assert!(app
22458            .lambda_resource_visible_column_ids
22459            .contains(&"column.lambda.resource.logical_id"));
22460        assert!(app
22461            .lambda_resource_column_ids
22462            .contains(&"column.lambda.resource.logical_id"));
22463    }
22464
22465    #[test]
22466    fn test_lambda_functions_list_right_expands_left_collapses() {
22467        use crate::lambda::Function;
22468        let mut app = test_app();
22469        app.mode = Mode::Normal;
22470        app.current_service = Service::LambdaFunctions;
22471        app.service_selected = true;
22472        app.lambda_state.table.items = vec![Function {
22473            name: "fn1".to_string(),
22474            arn: "arn::fn1".to_string(),
22475            application: None,
22476            description: String::new(),
22477            package_type: String::new(),
22478            runtime: "python3.12".to_string(),
22479            architecture: String::new(),
22480            code_size: 0,
22481            code_sha256: String::new(),
22482            memory_mb: 128,
22483            timeout_seconds: 3,
22484            last_modified: String::new(),
22485            layers: vec![],
22486        }];
22487        app.lambda_state.table.selected = 0;
22488
22489        // Right arrow should expand
22490        app.handle_action(Action::NextPane);
22491        assert_eq!(app.lambda_state.table.expanded_item, Some(0));
22492
22493        // Right arrow again should NOT collapse (stays expanded)
22494        app.handle_action(Action::NextPane);
22495        assert_eq!(app.lambda_state.table.expanded_item, Some(0));
22496
22497        // Left arrow should collapse
22498        app.handle_action(Action::CollapseRow);
22499        assert_eq!(app.lambda_state.table.expanded_item, None);
22500    }
22501
22502    #[test]
22503    fn test_lambda_applications_list_right_expands_left_collapses() {
22504        use crate::lambda::Application;
22505        let mut app = test_app();
22506        app.mode = Mode::Normal;
22507        app.current_service = Service::LambdaApplications;
22508        app.service_selected = true;
22509        app.lambda_application_state.table.items = vec![Application {
22510            name: "my-app".to_string(),
22511            arn: String::new(),
22512            description: String::new(),
22513            status: "ACTIVE".to_string(),
22514            last_modified: String::new(),
22515        }];
22516        app.lambda_application_state.table.selected = 0;
22517
22518        // Right arrow should expand
22519        app.handle_action(Action::NextPane);
22520        assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
22521
22522        // Right arrow again should NOT collapse (stays expanded)
22523        app.handle_action(Action::NextPane);
22524        assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
22525
22526        // Left arrow should collapse
22527        app.handle_action(Action::CollapseRow);
22528        assert_eq!(app.lambda_application_state.table.expanded_item, None);
22529    }
22530
22531    #[test]
22532    fn test_lambda_application_enter_on_deployments_tab_does_not_switch_to_overview() {
22533        use crate::lambda::Application;
22534        let mut app = test_app();
22535        app.mode = Mode::Normal;
22536        app.current_service = Service::LambdaApplications;
22537        app.service_selected = true;
22538        app.lambda_application_state.table.items = vec![Application {
22539            name: "my-app".to_string(),
22540            arn: String::new(),
22541            description: String::new(),
22542            status: "ACTIVE".to_string(),
22543            last_modified: String::new(),
22544        }];
22545
22546        // Select the application (Enter on list)
22547        app.handle_action(Action::Select);
22548        assert!(app.lambda_application_state.current_application.is_some());
22549
22550        // Switch to Deployments tab
22551        use crate::ui::lambda::ApplicationDetailTab as LambdaApplicationDetailTab;
22552        app.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Deployments;
22553        assert_eq!(
22554            app.lambda_application_state.detail_tab,
22555            LambdaApplicationDetailTab::Deployments
22556        );
22557
22558        // Enter on Deployments tab must NOT switch back to Overview
22559        app.handle_action(Action::Select);
22560        assert_eq!(
22561            app.lambda_application_state.detail_tab,
22562            LambdaApplicationDetailTab::Deployments
22563        );
22564    }
22565
22566    #[test]
22567    fn test_lambda_applications_pagination_focus_left_right_jump_pages() {
22568        use crate::lambda::Application;
22569        let mut app = test_app();
22570        app.current_service = Service::LambdaApplications;
22571        app.service_selected = true;
22572        app.mode = Mode::FilterInput;
22573        app.lambda_application_state.input_focus = InputFocus::Pagination;
22574
22575        // 100 apps, page size 50 — 2 pages
22576        app.lambda_application_state.table.items = (0..100)
22577            .map(|i| Application {
22578                name: format!("app{:03}", i),
22579                arn: String::new(),
22580                description: String::new(),
22581                status: "ACTIVE".to_string(),
22582                last_modified: String::new(),
22583            })
22584            .collect();
22585        app.lambda_application_state.table.selected = 0;
22586
22587        // Right arrow (PageDown) must jump to page 2
22588        app.handle_action(Action::PageDown);
22589        assert_eq!(
22590            app.lambda_application_state.table.selected, 50,
22591            "Right arrow with pagination focus should jump to page 2"
22592        );
22593
22594        // Left arrow (PageUp) must jump back to page 1
22595        app.handle_action(Action::PageUp);
22596        assert_eq!(
22597            app.lambda_application_state.table.selected, 0,
22598            "Left arrow with pagination focus should jump back to page 1"
22599        );
22600
22601        // Up/Down arrows must NOT move table selection when pagination is focused
22602        app.handle_action(Action::NextItem);
22603        assert_eq!(
22604            app.lambda_application_state.table.selected, 0,
22605            "Down arrow must not move table when pagination is focused"
22606        );
22607        app.lambda_application_state.table.selected = 5;
22608        app.handle_action(Action::PrevItem);
22609        assert_eq!(
22610            app.lambda_application_state.table.selected, 5,
22611            "Up arrow must not move table when pagination is focused"
22612        );
22613    }
22614
22615    #[test]
22616    fn test_cloudwatch_alarms_enter_drills_down() {
22617        let mut app = test_app();
22618        app.mode = Mode::Normal;
22619        app.current_service = Service::CloudWatchAlarms;
22620        app.service_selected = true;
22621        app.view_mode = ViewMode::List;
22622
22623        // Add test alarm
22624        app.alarms_state.table.items = vec![Alarm {
22625            name: "test-alarm".to_string(),
22626            state: "ALARM".to_string(),
22627            state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
22628            description: "Test alarm".to_string(),
22629            metric_name: "CPUUtilization".to_string(),
22630            namespace: "AWS/EC2".to_string(),
22631            statistic: "Average".to_string(),
22632            period: 300,
22633            comparison_operator: "GreaterThanThreshold".to_string(),
22634            threshold: 80.0,
22635            actions_enabled: true,
22636            state_reason: "Threshold crossed".to_string(),
22637            resource: "".to_string(),
22638            dimensions: "".to_string(),
22639            expression: "".to_string(),
22640            alarm_type: "MetricAlarm".to_string(),
22641            cross_account: "".to_string(),
22642        }];
22643
22644        assert!(app.alarms_state.current_alarm.is_none());
22645        assert_eq!(app.view_mode, ViewMode::List);
22646        assert!(!app.alarms_state.metrics_loading);
22647
22648        // Press Enter - should drill into alarm and trigger metrics loading
22649        app.handle_action(Action::Select);
22650
22651        assert_eq!(
22652            app.alarms_state.current_alarm,
22653            Some("test-alarm".to_string())
22654        );
22655        assert_eq!(app.view_mode, ViewMode::Detail);
22656        assert!(
22657            app.alarms_state.metrics_loading,
22658            "Should trigger metrics loading"
22659        );
22660    }
22661
22662    #[test]
22663    fn test_cloudwatch_alarms_metric_data_renders() {
22664        let mut app = test_app();
22665        app.mode = Mode::Normal;
22666        app.current_service = Service::CloudWatchAlarms;
22667        app.service_selected = true;
22668        app.view_mode = ViewMode::Detail;
22669
22670        // Add test alarm
22671        app.alarms_state.table.items = vec![Alarm {
22672            name: "test-alarm".to_string(),
22673            state: "ALARM".to_string(),
22674            state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
22675            description: "Test alarm".to_string(),
22676            metric_name: "CPUUtilization".to_string(),
22677            namespace: "AWS/EC2".to_string(),
22678            statistic: "Average".to_string(),
22679            period: 300,
22680            comparison_operator: "GreaterThanThreshold".to_string(),
22681            threshold: 80.0,
22682            actions_enabled: true,
22683            state_reason: "Threshold crossed".to_string(),
22684            resource: "".to_string(),
22685            dimensions: "".to_string(),
22686            expression: "".to_string(),
22687            alarm_type: "MetricAlarm".to_string(),
22688            cross_account: "".to_string(),
22689        }];
22690
22691        app.alarms_state.current_alarm = Some("test-alarm".to_string());
22692
22693        // Add metric data
22694        app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0), (3000, 70.0)];
22695        app.alarms_state.metrics_loading = false;
22696
22697        // Verify metric data is present and will be rendered
22698        assert!(!app.alarms_state.metric_data.is_empty());
22699        assert_eq!(app.alarms_state.metric_data.len(), 3);
22700        assert!(!app.alarms_state.metrics_loading);
22701    }
22702
22703    #[test]
22704    fn test_cloudwatch_alarms_back_clears_metrics() {
22705        let mut app = test_app();
22706        app.mode = Mode::Normal;
22707        app.current_service = Service::CloudWatchAlarms;
22708        app.service_selected = true;
22709        app.view_mode = ViewMode::Detail;
22710
22711        // Set up alarm detail view with metrics
22712        app.alarms_state.current_alarm = Some("test-alarm".to_string());
22713        app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
22714
22715        assert!(!app.alarms_state.metric_data.is_empty());
22716
22717        // Go back
22718        app.handle_action(Action::GoBack);
22719
22720        // Should clear current alarm and metrics
22721        assert!(app.alarms_state.current_alarm.is_none());
22722        assert!(app.alarms_state.metric_data.is_empty());
22723        assert_eq!(app.view_mode, ViewMode::List);
22724    }
22725
22726    #[test]
22727    fn test_cloudwatch_alarms_refresh_reloads_metrics() {
22728        let mut app = test_app();
22729        app.mode = Mode::Normal;
22730        app.current_service = Service::CloudWatchAlarms;
22731        app.service_selected = true;
22732        app.view_mode = ViewMode::Detail;
22733
22734        // Set up alarm detail view with old metrics
22735        app.alarms_state.current_alarm = Some("test-alarm".to_string());
22736        app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
22737        app.alarms_state.metrics_loading = false;
22738
22739        assert!(!app.alarms_state.metric_data.is_empty());
22740        assert!(!app.alarms_state.metrics_loading);
22741
22742        // Refresh with Ctrl+R
22743        app.handle_action(Action::Refresh);
22744
22745        // Should keep old metrics visible until new data arrives
22746        assert!(!app.alarms_state.metric_data.is_empty());
22747        assert!(app.alarms_state.metrics_loading);
22748    }
22749
22750    #[test]
22751    fn test_cloudwatch_alarms_right_arrow_expands() {
22752        let mut app = test_app();
22753        app.mode = Mode::Normal;
22754        app.current_service = Service::CloudWatchAlarms;
22755        app.service_selected = true;
22756        app.view_mode = ViewMode::List;
22757
22758        // Add test alarm
22759        app.alarms_state.table.items = vec![Alarm {
22760            name: "test-alarm".to_string(),
22761            state: "ALARM".to_string(),
22762            state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
22763            description: "Test alarm".to_string(),
22764            metric_name: "CPUUtilization".to_string(),
22765            namespace: "AWS/EC2".to_string(),
22766            statistic: "Average".to_string(),
22767            period: 300,
22768            comparison_operator: "GreaterThanThreshold".to_string(),
22769            threshold: 80.0,
22770            actions_enabled: true,
22771            state_reason: "Threshold crossed".to_string(),
22772            resource: "".to_string(),
22773            dimensions: "".to_string(),
22774            expression: "".to_string(),
22775            alarm_type: "MetricAlarm".to_string(),
22776            cross_account: "".to_string(),
22777        }];
22778
22779        assert!(!app.alarms_state.table.is_expanded());
22780
22781        // Right arrow should expand
22782        app.handle_action(Action::NextPane);
22783        assert!(app.alarms_state.table.is_expanded());
22784
22785        // Left arrow should collapse
22786        app.handle_action(Action::PrevPane);
22787        assert!(!app.alarms_state.table.is_expanded());
22788    }
22789
22790    #[test]
22791    fn test_cloudwatch_alarms_tab_switches_tabs() {
22792        let mut app = test_app();
22793        app.mode = Mode::Normal;
22794        app.current_service = Service::CloudWatchAlarms;
22795        app.service_selected = true;
22796
22797        // Start on All Alarms tab
22798        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
22799
22800        // Tab switches to In Alarm
22801        app.handle_action(Action::NextDetailTab);
22802        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
22803
22804        // Tab again switches back to All Alarms
22805        app.handle_action(Action::NextDetailTab);
22806        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
22807
22808        // Shift+Tab switches to In Alarm
22809        app.handle_action(Action::PrevDetailTab);
22810        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
22811    }
22812
22813    #[test]
22814    fn test_column_toggle_prevents_hiding_last_column() {
22815        let mut app = test_app();
22816        app.mode = Mode::Normal;
22817        app.current_service = Service::CloudWatchAlarms;
22818        app.service_selected = true;
22819
22820        // Start with only one visible column
22821        app.cw_alarm_visible_column_ids = vec!["column.cw.alarm.name"];
22822
22823        // Open column selector
22824        app.handle_action(Action::OpenColumnSelector);
22825        assert_eq!(app.mode, Mode::ColumnSelector);
22826
22827        // Select the only visible column (index 1)
22828        app.column_selector_index = 1;
22829
22830        // Try to toggle it off - should NOT remove it
22831        app.handle_action(Action::ToggleColumn);
22832
22833        // Should still have one column visible
22834        assert_eq!(app.cw_alarm_visible_column_ids.len(), 1);
22835        assert_eq!(app.cw_alarm_visible_column_ids[0], "column.cw.alarm.name");
22836    }
22837}