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::route::Column as RouteColumn;
4pub use crate::aws::{filter_profiles, Profile as AwsProfile, Region as AwsRegion};
5use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
6use crate::cloudtrail::{CloudTrailEvent, CloudTrailEventColumn, EventResourceColumn};
7use crate::common::{ColumnId, CyclicEnum, InputFocus, PageSize, SortDirection};
8pub use crate::cw::insights::InsightsFocus;
9use crate::cw::insights::InsightsState;
10pub use crate::cw::{Alarm, AlarmColumn};
11pub use crate::ec2::{Column as Ec2Column, Instance as Ec2Instance};
12use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
13use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
14use crate::efs::fs::{Column as EfsColumn, FileSystem as EfsFileSystem};
15use crate::fsx::fs::Column as FsxColumn;
16use crate::iam::{
17    GroupUser as IamGroupUser, Policy as IamPolicy, RoleColumn, RoleTag as IamRoleTag, UserColumn,
18    UserTag as IamUserTag,
19};
20#[cfg(test)]
21use crate::iam::{IamRole, IamUser, LastAccessedService};
22use crate::keymap::{Action, Mode};
23use crate::kms::key::Key as KmsKey;
24pub use crate::lambda::{
25    Alias as LambdaAlias, Application as LambdaApplication,
26    ApplicationColumn as LambdaApplicationColumn, Deployment, DeploymentColumn,
27    Function as LambdaFunction, FunctionColumn as LambdaColumn, Layer as LambdaLayer, Resource,
28    ResourceColumn as LambdaResourceColumn, Version as LambdaVersion,
29};
30pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
31use crate::session::{Session, SessionTab};
32pub use crate::sqs::queue::Column as SqsColumn;
33pub use crate::sqs::trigger::Column as SqsTriggerColumn;
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};
42pub use crate::ui::cfn::{
43    change_set_column_ids, change_set_visible_column_ids, event_column_ids,
44    event_visible_column_ids, filtered_change_sets, filtered_cloudformation_stacks,
45    filtered_events, filtered_outputs, filtered_parameters, filtered_resources, output_column_ids,
46    parameter_column_ids, resource_column_ids, DetailTab as CfnDetailTab,
47    EventsView as CfnEventsView, State as CfnState, StatusFilter as CfnStatusFilter,
48};
49pub use crate::ui::cw::alarms::{
50    AlarmDetailTab, AlarmTab, AlarmViewMode, FILTER_CONTROLS as ALARM_FILTER_CONTROLS,
51};
52pub use crate::ui::cw::logs::{
53    filtered_log_events, filtered_log_groups, filtered_log_streams, selected_log_group,
54    DetailTab as CwLogsDetailTab, EventFilterFocus, FILTER_CONTROLS as LOG_FILTER_CONTROLS,
55};
56pub use crate::ui::ec2::{
57    DetailTab as Ec2DetailTab, State as Ec2State, StateFilter as Ec2StateFilter,
58    STATE_FILTER as EC2_STATE_FILTER,
59};
60pub use crate::ui::ecr::{
61    filtered_ecr_images, filtered_ecr_repositories, State as EcrState, Tab as EcrTab,
62    FILTER_CONTROLS as ECR_FILTER_CONTROLS,
63};
64pub use crate::ui::efs::State as EfsState;
65pub use crate::ui::fsx::State as FsxState;
66use crate::ui::iam::{GroupTab, RoleTab, State as IamState, UserTab};
67pub use crate::ui::kms::{State as KmsState, Tab as KmsTab};
68pub use crate::ui::lambda::{
69    filtered_lambda_applications, filtered_lambda_functions,
70    ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
71    DetailTab as LambdaDetailTab, State as LambdaState, FILTER_CONTROLS as LAMBDA_FILTER_CONTROLS,
72};
73use crate::ui::monitoring::MonitoringState;
74pub use crate::ui::s3::{
75    calculate_total_bucket_rows, calculate_total_object_rows, BucketType as S3BucketType,
76    ObjectTab as S3ObjectTab, State as S3State,
77};
78pub use crate::ui::sqs::{
79    extract_account_id, extract_region, filtered_eventbridge_pipes, filtered_lambda_triggers,
80    filtered_queues, filtered_subscriptions, filtered_tags, QueueDetailTab as SqsQueueDetailTab,
81    State as SqsState, FILTER_CONTROLS as SQS_FILTER_CONTROLS,
82    SUBSCRIPTION_FILTER_CONTROLS as SQS_SUBSCRIPTION_FILTER_CONTROLS, SUBSCRIPTION_REGION,
83};
84pub use crate::ui::{
85    CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, LogGroupColumn, Preferences,
86    StreamColumn, StreamSort, TimeUnit,
87};
88#[cfg(test)]
89use rusticity_core::LogStream;
90use rusticity_core::{
91    AlarmsClient, ApiGatewayClient, AwsConfig, CloudFormationClient, CloudTrailClient,
92    CloudWatchClient, Ec2Client, EcrClient, EfsClient, FsxClient, IamClient, KmsClient,
93    LambdaClient, S3Client, SqsClient,
94};
95
96#[derive(Clone)]
97pub struct Tab {
98    pub service: Service,
99    pub title: String,
100    pub breadcrumb: String,
101}
102
103pub struct App {
104    pub running: bool,
105    pub mode: Mode,
106    pub config: AwsConfig,
107    pub cloudwatch_client: CloudWatchClient,
108    pub cloudtrail_client: CloudTrailClient,
109    pub s3_client: S3Client,
110    pub sqs_client: SqsClient,
111    pub alarms_client: AlarmsClient,
112    pub ec2_client: Ec2Client,
113    pub ecr_client: EcrClient,
114    pub kms_client: KmsClient,
115    pub efs_client: EfsClient,
116    pub fsx_client: FsxClient,
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 kms_state: KmsState,
136    pub efs_state: EfsState,
137    pub fsx_state: FsxState,
138    pub apig_state: ApigState,
139    pub lambda_state: LambdaState,
140    pub lambda_application_state: LambdaApplicationState,
141    pub cfn_state: CfnState,
142    pub iam_state: IamState,
143    pub service_picker: ServicePickerState,
144    pub service_selected: bool,
145    pub profile: String,
146    pub region: String,
147    pub region_selector_index: usize,
148    pub cw_log_group_visible_column_ids: Vec<ColumnId>,
149    pub cw_log_group_column_ids: Vec<ColumnId>,
150    pub column_selector_index: usize,
151    pub preference_section: Preferences,
152    pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
153    pub cw_log_stream_column_ids: Vec<ColumnId>,
154    pub cw_log_event_visible_column_ids: Vec<ColumnId>,
155    pub cw_log_event_column_ids: Vec<ColumnId>,
156    pub cw_log_tag_visible_column_ids: Vec<ColumnId>,
157    pub cw_log_tag_column_ids: Vec<ColumnId>,
158    pub cw_alarm_visible_column_ids: Vec<ColumnId>,
159    pub cw_alarm_column_ids: Vec<ColumnId>,
160    pub cloudtrail_event_visible_column_ids: Vec<ColumnId>,
161    pub cloudtrail_event_column_ids: Vec<ColumnId>,
162    pub cloudtrail_resource_visible_column_ids: Vec<ColumnId>,
163    pub cloudtrail_resource_column_ids: Vec<ColumnId>,
164    pub s3_bucket_visible_column_ids: Vec<ColumnId>,
165    pub s3_bucket_column_ids: Vec<ColumnId>,
166    pub sqs_visible_column_ids: Vec<ColumnId>,
167    pub sqs_column_ids: Vec<ColumnId>,
168    pub ec2_visible_column_ids: Vec<ColumnId>,
169    pub ec2_column_ids: Vec<ColumnId>,
170    pub ecr_repo_visible_column_ids: Vec<ColumnId>,
171    pub ecr_repo_column_ids: Vec<ColumnId>,
172    pub ecr_image_visible_column_ids: Vec<ColumnId>,
173    pub ecr_image_column_ids: Vec<ColumnId>,
174    pub efs_visible_column_ids: Vec<ColumnId>,
175    pub efs_column_ids: Vec<ColumnId>,
176    pub fsx_visible_column_ids: Vec<ColumnId>,
177    pub fsx_column_ids: Vec<ColumnId>,
178    pub apig_api_visible_column_ids: Vec<ColumnId>,
179    pub apig_api_column_ids: Vec<ColumnId>,
180    pub apig_route_visible_column_ids: Vec<ColumnId>,
181    pub apig_route_column_ids: Vec<ColumnId>,
182    pub apig_resource_visible_column_ids: Vec<ColumnId>,
183    pub apig_resource_column_ids: Vec<ColumnId>,
184    pub lambda_application_visible_column_ids: Vec<ColumnId>,
185    pub lambda_application_column_ids: Vec<ColumnId>,
186    pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
187    pub lambda_deployment_column_ids: Vec<ColumnId>,
188    pub lambda_resource_visible_column_ids: Vec<ColumnId>,
189    pub lambda_resource_column_ids: Vec<ColumnId>,
190    pub cfn_visible_column_ids: Vec<ColumnId>,
191    pub cfn_column_ids: Vec<ColumnId>,
192    pub cfn_parameter_visible_column_ids: Vec<ColumnId>,
193    pub cfn_parameter_column_ids: Vec<ColumnId>,
194    pub cfn_output_visible_column_ids: Vec<ColumnId>,
195    pub cfn_output_column_ids: Vec<ColumnId>,
196    pub cfn_resource_visible_column_ids: Vec<ColumnId>,
197    pub cfn_resource_column_ids: Vec<ColumnId>,
198    pub cfn_event_visible_column_ids: Vec<ColumnId>,
199    pub cfn_event_column_ids: Vec<ColumnId>,
200    pub cfn_change_set_visible_column_ids: Vec<ColumnId>,
201    pub cfn_change_set_column_ids: Vec<ColumnId>,
202    pub iam_user_visible_column_ids: Vec<ColumnId>,
203    pub iam_user_column_ids: Vec<ColumnId>,
204    pub iam_role_visible_column_ids: Vec<ColumnId>,
205    pub iam_role_column_ids: Vec<ColumnId>,
206    pub iam_group_visible_column_ids: Vec<String>,
207    pub iam_group_column_ids: Vec<String>,
208    pub iam_policy_visible_column_ids: Vec<String>,
209    pub iam_policy_column_ids: Vec<String>,
210    pub view_mode: ViewMode,
211    pub error_message: Option<String>,
212    pub error_scroll: usize,
213    pub page_input: String,
214    pub calendar_date: Option<time::Date>,
215    pub calendar_selecting: CalendarField,
216    pub cursor_pos: usize,
217    pub current_session: Option<Session>,
218    pub sessions: Vec<Session>,
219    pub session_picker_selected: usize,
220    pub session_filter: String,
221    pub session_filter_active: bool,
222    pub region_filter: String,
223    pub region_picker_selected: usize,
224    pub region_filter_active: bool,
225    pub region_latencies: std::collections::HashMap<String, u64>,
226    pub profile_filter: String,
227    pub profile_picker_selected: usize,
228    pub profile_filter_active: bool,
229    pub available_profiles: Vec<AwsProfile>,
230    pub snapshot_requested: bool,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq)]
234pub enum CalendarField {
235    StartDate,
236    EndDate,
237}
238
239pub struct CloudWatchInsightsState {
240    pub insights: InsightsState,
241    pub loading: bool,
242}
243
244pub struct CloudWatchAlarmsState {
245    pub table: TableState<Alarm>,
246    pub current_alarm: Option<String>,
247    pub alarm_tab: AlarmTab,
248    pub detail_tab: AlarmDetailTab,
249    pub view_as: AlarmViewMode,
250    pub wrap_lines: bool,
251    pub sort_column: String,
252    pub sort_direction: SortDirection,
253    pub input_focus: InputFocus,
254    pub metric_data: Vec<(i64, f64)>,
255    pub metrics_loading: bool,
256}
257
258#[derive(Debug, Clone)]
259pub struct CloudTrailState {
260    pub table: TableState<CloudTrailEvent>,
261    pub input_focus: InputFocus,
262    pub current_event: Option<CloudTrailEvent>,
263    pub event_json_scroll: usize,
264    pub detail_focus: CloudTrailDetailFocus,
265    pub resources_expanded_index: Option<usize>,
266    /// The EventName filter currently applied via API (set on Enter, cleared on empty filter).
267    pub active_event_name_filter: Option<String>,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq)]
271pub enum CloudTrailDetailFocus {
272    Resources,
273    EventRecord,
274}
275
276impl CyclicEnum for CloudTrailDetailFocus {
277    const ALL: &'static [Self] = &[Self::Resources, Self::EventRecord];
278}
279
280impl PageSize {
281    pub fn value(&self) -> usize {
282        match self {
283            PageSize::Ten => 10,
284            PageSize::TwentyFive => 25,
285            PageSize::Fifty => 50,
286            PageSize::OneHundred => 100,
287        }
288    }
289
290    pub fn next(&self) -> Self {
291        match self {
292            PageSize::Ten => PageSize::TwentyFive,
293            PageSize::TwentyFive => PageSize::Fifty,
294            PageSize::Fifty => PageSize::OneHundred,
295            PageSize::OneHundred => PageSize::Ten,
296        }
297    }
298}
299
300pub struct ServicePickerState {
301    pub filter: String,
302    pub filter_active: bool,
303    pub selected: usize,
304    pub services: Vec<&'static str>,
305}
306
307#[derive(Debug, Clone, Copy, PartialEq)]
308pub enum ViewMode {
309    List,
310    Detail,
311    Events,
312    InsightsResults,
313    PolicyView,
314}
315
316#[derive(Debug, Clone, Copy, PartialEq)]
317pub enum Service {
318    ApiGatewayApis,
319    CloudWatchLogGroups,
320    CloudWatchInsights,
321    CloudWatchAlarms,
322    CloudTrailEvents,
323    S3Buckets,
324    SqsQueues,
325    Ec2Instances,
326    EcrRepositories,
327    KmsKeys,
328    EfsFileSystems,
329    FsxFileSystems,
330    LambdaFunctions,
331    LambdaApplications,
332    CloudFormationStacks,
333    IamUsers,
334    IamRoles,
335    IamUserGroups,
336}
337
338impl Service {
339    pub fn name(&self) -> &str {
340        match self {
341            Service::ApiGatewayApis => "API Gateway › APIs",
342            Service::CloudWatchLogGroups => "CloudWatch › Log Groups",
343            Service::CloudWatchInsights => "CloudWatch › Logs Insights",
344            Service::CloudWatchAlarms => "CloudWatch › Alarms",
345            Service::CloudTrailEvents => "CloudTrail › Event History",
346            Service::S3Buckets => "S3 › Buckets",
347            Service::SqsQueues => "SQS › Queues",
348            Service::Ec2Instances => "EC2 › Instances",
349            Service::EcrRepositories => "ECR › Repositories",
350            Service::KmsKeys => "KMS › Managed Keys",
351            Service::EfsFileSystems => "EFS › File Systems",
352            Service::FsxFileSystems => "FSx › File Systems",
353            Service::LambdaFunctions => "Lambda › Functions",
354            Service::LambdaApplications => "Lambda › Applications",
355            Service::CloudFormationStacks => "CloudFormation › Stacks",
356            Service::IamUsers => "IAM › Users",
357            Service::IamRoles => "IAM › Roles",
358            Service::IamUserGroups => "IAM › User Groups",
359        }
360    }
361}
362
363pub(crate) fn copy_to_clipboard(text: &str) {
364    use std::io::Write;
365    use std::process::{Command, Stdio};
366    if let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() {
367        if let Some(mut stdin) = child.stdin.take() {
368            let _ = stdin.write_all(text.as_bytes());
369        }
370        let _ = child.wait();
371    }
372}
373
374pub(crate) fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
375    if max > 0 {
376        *selected = (*selected + page_size).min(max - 1);
377    }
378}
379
380pub(crate) fn toggle_iam_preference(
381    idx: usize,
382    column_ids: &[String],
383    visible_column_ids: &mut Vec<String>,
384    page_size: &mut PageSize,
385) {
386    if idx > 0 && idx <= column_ids.len() {
387        if let Some(col) = column_ids.get(idx - 1) {
388            if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
389                // Only remove if more than one column is visible
390                if visible_column_ids.len() > 1 {
391                    visible_column_ids.remove(pos);
392                }
393            } else {
394                visible_column_ids.push(col.clone());
395            }
396        }
397    } else if idx == column_ids.len() + 3 {
398        *page_size = PageSize::Ten;
399    } else if idx == column_ids.len() + 4 {
400        *page_size = PageSize::TwentyFive;
401    } else if idx == column_ids.len() + 5 {
402        *page_size = PageSize::Fifty;
403    }
404}
405
406pub(crate) fn toggle_iam_preference_static(
407    idx: usize,
408    column_ids: &[ColumnId],
409    visible_column_ids: &mut Vec<ColumnId>,
410    page_size: &mut PageSize,
411) {
412    if idx > 0 && idx <= column_ids.len() {
413        if let Some(col) = column_ids.get(idx - 1) {
414            if let Some(pos) = visible_column_ids.iter().position(|c| c == col) {
415                // Only remove if more than one column is visible
416                if visible_column_ids.len() > 1 {
417                    visible_column_ids.remove(pos);
418                }
419            } else {
420                visible_column_ids.push(*col);
421            }
422        }
423    } else if idx == column_ids.len() + 3 {
424        *page_size = PageSize::Ten;
425    } else if idx == column_ids.len() + 4 {
426        *page_size = PageSize::TwentyFive;
427    } else if idx == column_ids.len() + 5 {
428        *page_size = PageSize::Fifty;
429    }
430}
431
432pub(crate) fn toggle_iam_page_size_only(idx: usize, base_idx: usize, page_size: &mut PageSize) {
433    if idx == base_idx {
434        *page_size = PageSize::Ten;
435    } else if idx == base_idx + 1 {
436        *page_size = PageSize::TwentyFive;
437    } else if idx == base_idx + 2 {
438        *page_size = PageSize::Fifty;
439    }
440}
441
442/// Helper to cycle to next preference section (Columns -> PageSize -> Columns)
443fn cycle_preference_next(current_idx: &mut usize, num_columns: usize) {
444    let page_size_idx = num_columns + 2;
445    if *current_idx < page_size_idx {
446        *current_idx = page_size_idx;
447    } else {
448        *current_idx = 0;
449    }
450}
451
452/// Helper to cycle to previous preference section (Columns <- PageSize <- Columns)
453fn cycle_preference_prev(current_idx: &mut usize, num_columns: usize) {
454    let page_size_idx = num_columns + 2;
455    if *current_idx >= page_size_idx {
456        *current_idx = 0;
457    } else {
458        *current_idx = page_size_idx;
459    }
460}
461
462impl App {
463    pub fn get_input_focus(&self) -> InputFocus {
464        InputFocus::Filter
465    }
466
467    fn get_active_filter_mut(&mut self) -> Option<&mut String> {
468        if self.current_service == Service::ApiGatewayApis {
469            crate::apig::actions::get_active_filter_mut(self)
470        } else if self.current_service == Service::CloudWatchAlarms {
471            Some(&mut self.alarms_state.table.filter)
472        } else if self.current_service == Service::CloudTrailEvents {
473            Some(&mut self.cloudtrail_state.table.filter)
474        } else if self.current_service == Service::Ec2Instances {
475            crate::ec2::actions::get_active_filter_mut(self)
476        } else if self.current_service == Service::S3Buckets {
477            if self.s3_state.current_bucket.is_some() {
478                Some(&mut self.s3_state.object_filter)
479            } else {
480                Some(&mut self.s3_state.buckets.filter)
481            }
482        } else if self.current_service == Service::EcrRepositories {
483            if self.ecr_state.current_repository.is_some() {
484                Some(&mut self.ecr_state.images.filter)
485            } else {
486                Some(&mut self.ecr_state.repositories.filter)
487            }
488        } else if self.current_service == Service::SqsQueues {
489            crate::sqs::actions::get_active_filter_mut(self)
490        } else if self.current_service == Service::LambdaFunctions {
491            crate::lambda::functions::get_active_filter_mut(self)
492        } else if self.current_service == Service::LambdaApplications {
493            crate::lambda::applications::get_active_filter_mut(self)
494        } else if self.current_service == Service::KmsKeys {
495            crate::kms::actions::get_active_filter_mut(self)
496        } else if self.current_service == Service::EfsFileSystems {
497            crate::efs::actions::get_active_filter_mut(self)
498        } else if self.current_service == Service::FsxFileSystems {
499            crate::fsx::actions::get_active_filter_mut(self)
500        } else if self.current_service == Service::CloudFormationStacks {
501            if self.cfn_state.current_stack.is_some()
502                && self.cfn_state.detail_tab == CfnDetailTab::Resources
503            {
504                Some(&mut self.cfn_state.resources.filter)
505            } else {
506                Some(&mut self.cfn_state.table.filter)
507            }
508        } else if self.current_service == Service::IamUsers {
509            if self.iam_state.current_user.is_some() {
510                if self.iam_state.user_tab == UserTab::Tags {
511                    Some(&mut self.iam_state.user_tags.filter)
512                } else {
513                    Some(&mut self.iam_state.policies.filter)
514                }
515            } else {
516                Some(&mut self.iam_state.users.filter)
517            }
518        } else if self.current_service == Service::IamRoles {
519            if self.iam_state.current_role.is_some() {
520                if self.iam_state.role_tab == RoleTab::Tags {
521                    Some(&mut self.iam_state.tags.filter)
522                } else if self.iam_state.role_tab == RoleTab::LastAccessed {
523                    Some(&mut self.iam_state.last_accessed_filter)
524                } else {
525                    Some(&mut self.iam_state.policies.filter)
526                }
527            } else {
528                Some(&mut self.iam_state.roles.filter)
529            }
530        } else if self.current_service == Service::IamUserGroups {
531            if self.iam_state.current_group.is_some() {
532                if self.iam_state.group_tab == GroupTab::Permissions {
533                    Some(&mut self.iam_state.policies.filter)
534                } else if self.iam_state.group_tab == GroupTab::Users {
535                    Some(&mut self.iam_state.group_users.filter)
536                } else {
537                    None
538                }
539            } else {
540                Some(&mut self.iam_state.groups.filter)
541            }
542        } else if self.view_mode == ViewMode::List {
543            Some(&mut self.log_groups_state.log_groups.filter)
544        } else if self.view_mode == ViewMode::Detail
545            && self.log_groups_state.detail_tab == DetailTab::LogStreams
546        {
547            Some(&mut self.log_groups_state.stream_filter)
548        } else {
549            None
550        }
551    }
552
553    fn apply_filter_operation<F>(&mut self, op: F)
554    where
555        F: FnOnce(&mut String),
556    {
557        if let Some(filter) = self.get_active_filter_mut() {
558            op(filter);
559            // Automatically reset selection for all services
560            if self.current_service == Service::CloudWatchAlarms {
561                crate::cw::actions::alarms_apply_filter_reset(self);
562            } else if self.current_service == Service::CloudTrailEvents {
563                crate::cloudtrail::actions::apply_filter_reset(self);
564            } else if self.current_service == Service::Ec2Instances {
565                crate::ec2::actions::apply_filter_reset(self);
566            } else if self.current_service == Service::S3Buckets {
567                crate::s3::actions::apply_filter_reset(self);
568            } else if self.current_service == Service::EcrRepositories {
569                crate::ecr::actions::apply_filter_reset(self);
570            } else if self.current_service == Service::KmsKeys {
571                crate::kms::actions::apply_filter_reset(self);
572            } else if self.current_service == Service::EfsFileSystems {
573                crate::efs::actions::apply_filter_reset(self);
574            } else if self.current_service == Service::FsxFileSystems {
575                crate::fsx::actions::apply_filter_reset(self);
576            } else if self.current_service == Service::SqsQueues {
577                crate::sqs::actions::apply_filter_reset(self);
578            } else if self.current_service == Service::LambdaFunctions {
579                crate::lambda::functions::apply_filter_reset(self);
580            } else if self.current_service == Service::LambdaApplications {
581                crate::lambda::applications::apply_filter_reset(self);
582            } else if self.current_service == Service::CloudFormationStacks {
583                crate::cfn::actions::apply_filter_reset(self);
584            } else if self.current_service == Service::IamUsers {
585                crate::iam::actions::apply_filter_reset_users(self);
586            } else if self.current_service == Service::IamRoles {
587                crate::iam::actions::apply_filter_reset_roles(self);
588            } else if self.current_service == Service::IamUserGroups {
589                crate::iam::actions::apply_filter_reset_groups(self);
590            } else if self.current_service == Service::CloudWatchLogGroups {
591                if self.view_mode == ViewMode::List {
592                    self.log_groups_state.log_groups.reset();
593                } else if self.log_groups_state.detail_tab == DetailTab::LogStreams {
594                    self.log_groups_state.selected_stream = 0;
595                }
596            } else if self.current_service == Service::ApiGatewayApis {
597                crate::apig::actions::apply_filter_reset(self);
598            }
599        }
600    }
601
602    pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
603        let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
604            .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
605
606        std::env::set_var("AWS_PROFILE", &profile_name);
607
608        let config = AwsConfig::new(region).await?;
609        let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
610        let cloudtrail_client = CloudTrailClient::new(config.clone());
611        let s3_client = S3Client::new(config.clone());
612        let sqs_client = SqsClient::new(config.clone());
613        let alarms_client = AlarmsClient::new(config.clone());
614        let ec2_client = Ec2Client::new(config.clone());
615        let ecr_client = EcrClient::new(config.clone());
616        let apig_client = ApiGatewayClient::new(config.clone());
617        let iam_client = IamClient::new(config.clone());
618        let kms_client = KmsClient::new(config.clone());
619        let efs_client = EfsClient::new(config.clone());
620        let fsx_client = FsxClient::new(config.clone());
621        let lambda_client = LambdaClient::new(config.clone());
622        let cloudformation_client = CloudFormationClient::new(config.clone());
623        let region_name = config.region.clone();
624
625        Ok(Self {
626            running: true,
627            mode: Mode::ServicePicker,
628            config,
629            cloudwatch_client,
630            cloudtrail_client,
631            s3_client,
632            sqs_client,
633            alarms_client,
634            ec2_client,
635            ecr_client,
636            kms_client,
637            efs_client,
638            fsx_client,
639            apig_client,
640            iam_client,
641            lambda_client,
642            cloudformation_client,
643            current_service: Service::CloudWatchLogGroups,
644            tabs: Vec::new(),
645            current_tab: 0,
646            tab_picker_selected: 0,
647            tab_filter: String::new(),
648            pending_key: None,
649            log_groups_state: CloudWatchLogGroupsState::new(),
650            insights_state: CloudWatchInsightsState::new(),
651            alarms_state: CloudWatchAlarmsState::new(),
652            cloudtrail_state: CloudTrailState {
653                table: TableState::new(),
654                input_focus: InputFocus::Filter,
655                current_event: None,
656                event_json_scroll: 0,
657                detail_focus: CloudTrailDetailFocus::Resources,
658                resources_expanded_index: None,
659                active_event_name_filter: None,
660            },
661            s3_state: S3State::new(),
662            sqs_state: SqsState::new(),
663            ec2_state: Ec2State::default(),
664            ecr_state: EcrState::new(),
665            kms_state: KmsState::new(),
666            efs_state: EfsState::new(),
667            fsx_state: FsxState::new(),
668            apig_state: ApigState::new(),
669            lambda_state: LambdaState::new(),
670            lambda_application_state: LambdaApplicationState::new(),
671            cfn_state: CfnState::new(),
672            iam_state: IamState::new(),
673            service_picker: ServicePickerState::new(),
674            service_selected: false,
675            profile: profile_name,
676            region: region_name,
677            region_selector_index: 0,
678            cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
679            cw_log_group_column_ids: LogGroupColumn::ids(),
680            column_selector_index: 0,
681            cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
682            cw_log_stream_column_ids: StreamColumn::ids(),
683            cw_log_event_visible_column_ids: EventColumn::default_visible(),
684            cw_log_event_column_ids: EventColumn::ids(),
685            cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
686            cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
687            cw_alarm_visible_column_ids: [
688                AlarmColumn::Name,
689                AlarmColumn::State,
690                AlarmColumn::LastStateUpdate,
691                AlarmColumn::Conditions,
692                AlarmColumn::Actions,
693            ]
694            .iter()
695            .map(|c| c.id())
696            .collect(),
697            cw_alarm_column_ids: AlarmColumn::ids(),
698            cloudtrail_event_visible_column_ids: [
699                CloudTrailEventColumn::EventName,
700                CloudTrailEventColumn::EventTime,
701                CloudTrailEventColumn::Username,
702                CloudTrailEventColumn::EventSource,
703                CloudTrailEventColumn::ResourceType,
704                CloudTrailEventColumn::ResourceName,
705            ]
706            .iter()
707            .map(|c| c.id())
708            .collect(),
709            cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
710            cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
711            cloudtrail_resource_column_ids: EventResourceColumn::ids(),
712            s3_bucket_visible_column_ids: S3BucketColumn::ids(),
713            s3_bucket_column_ids: S3BucketColumn::ids(),
714            sqs_visible_column_ids: [
715                SqsColumn::Name,
716                SqsColumn::Type,
717                SqsColumn::Created,
718                SqsColumn::MessagesAvailable,
719                SqsColumn::MessagesInFlight,
720                SqsColumn::Encryption,
721                SqsColumn::ContentBasedDeduplication,
722            ]
723            .iter()
724            .map(|c| c.id())
725            .collect(),
726            sqs_column_ids: SqsColumn::ids(),
727            ec2_visible_column_ids: [
728                Ec2Column::Name,
729                Ec2Column::InstanceId,
730                Ec2Column::InstanceState,
731                Ec2Column::InstanceType,
732                Ec2Column::StatusCheck,
733                Ec2Column::AlarmStatus,
734                Ec2Column::AvailabilityZone,
735                Ec2Column::PublicIpv4Dns,
736                Ec2Column::PublicIpv4Address,
737                Ec2Column::ElasticIp,
738                Ec2Column::Ipv6Ips,
739                Ec2Column::Monitoring,
740                Ec2Column::SecurityGroupName,
741                Ec2Column::KeyName,
742                Ec2Column::LaunchTime,
743                Ec2Column::PlatformDetails,
744            ]
745            .iter()
746            .map(|c| c.id())
747            .collect(),
748            ec2_column_ids: Ec2Column::ids(),
749            ecr_repo_visible_column_ids: EcrColumn::ids(),
750            ecr_repo_column_ids: EcrColumn::ids(),
751            ecr_image_visible_column_ids: EcrImageColumn::ids(),
752            ecr_image_column_ids: EcrImageColumn::ids(),
753            efs_visible_column_ids: EfsColumn::default_visible_ids(),
754            efs_column_ids: EfsColumn::ids(),
755            fsx_visible_column_ids: FsxColumn::default_visible_ids(),
756            fsx_column_ids: FsxColumn::ids(),
757            apig_api_visible_column_ids: ApigColumn::ids(),
758            apig_api_column_ids: ApigColumn::ids(),
759            apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
760            apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
761            apig_resource_visible_column_ids: ResourceColumn::all()
762                .iter()
763                .map(|c| c.id())
764                .collect(),
765            apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
766            lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
767            lambda_application_column_ids: LambdaApplicationColumn::ids(),
768            lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
769            lambda_deployment_column_ids: DeploymentColumn::ids(),
770            lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
771            lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
772            cfn_visible_column_ids: [
773                CfnColumn::Name,
774                CfnColumn::Status,
775                CfnColumn::CreatedTime,
776                CfnColumn::Description,
777            ]
778            .iter()
779            .map(|c| c.id())
780            .collect(),
781            cfn_column_ids: CfnColumn::ids(),
782            cfn_parameter_visible_column_ids: parameter_column_ids(),
783            cfn_parameter_column_ids: parameter_column_ids(),
784            cfn_output_visible_column_ids: output_column_ids(),
785            cfn_output_column_ids: output_column_ids(),
786            cfn_resource_visible_column_ids: resource_column_ids(),
787            cfn_resource_column_ids: resource_column_ids(),
788            cfn_event_visible_column_ids: event_visible_column_ids(),
789            cfn_event_column_ids: event_column_ids(),
790            cfn_change_set_visible_column_ids: change_set_visible_column_ids(),
791            cfn_change_set_column_ids: change_set_column_ids(),
792            iam_user_visible_column_ids: UserColumn::visible(),
793            iam_user_column_ids: UserColumn::ids(),
794            iam_role_visible_column_ids: RoleColumn::visible(),
795            iam_role_column_ids: RoleColumn::ids(),
796            iam_group_visible_column_ids: vec![
797                "Group name".to_string(),
798                "Users".to_string(),
799                "Permissions".to_string(),
800                "Creation time".to_string(),
801            ],
802            iam_group_column_ids: vec![
803                "Group name".to_string(),
804                "Path".to_string(),
805                "Users".to_string(),
806                "Permissions".to_string(),
807                "Creation time".to_string(),
808            ],
809            iam_policy_visible_column_ids: vec![
810                "Policy name".to_string(),
811                "Type".to_string(),
812                "Attached via".to_string(),
813            ],
814            iam_policy_column_ids: vec![
815                "Policy name".to_string(),
816                "Type".to_string(),
817                "Attached via".to_string(),
818                "Attached entities".to_string(),
819                "Description".to_string(),
820                "Creation time".to_string(),
821                "Edited time".to_string(),
822            ],
823            preference_section: Preferences::Columns,
824            view_mode: ViewMode::List,
825            error_message: None,
826            error_scroll: 0,
827            page_input: String::new(),
828            calendar_date: None,
829            calendar_selecting: CalendarField::StartDate,
830            cursor_pos: 0,
831            current_session: None,
832            sessions: Vec::new(),
833            session_picker_selected: 0,
834            session_filter: String::new(),
835            session_filter_active: false,
836            region_filter: String::new(),
837            region_filter_active: false,
838            region_picker_selected: 0,
839            region_latencies: std::collections::HashMap::new(),
840            profile_filter: String::new(),
841            profile_filter_active: false,
842            profile_picker_selected: 0,
843            available_profiles: Vec::new(),
844            snapshot_requested: false,
845        })
846    }
847
848    pub fn new_without_client(profile: String, region: Option<String>) -> Self {
849        let config = AwsConfig::dummy(region.clone());
850        Self {
851            running: true,
852            mode: Mode::ServicePicker,
853            config: config.clone(),
854            cloudwatch_client: CloudWatchClient::dummy(config.clone()),
855            cloudtrail_client: CloudTrailClient::new(config.clone()),
856            s3_client: S3Client::new(config.clone()),
857            sqs_client: SqsClient::new(config.clone()),
858            alarms_client: AlarmsClient::new(config.clone()),
859            ec2_client: Ec2Client::new(config.clone()),
860            ecr_client: EcrClient::new(config.clone()),
861            kms_client: KmsClient::new(config.clone()),
862            efs_client: EfsClient::new(config.clone()),
863            fsx_client: FsxClient::new(config.clone()),
864            apig_client: ApiGatewayClient::new(config.clone()),
865            iam_client: IamClient::new(config.clone()),
866            lambda_client: LambdaClient::new(config.clone()),
867            cloudformation_client: CloudFormationClient::new(config.clone()),
868            current_service: Service::CloudWatchLogGroups,
869            tabs: Vec::new(),
870            current_tab: 0,
871            tab_picker_selected: 0,
872            tab_filter: String::new(),
873            pending_key: None,
874            log_groups_state: CloudWatchLogGroupsState::new(),
875            insights_state: CloudWatchInsightsState::new(),
876            alarms_state: CloudWatchAlarmsState::new(),
877            s3_state: S3State::new(),
878            cloudtrail_state: CloudTrailState {
879                table: TableState::new(),
880                input_focus: InputFocus::Filter,
881                current_event: None,
882                event_json_scroll: 0,
883                detail_focus: CloudTrailDetailFocus::Resources,
884                resources_expanded_index: None,
885                active_event_name_filter: None,
886            },
887            sqs_state: SqsState::new(),
888            ec2_state: Ec2State::default(),
889            ecr_state: EcrState::new(),
890            kms_state: KmsState::new(),
891            efs_state: EfsState::new(),
892            fsx_state: FsxState::new(),
893            apig_state: ApigState::new(),
894            lambda_state: LambdaState::new(),
895            lambda_application_state: LambdaApplicationState::new(),
896            cfn_state: CfnState::new(),
897            iam_state: IamState::new(),
898            service_picker: ServicePickerState::new(),
899            service_selected: false,
900            profile,
901            region: region.unwrap_or_default(),
902            region_selector_index: 0,
903            cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
904            cw_log_group_column_ids: LogGroupColumn::ids(),
905            column_selector_index: 0,
906            preference_section: Preferences::Columns,
907            cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
908            cw_log_stream_column_ids: StreamColumn::ids(),
909            cw_log_event_visible_column_ids: EventColumn::default_visible(),
910            cw_log_event_column_ids: EventColumn::ids(),
911            cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
912            cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
913            cw_alarm_visible_column_ids: [
914                AlarmColumn::Name,
915                AlarmColumn::State,
916                AlarmColumn::LastStateUpdate,
917                AlarmColumn::Conditions,
918                AlarmColumn::Actions,
919            ]
920            .iter()
921            .map(|c| c.id())
922            .collect(),
923            cw_alarm_column_ids: AlarmColumn::ids(),
924            s3_bucket_visible_column_ids: S3BucketColumn::ids(),
925            cloudtrail_event_visible_column_ids: [
926                CloudTrailEventColumn::EventName,
927                CloudTrailEventColumn::EventTime,
928                CloudTrailEventColumn::Username,
929                CloudTrailEventColumn::EventSource,
930                CloudTrailEventColumn::ResourceType,
931                CloudTrailEventColumn::ResourceName,
932            ]
933            .iter()
934            .map(|c| c.id())
935            .collect(),
936            cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
937            cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
938            cloudtrail_resource_column_ids: EventResourceColumn::ids(),
939            s3_bucket_column_ids: S3BucketColumn::ids(),
940            sqs_visible_column_ids: [
941                SqsColumn::Name,
942                SqsColumn::Type,
943                SqsColumn::Created,
944                SqsColumn::MessagesAvailable,
945                SqsColumn::MessagesInFlight,
946                SqsColumn::Encryption,
947                SqsColumn::ContentBasedDeduplication,
948            ]
949            .iter()
950            .map(|c| c.id())
951            .collect(),
952            sqs_column_ids: SqsColumn::ids(),
953            ec2_visible_column_ids: [
954                Ec2Column::Name,
955                Ec2Column::InstanceId,
956                Ec2Column::InstanceState,
957                Ec2Column::InstanceType,
958                Ec2Column::StatusCheck,
959                Ec2Column::AlarmStatus,
960                Ec2Column::AvailabilityZone,
961                Ec2Column::PublicIpv4Dns,
962                Ec2Column::PublicIpv4Address,
963                Ec2Column::ElasticIp,
964                Ec2Column::Ipv6Ips,
965                Ec2Column::Monitoring,
966                Ec2Column::SecurityGroupName,
967                Ec2Column::KeyName,
968                Ec2Column::LaunchTime,
969                Ec2Column::PlatformDetails,
970            ]
971            .iter()
972            .map(|c| c.id())
973            .collect(),
974            ec2_column_ids: Ec2Column::ids(),
975            ecr_repo_visible_column_ids: EcrColumn::ids(),
976            ecr_repo_column_ids: EcrColumn::ids(),
977            ecr_image_visible_column_ids: EcrImageColumn::ids(),
978            ecr_image_column_ids: EcrImageColumn::ids(),
979            efs_visible_column_ids: EfsColumn::default_visible_ids(),
980            efs_column_ids: EfsColumn::ids(),
981            fsx_visible_column_ids: FsxColumn::default_visible_ids(),
982            fsx_column_ids: FsxColumn::ids(),
983            lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
984            lambda_application_column_ids: LambdaApplicationColumn::ids(),
985            apig_api_visible_column_ids: ApigColumn::ids(),
986            apig_api_column_ids: ApigColumn::ids(),
987            apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
988            apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
989            apig_resource_visible_column_ids: ResourceColumn::all()
990                .iter()
991                .map(|c| c.id())
992                .collect(),
993            apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
994            lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
995            lambda_deployment_column_ids: DeploymentColumn::ids(),
996            lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
997            lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
998            cfn_visible_column_ids: [
999                CfnColumn::Name,
1000                CfnColumn::Status,
1001                CfnColumn::CreatedTime,
1002                CfnColumn::Description,
1003            ]
1004            .iter()
1005            .map(|c| c.id())
1006            .collect(),
1007            cfn_column_ids: CfnColumn::ids(),
1008            iam_user_visible_column_ids: UserColumn::visible(),
1009            cfn_parameter_visible_column_ids: parameter_column_ids(),
1010            cfn_parameter_column_ids: parameter_column_ids(),
1011            cfn_output_visible_column_ids: output_column_ids(),
1012            cfn_output_column_ids: output_column_ids(),
1013            cfn_resource_visible_column_ids: resource_column_ids(),
1014            cfn_resource_column_ids: resource_column_ids(),
1015            cfn_event_visible_column_ids: event_visible_column_ids(),
1016            cfn_event_column_ids: event_column_ids(),
1017            cfn_change_set_visible_column_ids: change_set_visible_column_ids(),
1018            cfn_change_set_column_ids: change_set_column_ids(),
1019            iam_user_column_ids: UserColumn::ids(),
1020            iam_role_visible_column_ids: RoleColumn::visible(),
1021            iam_role_column_ids: RoleColumn::ids(),
1022            iam_group_visible_column_ids: vec![
1023                "Group name".to_string(),
1024                "Users".to_string(),
1025                "Permissions".to_string(),
1026                "Creation time".to_string(),
1027            ],
1028            iam_group_column_ids: vec![
1029                "Group name".to_string(),
1030                "Path".to_string(),
1031                "Users".to_string(),
1032                "Permissions".to_string(),
1033                "Creation time".to_string(),
1034            ],
1035            iam_policy_visible_column_ids: vec![
1036                "Policy name".to_string(),
1037                "Type".to_string(),
1038                "Attached via".to_string(),
1039            ],
1040            iam_policy_column_ids: vec![
1041                "Policy name".to_string(),
1042                "Type".to_string(),
1043                "Attached via".to_string(),
1044                "Attached entities".to_string(),
1045                "Description".to_string(),
1046                "Creation time".to_string(),
1047                "Edited time".to_string(),
1048            ],
1049            view_mode: ViewMode::List,
1050            error_message: None,
1051            error_scroll: 0,
1052            page_input: String::new(),
1053            calendar_date: None,
1054            calendar_selecting: CalendarField::StartDate,
1055            cursor_pos: 0,
1056            current_session: None,
1057            sessions: Vec::new(),
1058            session_picker_selected: 0,
1059            session_filter: String::new(),
1060            session_filter_active: false,
1061            region_filter: String::new(),
1062            region_filter_active: false,
1063            region_picker_selected: 0,
1064            region_latencies: std::collections::HashMap::new(),
1065            profile_filter: String::new(),
1066            profile_filter_active: false,
1067            profile_picker_selected: 0,
1068            available_profiles: Vec::new(),
1069            snapshot_requested: false,
1070        }
1071    }
1072
1073    pub fn handle_action(&mut self, action: Action) {
1074        match action {
1075            Action::Noop => {}
1076            Action::EnterFilterMode => match self.mode {
1077                Mode::ServicePicker => self.service_picker.filter_active = true,
1078                Mode::RegionPicker => self.region_filter_active = true,
1079                Mode::SessionPicker => self.session_filter_active = true,
1080                Mode::ProfilePicker => self.profile_filter_active = true,
1081                _ => {}
1082            },
1083            Action::ExitFilterMode => match self.mode {
1084                Mode::ServicePicker => {
1085                    if self.service_picker.filter_active {
1086                        self.service_picker.filter_active = false;
1087                    } else {
1088                        self.mode = Mode::Normal;
1089                    }
1090                }
1091                Mode::RegionPicker => {
1092                    if self.region_filter_active {
1093                        self.region_filter_active = false;
1094                    } else {
1095                        self.mode = Mode::Normal;
1096                    }
1097                }
1098                Mode::SessionPicker => {
1099                    if self.session_filter_active {
1100                        self.session_filter_active = false;
1101                    } else {
1102                        self.mode = Mode::Normal;
1103                    }
1104                }
1105                Mode::ProfilePicker => {
1106                    if self.profile_filter_active {
1107                        self.profile_filter_active = false;
1108                    } else {
1109                        self.mode = Mode::Normal;
1110                    }
1111                }
1112                _ => {}
1113            },
1114            Action::Quit => {
1115                self.mode = Mode::QuitConfirm;
1116            }
1117            Action::ConfirmQuit => {
1118                self.save_current_session();
1119                self.running = false;
1120            }
1121            Action::CancelQuit => {
1122                self.mode = Mode::Normal;
1123            }
1124            Action::CloseService => {
1125                if !self.tabs.is_empty() {
1126                    // Close the current tab
1127                    self.tabs.remove(self.current_tab);
1128
1129                    if self.tabs.is_empty() {
1130                        // Last tab closed - show service picker
1131                        self.service_selected = false;
1132                        self.current_tab = 0;
1133                        self.mode = Mode::ServicePicker;
1134                    } else {
1135                        // Tabs remain - switch to adjacent tab
1136                        if self.current_tab >= self.tabs.len() {
1137                            self.current_tab = self.tabs.len() - 1;
1138                        }
1139                        self.current_service = self.tabs[self.current_tab].service;
1140                        self.service_selected = true;
1141                        self.mode = Mode::Normal;
1142                    }
1143                } else {
1144                    // No tabs - just close service picker if open
1145                    self.service_selected = false;
1146                    self.mode = Mode::Normal;
1147                }
1148                self.service_picker.filter.clear();
1149                self.service_picker.selected = 0;
1150            }
1151            Action::NextItem => {
1152                // Only navigate when filter is not active
1153                let should_navigate = match self.mode {
1154                    Mode::ServicePicker => !self.service_picker.filter_active,
1155                    Mode::RegionPicker => !self.region_filter_active,
1156                    Mode::SessionPicker => !self.session_filter_active,
1157                    Mode::ProfilePicker => !self.profile_filter_active,
1158                    _ => true,
1159                };
1160                if should_navigate {
1161                    self.next_item();
1162                } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1163                    // In INSERT mode, 'j' goes to filter
1164                    self.region_filter.push('j');
1165                    self.region_picker_selected = 0;
1166                }
1167            }
1168            Action::PrevItem => {
1169                // Only navigate when filter is not active
1170                let should_navigate = match self.mode {
1171                    Mode::ServicePicker => !self.service_picker.filter_active,
1172                    Mode::RegionPicker => !self.region_filter_active,
1173                    Mode::SessionPicker => !self.session_filter_active,
1174                    Mode::ProfilePicker => !self.profile_filter_active,
1175                    _ => true,
1176                };
1177                if should_navigate {
1178                    self.prev_item();
1179                } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1180                    // In INSERT mode, 'k' goes to filter
1181                    self.region_filter.push('k');
1182                    self.region_picker_selected = 0;
1183                }
1184            }
1185            Action::PageUp => self.page_up(),
1186            Action::PageDown => self.page_down(),
1187            Action::NextPane => self.next_pane(),
1188            Action::PrevPane => self.prev_pane(),
1189            Action::CollapseRow => self.collapse_row(),
1190            Action::ExpandRow => self.expand_row(),
1191            Action::Select => self.select_item(),
1192            Action::OpenSpaceMenu => {
1193                // If Insights log group search is focused, Space opens/toggles dropdown
1194                use crate::app::InsightsFocus;
1195                if self.current_service == Service::CloudWatchInsights
1196                    && self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
1197                {
1198                    if self.insights_state.insights.show_dropdown
1199                        && !self.insights_state.insights.log_group_matches.is_empty()
1200                    {
1201                        // Dropdown open: toggle highlighted entry
1202                        self.handle_action(Action::ToggleFilterCheckbox);
1203                    } else {
1204                        // Dropdown closed: open it with all log groups
1205                        self.handle_action(Action::Select);
1206                    }
1207                    return;
1208                }
1209                self.mode = Mode::SpaceMenu;
1210                self.service_picker.filter.clear();
1211                self.service_picker.filter_active = false;
1212                self.service_picker.selected = 0;
1213            }
1214            Action::CloseMenu => {
1215                self.mode = Mode::Normal;
1216                self.service_picker.filter.clear();
1217                // Reset selection when closing filter to avoid out-of-bounds
1218                match self.current_service {
1219                    Service::S3Buckets => {
1220                        crate::s3::actions::reset_on_service_select(self);
1221                    }
1222                    Service::CloudFormationStacks => {
1223                        crate::cfn::actions::apply_filter_reset(self);
1224                    }
1225                    Service::LambdaFunctions => {
1226                        crate::lambda::functions::apply_filter_reset(self);
1227                    }
1228                    Service::SqsQueues => {
1229                        self.sqs_state.queues.reset();
1230                    }
1231                    Service::IamRoles => {
1232                        crate::iam::actions::reset_on_service_select_roles(self);
1233                    }
1234                    Service::IamUsers => {
1235                        crate::iam::actions::reset_on_service_select_users(self);
1236                    }
1237                    Service::IamUserGroups => {
1238                        crate::iam::actions::reset_on_service_select_groups(self);
1239                    }
1240                    Service::CloudWatchAlarms => {
1241                        self.alarms_state.table.reset();
1242                    }
1243                    Service::Ec2Instances => {
1244                        crate::ec2::actions::apply_filter_reset(self);
1245                    }
1246                    Service::EcrRepositories => {
1247                        crate::ecr::actions::apply_filter_reset(self);
1248                    }
1249                    Service::KmsKeys => {
1250                        crate::kms::actions::apply_filter_reset(self);
1251                    }
1252                    Service::EfsFileSystems => {
1253                        crate::efs::actions::apply_filter_reset(self);
1254                    }
1255                    Service::FsxFileSystems => {
1256                        crate::fsx::actions::apply_filter_reset(self);
1257                    }
1258                    Service::ApiGatewayApis => {
1259                        crate::apig::actions::apply_filter_reset(self);
1260                    }
1261                    Service::LambdaApplications => {
1262                        crate::lambda::applications::apply_filter_reset(self);
1263                    }
1264                    _ => {}
1265                }
1266            }
1267            Action::NextTab => {
1268                if !self.tabs.is_empty() {
1269                    self.current_tab = (self.current_tab + 1) % self.tabs.len();
1270                    self.current_service = self.tabs[self.current_tab].service;
1271                }
1272            }
1273            Action::PrevTab => {
1274                if !self.tabs.is_empty() {
1275                    self.current_tab = if self.current_tab == 0 {
1276                        self.tabs.len() - 1
1277                    } else {
1278                        self.current_tab - 1
1279                    };
1280                    self.current_service = self.tabs[self.current_tab].service;
1281                }
1282            }
1283            Action::CloseTab => {
1284                if !self.tabs.is_empty() {
1285                    self.tabs.remove(self.current_tab);
1286                    if self.tabs.is_empty() {
1287                        // Last tab closed - show service picker
1288                        self.service_selected = false;
1289                        self.current_tab = 0;
1290                        self.service_picker.filter.clear();
1291                        self.service_picker.selected = 0;
1292                        self.mode = Mode::ServicePicker;
1293                    } else {
1294                        // If we closed the last tab, move to the left
1295                        // Otherwise stay at same index (which is now the next tab to the right)
1296                        if self.current_tab >= self.tabs.len() {
1297                            self.current_tab = self.tabs.len() - 1;
1298                        }
1299                        self.current_service = self.tabs[self.current_tab].service;
1300                        self.service_selected = true;
1301                        self.mode = Mode::Normal;
1302                    }
1303                }
1304            }
1305            Action::OpenTabPicker => {
1306                if !self.tabs.is_empty() {
1307                    self.tab_picker_selected = self.current_tab;
1308                    self.mode = Mode::TabPicker;
1309                } else {
1310                    self.mode = Mode::Normal;
1311                }
1312            }
1313            Action::OpenSessionPicker => {
1314                self.save_current_session();
1315                self.sessions = Session::list_all().unwrap_or_default();
1316                self.session_picker_selected = 0;
1317                self.mode = Mode::SessionPicker;
1318            }
1319            Action::LoadSession => {
1320                let filtered_sessions = self.get_filtered_sessions();
1321                if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
1322                    let session = session.clone();
1323                    // Load the session
1324                    self.profile = session.profile.clone();
1325                    self.region = session.region.clone();
1326                    self.config.account_id = session.account_id.clone();
1327                    self.config.role_arn = session.role_arn.clone();
1328
1329                    // Clear existing tabs and load session tabs
1330                    self.tabs.clear();
1331                    for session_tab in &session.tabs {
1332                        // Parse service from string
1333                        let service = match session_tab.service.as_str() {
1334                            "ApiGatewayApis" => Service::ApiGatewayApis,
1335                            "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
1336                            "CloudWatchInsights" => Service::CloudWatchInsights,
1337                            "CloudWatchAlarms" => Service::CloudWatchAlarms,
1338                            "CloudTrailEvents" => Service::CloudTrailEvents,
1339                            "S3Buckets" => Service::S3Buckets,
1340                            "SqsQueues" => Service::SqsQueues,
1341                            "Ec2Instances" => Service::Ec2Instances,
1342                            "EcrRepositories" => Service::EcrRepositories,
1343                            "KmsKeys" => Service::KmsKeys,
1344                            "EfsFileSystems" => Service::EfsFileSystems,
1345                            "FsxFileSystems" => Service::FsxFileSystems,
1346                            "LambdaFunctions" => Service::LambdaFunctions,
1347                            "LambdaApplications" => Service::LambdaApplications,
1348                            "CloudFormationStacks" => Service::CloudFormationStacks,
1349                            "IamUsers" => Service::IamUsers,
1350                            "IamRoles" => Service::IamRoles,
1351                            "IamUserGroups" => Service::IamUserGroups,
1352                            _ => continue,
1353                        };
1354
1355                        self.tabs.push(Tab {
1356                            service,
1357                            title: session_tab.title.clone(),
1358                            breadcrumb: session_tab.breadcrumb.clone(),
1359                        });
1360
1361                        // Restore filter if present
1362                        if let Some(filter) = &session_tab.filter {
1363                            if service == Service::CloudWatchLogGroups {
1364                                self.log_groups_state.log_groups.filter = filter.clone();
1365                            }
1366                        }
1367                    }
1368
1369                    if !self.tabs.is_empty() {
1370                        self.current_tab = 0;
1371                        self.current_service = self.tabs[0].service;
1372                        self.service_selected = true;
1373                        self.current_session = Some(session.clone());
1374                    }
1375                }
1376                self.mode = Mode::Normal;
1377            }
1378            Action::SaveSession => {
1379                // TODO: Implement session saving
1380            }
1381            Action::OpenServicePicker => {
1382                if self.mode == Mode::ServicePicker {
1383                    self.tabs.push(Tab {
1384                        service: Service::S3Buckets,
1385                        title: "S3 › Buckets".to_string(),
1386                        breadcrumb: "S3 › Buckets".to_string(),
1387                    });
1388                    self.current_tab = self.tabs.len() - 1;
1389                    self.current_service = Service::S3Buckets;
1390                    self.view_mode = ViewMode::List;
1391                    self.service_selected = true;
1392                    self.mode = Mode::Normal;
1393                } else {
1394                    self.mode = Mode::ServicePicker;
1395                    self.service_picker.filter.clear();
1396                    self.service_picker.selected = 0;
1397                }
1398            }
1399            Action::OpenCloudWatch => {
1400                self.current_service = Service::CloudWatchLogGroups;
1401                self.view_mode = ViewMode::List;
1402                self.service_selected = true;
1403                self.mode = Mode::Normal;
1404            }
1405            Action::OpenCloudWatchSplit => {
1406                self.current_service = Service::CloudWatchInsights;
1407                self.view_mode = ViewMode::InsightsResults;
1408                self.service_selected = true;
1409                self.mode = Mode::Normal;
1410            }
1411            Action::OpenCloudWatchAlarms => {
1412                self.current_service = Service::CloudWatchAlarms;
1413                self.view_mode = ViewMode::List;
1414                self.service_selected = true;
1415                self.mode = Mode::Normal;
1416            }
1417            Action::FilterInput(c) => {
1418                if self.mode == Mode::TabPicker {
1419                    self.tab_filter.push(c);
1420                    self.tab_picker_selected = 0;
1421                } else if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1422                    self.service_picker.filter.push(c);
1423                    self.service_picker.selected = 0;
1424                } else if self.mode == Mode::ServicePicker && c == 'q' {
1425                    self.mode = Mode::QuitConfirm;
1426                } else if self.mode == Mode::ServicePicker && c == 'i' {
1427                    self.service_picker.filter_active = true;
1428                } else if self.mode == Mode::ServicePicker {
1429                    // Any other char auto-activates filter and types it
1430                    self.service_picker.filter_active = true;
1431                    self.service_picker.filter.push(c);
1432                    self.service_picker.selected = 0;
1433                } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1434                    self.region_filter.push(c);
1435                    self.region_picker_selected = 0;
1436                } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1437                    self.profile_filter.push(c);
1438                    self.profile_picker_selected = 0;
1439                } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1440                    self.session_filter.push(c);
1441                    self.session_picker_selected = 0;
1442                } else if self.mode == Mode::InsightsInput {
1443                    match self.insights_state.insights.insights_focus {
1444                        InsightsFocus::Query => {
1445                            self.insights_state.insights.query_text.push(c);
1446                        }
1447                        InsightsFocus::LogGroupSearch => {
1448                            self.insights_state.insights.log_group_search.push(c);
1449                            // Update matches
1450                            if !self.insights_state.insights.log_group_search.is_empty() {
1451                                self.insights_state.insights.log_group_matches = self
1452                                    .log_groups_state
1453                                    .log_groups
1454                                    .items
1455                                    .iter()
1456                                    .filter(|g| {
1457                                        g.name.to_lowercase().contains(
1458                                            &self
1459                                                .insights_state
1460                                                .insights
1461                                                .log_group_search
1462                                                .to_lowercase(),
1463                                        )
1464                                    })
1465                                    .take(50)
1466                                    .map(|g| g.name.clone())
1467                                    .collect();
1468                                self.insights_state.insights.show_dropdown = true;
1469                            } else {
1470                                self.insights_state.insights.log_group_matches.clear();
1471                                self.insights_state.insights.show_dropdown = false;
1472                            }
1473                        }
1474                        _ => {}
1475                    }
1476                } else if self.mode == Mode::FilterInput {
1477                    // Check if we should capture digits for page navigation
1478                    let is_pagination_focused = if self.current_service
1479                        == Service::LambdaApplications
1480                    {
1481                        crate::lambda::applications::is_pagination_focused(self)
1482                    } else if self.current_service == Service::CloudFormationStacks {
1483                        crate::cfn::actions::is_pagination_focused(self)
1484                    } else if self.current_service == Service::IamRoles
1485                        && self.iam_state.current_role.is_none()
1486                    {
1487                        crate::iam::actions::is_pagination_focused_roles(self)
1488                    } else if self.view_mode == ViewMode::PolicyView {
1489                        crate::iam::actions::is_pagination_focused_policy_view(self)
1490                    } else if self.current_service == Service::CloudWatchAlarms {
1491                        crate::cw::actions::alarms_is_pagination_focused(self)
1492                    } else if self.current_service == Service::CloudTrailEvents {
1493                        crate::cloudtrail::actions::is_pagination_focused(self)
1494                    } else if self.current_service == Service::Ec2Instances {
1495                        crate::ec2::actions::is_pagination_focused(self)
1496                    } else if self.current_service == Service::CloudWatchLogGroups {
1497                        crate::cw::actions::logs_is_pagination_focused(self)
1498                    } else if self.current_service == Service::ApiGatewayApis {
1499                        crate::apig::actions::is_pagination_focused(self)
1500                    } else if self.current_service == Service::EcrRepositories
1501                        && self.ecr_state.current_repository.is_none()
1502                    {
1503                        crate::ecr::actions::is_pagination_focused(self)
1504                    } else if self.current_service == Service::KmsKeys {
1505                        crate::kms::actions::is_pagination_focused(self)
1506                    } else if self.current_service == Service::EfsFileSystems {
1507                        crate::efs::actions::is_pagination_focused(self)
1508                    } else if self.current_service == Service::FsxFileSystems {
1509                        crate::fsx::actions::is_pagination_focused(self)
1510                    } else if self.current_service == Service::LambdaFunctions {
1511                        crate::lambda::functions::is_pagination_focused(self)
1512                    } else if self.current_service == Service::SqsQueues {
1513                        if self.sqs_state.current_queue.is_some()
1514                            && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1515                                || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1516                                || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1517                                || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1518                                || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1519                        {
1520                            self.sqs_state.input_focus == InputFocus::Pagination
1521                        } else {
1522                            false
1523                        }
1524                    } else {
1525                        false
1526                    };
1527
1528                    if is_pagination_focused && c.is_ascii_digit() {
1529                        self.page_input.push(c);
1530                    } else if self.current_service == Service::LambdaApplications {
1531                        if crate::lambda::applications::is_filter_focused(self) {
1532                            self.apply_filter_operation(|f| f.push(c));
1533                        }
1534                    } else if self.current_service == Service::CloudFormationStacks {
1535                        crate::cfn::actions::filter_char_push(self, c);
1536                    } else if self.current_service == Service::EcrRepositories
1537                        && self.ecr_state.current_repository.is_none()
1538                    {
1539                        if self.ecr_state.input_focus == InputFocus::Filter {
1540                            self.apply_filter_operation(|f| f.push(c));
1541                        }
1542                    } else if self.current_service == Service::IamRoles
1543                        && self.iam_state.current_role.is_none()
1544                    {
1545                        if crate::iam::actions::is_filter_focused_roles(self) {
1546                            self.apply_filter_operation(|f| f.push(c));
1547                        }
1548                    } else if self.view_mode == ViewMode::PolicyView {
1549                        if crate::iam::actions::is_filter_focused_policy_view(self) {
1550                            self.apply_filter_operation(|f| f.push(c));
1551                        }
1552                    } else if self.current_service == Service::ApiGatewayApis
1553                        && self.apig_state.current_api.is_some()
1554                        && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
1555                    {
1556                        if crate::apig::actions::filter_char_allowed(self) {
1557                            self.apply_filter_operation(|f| f.push(c));
1558                        }
1559                    } else if self.current_service == Service::LambdaFunctions {
1560                        if crate::lambda::functions::filter_char_allowed(self) {
1561                            self.apply_filter_operation(|f| f.push(c));
1562                        }
1563                    } else if self.current_service == Service::SqsQueues
1564                        && self.sqs_state.current_queue.is_some()
1565                        && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1566                            || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1567                            || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1568                            || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1569                            || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1570                    {
1571                        if self.sqs_state.input_focus == InputFocus::Filter {
1572                            self.apply_filter_operation(|f| f.push(c));
1573                        }
1574                    } else if self.current_service == Service::Ec2Instances
1575                        && self.ec2_state.current_instance.is_some()
1576                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1577                    {
1578                        crate::ec2::actions::filter_char_push(self, c);
1579                    } else if self.current_service == Service::CloudWatchLogGroups {
1580                        if self.log_groups_state.input_focus == InputFocus::Filter {
1581                            self.apply_filter_operation(|f| f.push(c));
1582                        }
1583                    } else {
1584                        self.apply_filter_operation(|f| f.push(c));
1585                    }
1586                } else if self.mode == Mode::EventFilterInput {
1587                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1588                        self.log_groups_state.event_filter.push(c);
1589                    } else if c.is_ascii_digit() {
1590                        self.log_groups_state.relative_amount.push(c);
1591                    }
1592                } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1593                    self.page_input.push(c);
1594                }
1595            }
1596            Action::FilterBackspace => {
1597                if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1598                    self.service_picker.filter.pop();
1599                    self.service_picker.selected = 0;
1600                } else if self.mode == Mode::TabPicker {
1601                    self.tab_filter.pop();
1602                    self.tab_picker_selected = 0;
1603                } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1604                    self.region_filter.pop();
1605                    self.region_picker_selected = 0;
1606                } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1607                    self.profile_filter.pop();
1608                    self.profile_picker_selected = 0;
1609                } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1610                    self.session_filter.pop();
1611                    self.session_picker_selected = 0;
1612                } else if self.mode == Mode::InsightsInput {
1613                    match self.insights_state.insights.insights_focus {
1614                        InsightsFocus::Query => {
1615                            self.insights_state.insights.query_text.pop();
1616                        }
1617                        InsightsFocus::LogGroupSearch => {
1618                            self.insights_state.insights.log_group_search.pop();
1619                            // Update matches
1620                            if !self.insights_state.insights.log_group_search.is_empty() {
1621                                self.insights_state.insights.log_group_matches = self
1622                                    .log_groups_state
1623                                    .log_groups
1624                                    .items
1625                                    .iter()
1626                                    .filter(|g| {
1627                                        g.name.to_lowercase().contains(
1628                                            &self
1629                                                .insights_state
1630                                                .insights
1631                                                .log_group_search
1632                                                .to_lowercase(),
1633                                        )
1634                                    })
1635                                    .take(50)
1636                                    .map(|g| g.name.clone())
1637                                    .collect();
1638                                self.insights_state.insights.show_dropdown = true;
1639                            } else {
1640                                self.insights_state.insights.log_group_matches.clear();
1641                                self.insights_state.insights.show_dropdown = false;
1642                            }
1643                        }
1644                        _ => {}
1645                    }
1646                } else if self.mode == Mode::FilterInput {
1647                    // Only allow backspace when focus is on the input field
1648                    if self.current_service == Service::CloudFormationStacks {
1649                        if self.cfn_state.current_stack.is_some()
1650                            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1651                        {
1652                            if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1653                                self.cfn_state.parameters.filter.pop();
1654                                self.cfn_state.parameters.selected = 0;
1655                            }
1656                        } else if self.cfn_state.current_stack.is_some()
1657                            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1658                        {
1659                            if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1660                                self.cfn_state.outputs.filter.pop();
1661                                self.cfn_state.outputs.selected = 0;
1662                            }
1663                        } else if self.cfn_state.current_stack.is_some()
1664                            && self.cfn_state.detail_tab == CfnDetailTab::Resources
1665                        {
1666                            if self.cfn_state.resources_input_focus == InputFocus::Filter {
1667                                self.cfn_state.resources.filter.pop();
1668                                self.cfn_state.resources.selected = 0;
1669                            }
1670                        } else if self.cfn_state.input_focus == InputFocus::Filter {
1671                            self.apply_filter_operation(|f| {
1672                                f.pop();
1673                            });
1674                        }
1675                    } else if self.current_service == Service::Ec2Instances
1676                        && self.ec2_state.current_instance.is_some()
1677                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1678                    {
1679                        crate::ec2::actions::filter_char_pop(self);
1680                    } else if self.current_service == Service::CloudWatchLogGroups {
1681                        if self.log_groups_state.input_focus == InputFocus::Filter {
1682                            self.apply_filter_operation(|f| {
1683                                f.pop();
1684                            });
1685                        }
1686                    } else {
1687                        self.apply_filter_operation(|f| {
1688                            f.pop();
1689                        });
1690                    }
1691                } else if self.mode == Mode::EventFilterInput {
1692                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1693                        self.log_groups_state.event_filter.pop();
1694                    } else {
1695                        self.log_groups_state.relative_amount.pop();
1696                    }
1697                }
1698            }
1699            Action::DeleteWord => {
1700                let text = if self.mode == Mode::ServicePicker {
1701                    &mut self.service_picker.filter
1702                } else if self.mode == Mode::InsightsInput {
1703                    use crate::app::InsightsFocus;
1704                    match self.insights_state.insights.insights_focus {
1705                        InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1706                        InsightsFocus::LogGroupSearch => {
1707                            &mut self.insights_state.insights.log_group_search
1708                        }
1709                        _ => return,
1710                    }
1711                } else if self.mode == Mode::FilterInput {
1712                    if let Some(filter) = self.get_active_filter_mut() {
1713                        filter
1714                    } else {
1715                        return;
1716                    }
1717                } else if self.mode == Mode::EventFilterInput {
1718                    if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1719                        &mut self.log_groups_state.event_filter
1720                    } else {
1721                        &mut self.log_groups_state.relative_amount
1722                    }
1723                } else {
1724                    return;
1725                };
1726
1727                if text.is_empty() {
1728                    return;
1729                }
1730
1731                let mut chars: Vec<char> = text.chars().collect();
1732                while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1733                    chars.pop();
1734                }
1735                while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1736                    chars.pop();
1737                }
1738                *text = chars.into_iter().collect();
1739            }
1740            Action::WordLeft => {
1741                // Not implemented - would need cursor position tracking
1742            }
1743            Action::WordRight => {
1744                // Not implemented - would need cursor position tracking
1745            }
1746            Action::OpenColumnSelector => {
1747                // Block preferences for CFN non-column tabs
1748                if self.current_service == Service::CloudFormationStacks
1749                    && crate::cfn::actions::block_column_selector(self)
1750                {
1751                    return;
1752                }
1753
1754                // Don't allow opening preferences for IAM user tabs without preferences
1755                if self.current_service == Service::IamUsers
1756                    && self.iam_state.current_user.is_some()
1757                    && self.iam_state.user_tab == UserTab::SecurityCredentials
1758                {
1759                    return;
1760                }
1761
1762                // Don't allow opening preferences for IAM role tabs without preferences
1763                if self.current_service == Service::IamRoles
1764                    && self.iam_state.current_role.is_some()
1765                    && (self.iam_state.role_tab == RoleTab::TrustRelationships
1766                        || self.iam_state.role_tab == RoleTab::RevokeSessions)
1767                {
1768                    return;
1769                }
1770
1771                // Don't allow opening preferences for certain SQS tabs
1772                if self.current_service == Service::SqsQueues
1773                    && self.sqs_state.current_queue.is_some()
1774                    && matches!(
1775                        self.sqs_state.detail_tab,
1776                        SqsQueueDetailTab::QueuePolicies
1777                            | SqsQueueDetailTab::Monitoring
1778                            | SqsQueueDetailTab::DeadLetterQueue
1779                            | SqsQueueDetailTab::Encryption
1780                            | SqsQueueDetailTab::DeadLetterQueueRedriveTasks
1781                    )
1782                {
1783                    return;
1784                }
1785
1786                // Block preferences for EC2 non-tag tabs
1787                if self.current_service == Service::Ec2Instances
1788                    && crate::ec2::actions::block_column_selector(self)
1789                {
1790                    return;
1791                }
1792
1793                // Don't allow opening preferences for CloudTrail Event JSON
1794                if self.current_service == Service::CloudTrailEvents
1795                    && self.cloudtrail_state.current_event.is_some()
1796                    && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
1797                {
1798                    return;
1799                }
1800
1801                // If we have page input, apply it instead of opening column selector
1802                if !self.page_input.is_empty() {
1803                    if let Ok(page) = self.page_input.parse::<usize>() {
1804                        self.go_to_page(page);
1805                    }
1806                    self.page_input.clear();
1807                } else {
1808                    self.mode = Mode::ColumnSelector;
1809                    self.column_selector_index = 0;
1810                }
1811            }
1812            Action::ToggleColumn => {
1813                if self.current_service == Service::S3Buckets
1814                    && self.s3_state.current_bucket.is_none()
1815                {
1816                    crate::s3::actions::toggle_column(self);
1817                } else if self.current_service == Service::CloudWatchAlarms {
1818                    // Map flat list index to actual item
1819                    // 0: Columns header, 1-16: columns, 17: empty, 18: ViewAs header, 19-20: view options
1820                    // 21: empty, 22: PageSize header, 23-25: page sizes, 26: empty, 27: WrapLines header, 28: wrap option
1821                    let idx = self.column_selector_index;
1822                    if (1..=16).contains(&idx) {
1823                        // Column toggle
1824                        if let Some(col) = self.cw_alarm_column_ids.get(idx - 1) {
1825                            Self::toggle_column_visibility(
1826                                &mut self.cw_alarm_visible_column_ids,
1827                                &self.cw_alarm_column_ids,
1828                                *col,
1829                            );
1830                        }
1831                    } else if idx == 19 {
1832                        self.alarms_state.view_as = AlarmViewMode::Table;
1833                    } else if idx == 20 {
1834                        self.alarms_state.view_as = AlarmViewMode::Cards;
1835                    } else if idx == 23 {
1836                        self.alarms_state.table.page_size = PageSize::Ten;
1837                    } else if idx == 24 {
1838                        self.alarms_state.table.page_size = PageSize::TwentyFive;
1839                    } else if idx == 25 {
1840                        self.alarms_state.table.page_size = PageSize::Fifty;
1841                    } else if idx == 26 {
1842                        self.alarms_state.table.page_size = PageSize::OneHundred;
1843                    } else if idx == 29 {
1844                        self.alarms_state.wrap_lines = !self.alarms_state.wrap_lines;
1845                    }
1846                } else if self.current_service == Service::CloudTrailEvents {
1847                    if self.cloudtrail_state.current_event.is_some()
1848                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
1849                    {
1850                        // Resources table in detail view
1851                        let idx = self.column_selector_index;
1852                        if idx > 0 && idx <= self.cloudtrail_resource_column_ids.len() {
1853                            if let Some(col) = self.cloudtrail_resource_column_ids.get(idx - 1) {
1854                                Self::toggle_column_visibility(
1855                                    &mut self.cloudtrail_resource_visible_column_ids,
1856                                    &self.cloudtrail_resource_column_ids,
1857                                    *col,
1858                                );
1859                            }
1860                        }
1861                    } else {
1862                        // Main events table
1863                        let idx = self.column_selector_index;
1864                        if (1..=14).contains(&idx) {
1865                            if let Some(col) = self.cloudtrail_event_column_ids.get(idx - 1) {
1866                                Self::toggle_column_visibility(
1867                                    &mut self.cloudtrail_event_visible_column_ids,
1868                                    &self.cloudtrail_event_column_ids,
1869                                    *col,
1870                                );
1871                            }
1872                        } else if idx == 17 {
1873                            self.cloudtrail_state.table.page_size = PageSize::Ten;
1874                            self.cloudtrail_state.table.snap_to_page();
1875                        } else if idx == 18 {
1876                            self.cloudtrail_state.table.page_size = PageSize::TwentyFive;
1877                            self.cloudtrail_state.table.snap_to_page();
1878                        } else if idx == 19 {
1879                            self.cloudtrail_state.table.page_size = PageSize::Fifty;
1880                            self.cloudtrail_state.table.snap_to_page();
1881                        } else if idx == 20 {
1882                            self.cloudtrail_state.table.page_size = PageSize::OneHundred;
1883                            self.cloudtrail_state.table.snap_to_page();
1884                        }
1885                    }
1886                } else if self.current_service == Service::ApiGatewayApis {
1887                    crate::apig::actions::toggle_column(self);
1888                } else if self.current_service == Service::EcrRepositories {
1889                    crate::ecr::actions::toggle_column(self);
1890                } else if self.current_service == Service::KmsKeys {
1891                    crate::kms::actions::toggle_column(self);
1892                } else if self.current_service == Service::EfsFileSystems {
1893                    crate::efs::actions::toggle_column(self);
1894                } else if self.current_service == Service::FsxFileSystems {
1895                    crate::fsx::actions::toggle_column(self);
1896                } else if self.current_service == Service::Ec2Instances {
1897                    crate::ec2::actions::toggle_column(self);
1898                } else if self.current_service == Service::SqsQueues {
1899                    crate::sqs::actions::toggle_column(self);
1900                } else if self.current_service == Service::LambdaFunctions {
1901                    crate::lambda::functions::toggle_column(self);
1902                } else if self.current_service == Service::LambdaApplications {
1903                    crate::lambda::applications::toggle_column(self);
1904                } else if self.view_mode == ViewMode::Events {
1905                    if let Some(col) = self.cw_log_event_column_ids.get(self.column_selector_index)
1906                    {
1907                        if let Some(pos) = self
1908                            .cw_log_event_visible_column_ids
1909                            .iter()
1910                            .position(|c| c == col)
1911                        {
1912                            self.cw_log_event_visible_column_ids.remove(pos);
1913                        } else {
1914                            self.cw_log_event_visible_column_ids.push(*col);
1915                        }
1916                    }
1917                } else if self.view_mode == ViewMode::Detail {
1918                    let idx = self.column_selector_index;
1919                    if self.log_groups_state.detail_tab == DetailTab::Tags {
1920                        // Tags tab
1921                        if idx > 0 && idx <= self.cw_log_tag_column_ids.len() {
1922                            if let Some(col) = self.cw_log_tag_column_ids.get(idx - 1) {
1923                                if let Some(pos) = self
1924                                    .cw_log_tag_visible_column_ids
1925                                    .iter()
1926                                    .position(|c| c == col)
1927                                {
1928                                    self.cw_log_tag_visible_column_ids.remove(pos);
1929                                } else {
1930                                    self.cw_log_tag_visible_column_ids.push(*col);
1931                                }
1932                            }
1933                        } else if idx == self.cw_log_tag_column_ids.len() + 3 {
1934                            self.log_groups_state.tags.page_size = PageSize::Ten;
1935                        } else if idx == self.cw_log_tag_column_ids.len() + 4 {
1936                            self.log_groups_state.tags.page_size = PageSize::TwentyFive;
1937                        } else if idx == self.cw_log_tag_column_ids.len() + 5 {
1938                            self.log_groups_state.tags.page_size = PageSize::Fifty;
1939                        } else if idx == self.cw_log_tag_column_ids.len() + 6 {
1940                            self.log_groups_state.tags.page_size = PageSize::OneHundred;
1941                        }
1942                    } else {
1943                        // Log streams tab
1944                        if idx > 0 && idx <= self.cw_log_stream_column_ids.len() {
1945                            if let Some(col) = self.cw_log_stream_column_ids.get(idx - 1) {
1946                                if let Some(pos) = self
1947                                    .cw_log_stream_visible_column_ids
1948                                    .iter()
1949                                    .position(|c| c == col)
1950                                {
1951                                    self.cw_log_stream_visible_column_ids.remove(pos);
1952                                } else {
1953                                    self.cw_log_stream_visible_column_ids.push(*col);
1954                                }
1955                            }
1956                        } else if idx == self.cw_log_stream_column_ids.len() + 3 {
1957                            self.log_groups_state.stream_page_size = 10;
1958                            self.log_groups_state.stream_current_page = 0;
1959                        } else if idx == self.cw_log_stream_column_ids.len() + 4 {
1960                            self.log_groups_state.stream_page_size = 25;
1961                            self.log_groups_state.stream_current_page = 0;
1962                        } else if idx == self.cw_log_stream_column_ids.len() + 5 {
1963                            self.log_groups_state.stream_page_size = 50;
1964                            self.log_groups_state.stream_current_page = 0;
1965                        } else if idx == self.cw_log_stream_column_ids.len() + 6 {
1966                            self.log_groups_state.stream_page_size = 100;
1967                            self.log_groups_state.stream_current_page = 0;
1968                        }
1969                    }
1970                } else if self.current_service == Service::CloudFormationStacks {
1971                    crate::cfn::actions::toggle_column(self);
1972                } else if self.current_service == Service::IamUsers {
1973                    crate::iam::actions::toggle_column_users(self);
1974                } else if self.current_service == Service::IamRoles {
1975                    crate::iam::actions::toggle_column_roles(self);
1976                } else if self.current_service == Service::IamUserGroups {
1977                    crate::iam::actions::toggle_column_groups(self);
1978                } else {
1979                    let idx = self.column_selector_index;
1980                    if idx > 0 && idx <= self.cw_log_group_column_ids.len() {
1981                        if let Some(col) = self.cw_log_group_column_ids.get(idx - 1) {
1982                            if let Some(pos) = self
1983                                .cw_log_group_visible_column_ids
1984                                .iter()
1985                                .position(|c| c == col)
1986                            {
1987                                self.cw_log_group_visible_column_ids.remove(pos);
1988                            } else {
1989                                self.cw_log_group_visible_column_ids.push(*col);
1990                            }
1991                        }
1992                    } else if idx == self.cw_log_group_column_ids.len() + 3 {
1993                        self.log_groups_state.log_groups.page_size = PageSize::Ten;
1994                    } else if idx == self.cw_log_group_column_ids.len() + 4 {
1995                        self.log_groups_state.log_groups.page_size = PageSize::TwentyFive;
1996                    } else if idx == self.cw_log_group_column_ids.len() + 5 {
1997                        self.log_groups_state.log_groups.page_size = PageSize::Fifty;
1998                    } else if idx == self.cw_log_group_column_ids.len() + 6 {
1999                        self.log_groups_state.log_groups.page_size = PageSize::OneHundred;
2000                    }
2001                }
2002            }
2003            Action::NextPreferences => {
2004                if self.current_service == Service::ApiGatewayApis {
2005                    crate::apig::actions::next_preferences(self);
2006                } else if self.current_service == Service::CloudWatchAlarms {
2007                    crate::cw::actions::alarms_next_preferences(self);
2008                } else if self.current_service == Service::EcrRepositories
2009                    && self.ecr_state.current_repository.is_some()
2010                {
2011                    crate::ecr::actions::next_preferences(self);
2012                } else if self.current_service == Service::KmsKeys {
2013                    crate::kms::actions::next_preferences(self);
2014                } else if self.current_service == Service::EfsFileSystems {
2015                    crate::efs::actions::next_preferences(self);
2016                } else if self.current_service == Service::FsxFileSystems {
2017                    crate::fsx::actions::next_preferences(self);
2018                } else if self.current_service == Service::LambdaFunctions {
2019                    crate::lambda::functions::next_preferences(self);
2020                } else if self.current_service == Service::LambdaApplications {
2021                    crate::lambda::applications::next_preferences(self);
2022                } else if self.current_service == Service::CloudFormationStacks {
2023                    crate::cfn::actions::next_preferences(self);
2024                } else if self.current_service == Service::CloudTrailEvents {
2025                    if self.cloudtrail_state.current_event.is_some()
2026                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2027                    {
2028                        // Resources table has no page size, just columns
2029                        self.column_selector_index = 0;
2030                    } else {
2031                        let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2032                        if self.column_selector_index < page_size_idx {
2033                            self.column_selector_index = page_size_idx;
2034                        } else {
2035                            self.column_selector_index = 0;
2036                        }
2037                    }
2038                } else if self.current_service == Service::Ec2Instances {
2039                    let page_size_idx = self.ec2_column_ids.len() + 2;
2040                    if self.column_selector_index < page_size_idx {
2041                        self.column_selector_index = page_size_idx;
2042                    } else {
2043                        self.column_selector_index = 0;
2044                    }
2045                } else if self.current_service == Service::IamUsers {
2046                    crate::iam::actions::next_preferences_users(self);
2047                } else if self.current_service == Service::IamRoles {
2048                    crate::iam::actions::next_preferences_roles(self);
2049                } else if self.current_service == Service::IamUserGroups {
2050                    crate::iam::actions::next_preferences_groups(self);
2051                } else if self.current_service == Service::SqsQueues
2052                    && self.sqs_state.current_queue.is_some()
2053                    && self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
2054                {
2055                    // Triggers tab: Columns(0), PageSize(columns.len() + 2)
2056                    let page_size_idx = self.sqs_state.trigger_column_ids.len() + 2;
2057                    if self.column_selector_index < page_size_idx {
2058                        self.column_selector_index = page_size_idx;
2059                    } else {
2060                        self.column_selector_index = 0;
2061                    }
2062                } else if self.current_service == Service::SqsQueues
2063                    && self.sqs_state.current_queue.is_some()
2064                    && self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
2065                {
2066                    // Pipes tab: Columns(0), PageSize(columns.len() + 2)
2067                    let page_size_idx = self.sqs_state.pipe_column_ids.len() + 2;
2068                    if self.column_selector_index < page_size_idx {
2069                        self.column_selector_index = page_size_idx;
2070                    } else {
2071                        self.column_selector_index = 0;
2072                    }
2073                } else if self.current_service == Service::SqsQueues
2074                    && self.sqs_state.current_queue.is_some()
2075                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
2076                {
2077                    // Tags tab: Columns(0), PageSize(columns.len() + 2)
2078                    let page_size_idx = self.sqs_state.tag_column_ids.len() + 2;
2079                    if self.column_selector_index < page_size_idx {
2080                        self.column_selector_index = page_size_idx;
2081                    } else {
2082                        self.column_selector_index = 0;
2083                    }
2084                } else if self.current_service == Service::SqsQueues
2085                    && self.sqs_state.current_queue.is_some()
2086                    && self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions
2087                {
2088                    // Subscriptions tab: Columns(0), PageSize(columns.len() + 2)
2089                    let page_size_idx = self.sqs_state.subscription_column_ids.len() + 2;
2090                    if self.column_selector_index < page_size_idx {
2091                        self.column_selector_index = page_size_idx;
2092                    } else {
2093                        self.column_selector_index = 0;
2094                    }
2095                } else if self.current_service == Service::S3Buckets
2096                    && self.s3_state.current_bucket.is_none()
2097                {
2098                    crate::s3::actions::next_preferences(self);
2099                } else if self.current_service == Service::CloudWatchLogGroups {
2100                    if self.view_mode == ViewMode::Events {
2101                        // Events view: only columns, no sections to cycle
2102                    } else if self.view_mode == ViewMode::Detail {
2103                        if self.log_groups_state.detail_tab == DetailTab::Tags {
2104                            // Tags tab: Columns(0), PageSize(columns.len() + 2)
2105                            cycle_preference_next(
2106                                &mut self.column_selector_index,
2107                                self.cw_log_tag_column_ids.len(),
2108                            );
2109                        } else {
2110                            // Streams view: Columns(0), PageSize(columns.len() + 2)
2111                            cycle_preference_next(
2112                                &mut self.column_selector_index,
2113                                self.cw_log_stream_column_ids.len(),
2114                            );
2115                        }
2116                    } else {
2117                        // Log groups view: Columns(0), PageSize(columns.len() + 2)
2118                        cycle_preference_next(
2119                            &mut self.column_selector_index,
2120                            self.cw_log_group_column_ids.len(),
2121                        );
2122                    }
2123                }
2124            }
2125            Action::PrevPreferences => {
2126                if self.current_service == Service::ApiGatewayApis {
2127                    crate::apig::actions::prev_preferences(self);
2128                } else if self.current_service == Service::CloudWatchAlarms {
2129                    crate::cw::actions::alarms_prev_preferences(self);
2130                } else if self.current_service == Service::EcrRepositories
2131                    && self.ecr_state.current_repository.is_some()
2132                {
2133                    crate::ecr::actions::prev_preferences(self);
2134                } else if self.current_service == Service::KmsKeys {
2135                    crate::kms::actions::prev_preferences(self);
2136                } else if self.current_service == Service::EfsFileSystems {
2137                    crate::efs::actions::prev_preferences(self);
2138                } else if self.current_service == Service::FsxFileSystems {
2139                    crate::fsx::actions::prev_preferences(self);
2140                } else if self.current_service == Service::LambdaFunctions {
2141                    crate::lambda::functions::prev_preferences(self);
2142                } else if self.current_service == Service::LambdaApplications {
2143                    crate::lambda::applications::prev_preferences(self);
2144                } else if self.current_service == Service::CloudFormationStacks {
2145                    crate::cfn::actions::prev_preferences(self);
2146                } else if self.current_service == Service::CloudTrailEvents {
2147                    if self.cloudtrail_state.current_event.is_some()
2148                        && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
2149                    {
2150                        // Resources table has no page size, just columns
2151                        self.column_selector_index = 0;
2152                    } else {
2153                        let page_size_idx = self.cloudtrail_event_column_ids.len() + 2;
2154                        if self.column_selector_index >= page_size_idx {
2155                            self.column_selector_index = 0;
2156                        } else {
2157                            self.column_selector_index = page_size_idx;
2158                        }
2159                    }
2160                } else if self.current_service == Service::Ec2Instances {
2161                    crate::ec2::actions::next_preferences(self);
2162                } else if self.current_service == Service::IamUsers {
2163                    crate::iam::actions::prev_preferences_users(self);
2164                } else if self.current_service == Service::IamRoles {
2165                    crate::iam::actions::prev_preferences_roles(self);
2166                } else if self.current_service == Service::IamUserGroups {
2167                    crate::iam::actions::prev_preferences_groups(self);
2168                } else if self.current_service == Service::SqsQueues
2169                    && self.sqs_state.current_queue.is_some()
2170                {
2171                    let page_size_idx = match self.sqs_state.detail_tab {
2172                        SqsQueueDetailTab::LambdaTriggers => {
2173                            self.sqs_state.trigger_column_ids.len() + 2
2174                        }
2175                        SqsQueueDetailTab::EventBridgePipes => {
2176                            self.sqs_state.pipe_column_ids.len() + 2
2177                        }
2178                        SqsQueueDetailTab::Tagging => self.sqs_state.tag_column_ids.len() + 2,
2179                        SqsQueueDetailTab::SnsSubscriptions => {
2180                            self.sqs_state.subscription_column_ids.len() + 2
2181                        }
2182                        _ => 0,
2183                    };
2184                    if page_size_idx > 0 {
2185                        if self.column_selector_index >= page_size_idx {
2186                            self.column_selector_index = 0;
2187                        } else {
2188                            self.column_selector_index = page_size_idx;
2189                        }
2190                    }
2191                } else if self.current_service == Service::S3Buckets
2192                    && self.s3_state.current_bucket.is_none()
2193                {
2194                    crate::s3::actions::prev_preferences(self);
2195                } else if self.current_service == Service::CloudWatchLogGroups {
2196                    if self.view_mode == ViewMode::Events {
2197                        // Events view: only columns, no sections to cycle
2198                    } else if self.view_mode == ViewMode::Detail {
2199                        if self.log_groups_state.detail_tab == DetailTab::Tags {
2200                            // Tags tab: Columns(0), PageSize(columns.len() + 2)
2201                            cycle_preference_prev(
2202                                &mut self.column_selector_index,
2203                                self.cw_log_tag_column_ids.len(),
2204                            );
2205                        } else {
2206                            // Streams view: Columns(0), PageSize(columns.len() + 2)
2207                            cycle_preference_prev(
2208                                &mut self.column_selector_index,
2209                                self.cw_log_stream_column_ids.len(),
2210                            );
2211                        }
2212                    } else {
2213                        // Log groups view: Columns(0), PageSize(columns.len() + 2)
2214                        cycle_preference_prev(
2215                            &mut self.column_selector_index,
2216                            self.cw_log_group_column_ids.len(),
2217                        );
2218                    }
2219                }
2220            }
2221            Action::CloseColumnSelector => {
2222                self.mode = Mode::Normal;
2223                self.preference_section = Preferences::Columns;
2224            }
2225            Action::NextDetailTab => {
2226                if self.current_service == Service::CloudTrailEvents
2227                    && self.cloudtrail_state.current_event.is_some()
2228                {
2229                    crate::cloudtrail::actions::next_detail_tab(self);
2230                } else if self.current_service == Service::ApiGatewayApis
2231                    && self.apig_state.current_api.is_some()
2232                {
2233                    crate::apig::actions::next_detail_tab(self);
2234                } else if self.current_service == Service::SqsQueues
2235                    && self.sqs_state.current_queue.is_some()
2236                {
2237                    crate::sqs::actions::next_detail_tab(self);
2238                } else if self.current_service == Service::Ec2Instances
2239                    && self.ec2_state.current_instance.is_some()
2240                {
2241                    crate::ec2::actions::next_detail_tab(self);
2242                } else if self.current_service == Service::LambdaApplications
2243                    && self.lambda_application_state.current_application.is_some()
2244                {
2245                    crate::lambda::applications::next_detail_tab(self);
2246                } else if self.current_service == Service::IamRoles
2247                    && self.iam_state.current_role.is_some()
2248                {
2249                    crate::iam::actions::next_detail_tab_roles(self);
2250                } else if self.current_service == Service::IamUsers
2251                    && self.iam_state.current_user.is_some()
2252                {
2253                    crate::iam::actions::next_detail_tab_users(self);
2254                } else if self.current_service == Service::IamUserGroups
2255                    && self.iam_state.current_group.is_some()
2256                {
2257                    crate::iam::actions::next_detail_tab_groups(self);
2258                } else if self.current_service == Service::CloudWatchAlarms {
2259                    crate::cw::actions::alarms_next_detail_tab(self);
2260                } else if self.view_mode == ViewMode::Detail {
2261                    self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
2262                    if self.log_groups_state.detail_tab == DetailTab::Tags {
2263                        self.log_groups_state.tags.loading = true;
2264                    }
2265                } else if self.current_service == Service::S3Buckets {
2266                    crate::s3::actions::next_detail_tab(self);
2267                } else if self.current_service == Service::EcrRepositories
2268                    && self.ecr_state.current_repository.is_none()
2269                {
2270                    crate::ecr::actions::next_detail_tab(self);
2271                } else if self.current_service == Service::KmsKeys {
2272                    crate::kms::actions::next_detail_tab(self);
2273                } else if self.current_service == Service::EfsFileSystems
2274                    && self.efs_state.current_file_system.is_some()
2275                {
2276                    crate::efs::actions::next_detail_tab(self);
2277                } else if self.current_service == Service::FsxFileSystems
2278                    && self.fsx_state.current_file_system.is_some()
2279                {
2280                    crate::fsx::actions::next_detail_tab(self);
2281                } else if self.current_service == Service::LambdaFunctions
2282                    && self.lambda_state.current_function.is_some()
2283                {
2284                    crate::lambda::functions::next_detail_tab(self);
2285                } else if self.current_service == Service::CloudFormationStacks
2286                    && self.cfn_state.current_stack.is_some()
2287                {
2288                    crate::cfn::actions::next_detail_tab(self);
2289                }
2290            }
2291            Action::PrevDetailTab => {
2292                if self.current_service == Service::CloudTrailEvents
2293                    && self.cloudtrail_state.current_event.is_some()
2294                {
2295                    crate::cloudtrail::actions::prev_detail_tab(self);
2296                } else if self.current_service == Service::ApiGatewayApis
2297                    && self.apig_state.current_api.is_some()
2298                {
2299                    crate::apig::actions::prev_detail_tab(self);
2300                } else if self.current_service == Service::SqsQueues
2301                    && self.sqs_state.current_queue.is_some()
2302                {
2303                    crate::sqs::actions::prev_detail_tab(self);
2304                } else if self.current_service == Service::Ec2Instances
2305                    && self.ec2_state.current_instance.is_some()
2306                {
2307                    crate::ec2::actions::prev_detail_tab(self);
2308                } else if self.current_service == Service::LambdaApplications
2309                    && self.lambda_application_state.current_application.is_some()
2310                {
2311                    crate::lambda::applications::prev_detail_tab(self);
2312                } else if self.current_service == Service::IamRoles
2313                    && self.iam_state.current_role.is_some()
2314                {
2315                    crate::iam::actions::prev_detail_tab_roles(self);
2316                } else if self.current_service == Service::IamUsers
2317                    && self.iam_state.current_user.is_some()
2318                {
2319                    crate::iam::actions::prev_detail_tab_users(self);
2320                } else if self.current_service == Service::IamUserGroups
2321                    && self.iam_state.current_group.is_some()
2322                {
2323                    crate::iam::actions::prev_detail_tab_groups(self);
2324                } else if self.current_service == Service::CloudWatchAlarms {
2325                    crate::cw::actions::alarms_prev_detail_tab(self);
2326                } else if self.view_mode == ViewMode::Detail {
2327                    self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
2328                } else if self.current_service == Service::S3Buckets {
2329                    crate::s3::actions::prev_detail_tab(self);
2330                } else if self.current_service == Service::EcrRepositories
2331                    && self.ecr_state.current_repository.is_none()
2332                {
2333                    crate::ecr::actions::prev_detail_tab(self);
2334                } else if self.current_service == Service::KmsKeys {
2335                    crate::kms::actions::prev_detail_tab(self);
2336                } else if self.current_service == Service::EfsFileSystems
2337                    && self.efs_state.current_file_system.is_some()
2338                {
2339                    crate::efs::actions::prev_detail_tab(self);
2340                } else if self.current_service == Service::FsxFileSystems
2341                    && self.fsx_state.current_file_system.is_some()
2342                {
2343                    crate::fsx::actions::prev_detail_tab(self);
2344                } else if self.current_service == Service::LambdaFunctions
2345                    && self.lambda_state.current_function.is_some()
2346                {
2347                    crate::lambda::functions::prev_detail_tab(self);
2348                } else if self.current_service == Service::CloudFormationStacks
2349                    && self.cfn_state.current_stack.is_some()
2350                {
2351                    crate::cfn::actions::prev_detail_tab(self);
2352                }
2353            }
2354            Action::StartFilter => {
2355                // Don't allow filter mode when no service is selected and no tabs open
2356                if !self.service_selected && self.tabs.is_empty() {
2357                    return;
2358                }
2359
2360                if self.current_service == Service::CloudWatchInsights {
2361                    self.mode = Mode::InsightsInput;
2362                } else if self.current_service == Service::CloudWatchAlarms {
2363                    self.mode = Mode::FilterInput;
2364                } else if self.current_service == Service::CloudTrailEvents {
2365                    self.mode = Mode::FilterInput;
2366                    self.cloudtrail_state.input_focus = InputFocus::Filter;
2367                } else if self.current_service == Service::S3Buckets {
2368                    self.mode = Mode::FilterInput;
2369                    crate::s3::actions::start_filter(self);
2370                } else if self.current_service == Service::ApiGatewayApis
2371                    || self.current_service == Service::EcrRepositories
2372                    || self.current_service == Service::KmsKeys
2373                    || self.current_service == Service::EfsFileSystems
2374                    || self.current_service == Service::FsxFileSystems
2375                    || self.current_service == Service::IamUsers
2376                    || self.current_service == Service::IamUserGroups
2377                {
2378                    self.mode = Mode::FilterInput;
2379                    if self.current_service == Service::ApiGatewayApis {
2380                        crate::apig::actions::start_filter(self);
2381                    } else if self.current_service == Service::EcrRepositories
2382                        && self.ecr_state.current_repository.is_none()
2383                    {
2384                        self.ecr_state.input_focus = InputFocus::Filter;
2385                    } else if self.current_service == Service::KmsKeys {
2386                        crate::kms::actions::start_filter(self);
2387                    } else if self.current_service == Service::EfsFileSystems {
2388                        crate::efs::actions::start_filter(self);
2389                    } else if self.current_service == Service::FsxFileSystems {
2390                        crate::fsx::actions::start_filter(self);
2391                    }
2392                } else if self.current_service == Service::LambdaFunctions {
2393                    self.mode = Mode::FilterInput;
2394                    crate::lambda::functions::start_filter(self);
2395                } else if self.current_service == Service::LambdaApplications {
2396                    self.mode = Mode::FilterInput;
2397                    crate::lambda::applications::start_filter(self);
2398                } else if self.current_service == Service::IamRoles {
2399                    self.mode = Mode::FilterInput;
2400                } else if self.current_service == Service::CloudFormationStacks {
2401                    self.mode = Mode::FilterInput;
2402                    crate::cfn::actions::start_filter(self);
2403                } else if self.current_service == Service::SqsQueues {
2404                    self.mode = Mode::FilterInput;
2405                    self.sqs_state.input_focus = InputFocus::Filter;
2406                } else if self.view_mode == ViewMode::List
2407                    || (self.view_mode == ViewMode::Detail
2408                        && (self.log_groups_state.detail_tab == DetailTab::LogStreams
2409                            || self.log_groups_state.detail_tab == DetailTab::Tags))
2410                {
2411                    self.mode = Mode::FilterInput;
2412                    self.log_groups_state.filter_mode = true;
2413                    self.log_groups_state.input_focus = InputFocus::Filter;
2414                } else if self.view_mode == ViewMode::Events {
2415                    self.mode = Mode::EventFilterInput;
2416                }
2417            }
2418            Action::StartEventFilter => {
2419                if self.current_service == Service::CloudWatchInsights {
2420                    self.mode = Mode::InsightsInput;
2421                } else if self.view_mode == ViewMode::List {
2422                    self.mode = Mode::FilterInput;
2423                    self.log_groups_state.filter_mode = true;
2424                    self.log_groups_state.input_focus = InputFocus::Filter;
2425                } else if self.view_mode == ViewMode::Events {
2426                    self.mode = Mode::EventFilterInput;
2427                } else if self.view_mode == ViewMode::Detail
2428                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2429                {
2430                    self.mode = Mode::FilterInput;
2431                    self.log_groups_state.filter_mode = true;
2432                    self.log_groups_state.input_focus = InputFocus::Filter;
2433                }
2434            }
2435            Action::NextFilterFocus => {
2436                if self.current_service == Service::CloudTrailEvents
2437                    && self.cloudtrail_state.current_event.is_some()
2438                {
2439                    self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
2440                } else if self.mode == Mode::FilterInput
2441                    && self.current_service == Service::S3Buckets
2442                {
2443                    crate::s3::actions::next_filter_focus(self);
2444                } else if self.mode == Mode::FilterInput
2445                    && self.current_service == Service::Ec2Instances
2446                {
2447                    crate::ec2::actions::next_filter_focus(self);
2448                } else if self.mode == Mode::FilterInput
2449                    && self.current_service == Service::LambdaApplications
2450                {
2451                    crate::lambda::applications::next_filter_focus(self);
2452                } else if self.mode == Mode::FilterInput
2453                    && self.current_service == Service::IamRoles
2454                {
2455                    crate::iam::actions::next_filter_focus_roles(self);
2456                } else if self.mode == Mode::FilterInput
2457                    && self.current_service == Service::IamUsers
2458                    && self.iam_state.current_user.is_some()
2459                {
2460                    crate::iam::actions::next_filter_focus_users(self);
2461                } else if self.mode == Mode::FilterInput
2462                    && self.current_service == Service::IamUserGroups
2463                {
2464                    crate::iam::actions::next_filter_focus_groups(self);
2465                } else if self.mode == Mode::InsightsInput {
2466                    use crate::app::InsightsFocus;
2467                    self.insights_state.insights.insights_focus =
2468                        match self.insights_state.insights.insights_focus {
2469                            InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
2470                            InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
2471                            InsightsFocus::LogGroupSearch => InsightsFocus::Query,
2472                            InsightsFocus::Query => InsightsFocus::QueryLanguage,
2473                        };
2474                } else if self.mode == Mode::FilterInput
2475                    && self.current_service == Service::CloudFormationStacks
2476                {
2477                    crate::cfn::actions::next_filter_focus(self);
2478                } else if self.mode == Mode::FilterInput
2479                    && self.current_service == Service::SqsQueues
2480                {
2481                    crate::sqs::actions::next_filter_focus(self);
2482                } else if self.mode == Mode::FilterInput
2483                    && self.current_service == Service::CloudWatchLogGroups
2484                {
2485                    crate::cw::actions::logs_next_filter_focus(self);
2486                } else if self.mode == Mode::EventFilterInput {
2487                    crate::cw::actions::logs_next_event_filter_focus(self);
2488                } else if self.mode == Mode::FilterInput
2489                    && self.current_service == Service::CloudWatchAlarms
2490                {
2491                    crate::cw::actions::alarms_next_filter_focus(self);
2492                } else if self.mode == Mode::FilterInput
2493                    && self.current_service == Service::CloudTrailEvents
2494                {
2495                    crate::cloudtrail::actions::next_filter_focus(self);
2496                } else if self.mode == Mode::FilterInput
2497                    && self.current_service == Service::ApiGatewayApis
2498                {
2499                    crate::apig::actions::next_filter_focus(self);
2500                } else if self.mode == Mode::FilterInput
2501                    && self.current_service == Service::EcrRepositories
2502                    && self.ecr_state.current_repository.is_none()
2503                {
2504                    crate::ecr::actions::next_filter_focus(self);
2505                } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys
2506                {
2507                    crate::kms::actions::next_filter_focus(self);
2508                } else if self.mode == Mode::FilterInput
2509                    && self.current_service == Service::EfsFileSystems
2510                {
2511                    crate::efs::actions::next_filter_focus(self);
2512                } else if self.mode == Mode::FilterInput
2513                    && self.current_service == Service::FsxFileSystems
2514                {
2515                    crate::fsx::actions::next_filter_focus(self);
2516                } else if self.mode == Mode::FilterInput
2517                    && self.current_service == Service::LambdaFunctions
2518                {
2519                    crate::lambda::functions::next_filter_focus(self);
2520                }
2521            }
2522            Action::PrevFilterFocus => {
2523                if self.current_service == Service::CloudTrailEvents
2524                    && self.cloudtrail_state.current_event.is_some()
2525                {
2526                    self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
2527                } else if self.mode == Mode::FilterInput
2528                    && self.current_service == Service::ApiGatewayApis
2529                {
2530                    crate::apig::actions::prev_filter_focus(self);
2531                } else if self.mode == Mode::FilterInput
2532                    && self.current_service == Service::S3Buckets
2533                {
2534                    crate::s3::actions::prev_filter_focus(self);
2535                } else if self.mode == Mode::FilterInput
2536                    && self.current_service == Service::Ec2Instances
2537                {
2538                    crate::ec2::actions::prev_filter_focus(self);
2539                } else if self.mode == Mode::FilterInput
2540                    && self.current_service == Service::LambdaApplications
2541                {
2542                    crate::lambda::applications::prev_filter_focus(self);
2543                } else if self.mode == Mode::FilterInput
2544                    && self.current_service == Service::CloudFormationStacks
2545                {
2546                    crate::cfn::actions::prev_filter_focus(self);
2547                } else if self.mode == Mode::FilterInput
2548                    && self.current_service == Service::SqsQueues
2549                {
2550                    crate::sqs::actions::prev_filter_focus(self);
2551                } else if self.mode == Mode::FilterInput
2552                    && self.current_service == Service::IamRoles
2553                {
2554                    crate::iam::actions::prev_filter_focus_roles(self);
2555                } else if self.mode == Mode::FilterInput
2556                    && self.current_service == Service::IamUsers
2557                    && self.iam_state.current_user.is_some()
2558                {
2559                    crate::iam::actions::prev_filter_focus_users(self);
2560                } else if self.mode == Mode::FilterInput
2561                    && self.current_service == Service::IamUserGroups
2562                {
2563                    crate::iam::actions::prev_filter_focus_groups(self);
2564                } else if self.mode == Mode::FilterInput
2565                    && self.current_service == Service::CloudWatchLogGroups
2566                {
2567                    crate::cw::actions::logs_prev_filter_focus(self);
2568                } else if self.mode == Mode::EventFilterInput {
2569                    crate::cw::actions::logs_prev_event_filter_focus(self);
2570                } else if self.mode == Mode::FilterInput
2571                    && self.current_service == Service::IamRoles
2572                    && self.iam_state.current_role.is_some()
2573                {
2574                    crate::iam::actions::prev_filter_focus_roles(self);
2575                } else if self.mode == Mode::FilterInput
2576                    && self.current_service == Service::CloudWatchAlarms
2577                {
2578                    crate::cw::actions::alarms_prev_filter_focus(self);
2579                } else if self.mode == Mode::FilterInput
2580                    && self.current_service == Service::CloudTrailEvents
2581                {
2582                    crate::cloudtrail::actions::prev_filter_focus(self);
2583                } else if self.mode == Mode::FilterInput
2584                    && self.current_service == Service::EcrRepositories
2585                    && self.ecr_state.current_repository.is_none()
2586                {
2587                    crate::ecr::actions::prev_filter_focus(self);
2588                } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys
2589                {
2590                    crate::kms::actions::prev_filter_focus(self);
2591                } else if self.mode == Mode::FilterInput
2592                    && self.current_service == Service::EfsFileSystems
2593                {
2594                    crate::efs::actions::prev_filter_focus(self);
2595                } else if self.mode == Mode::FilterInput
2596                    && self.current_service == Service::FsxFileSystems
2597                {
2598                    crate::fsx::actions::prev_filter_focus(self);
2599                } else if self.mode == Mode::FilterInput
2600                    && self.current_service == Service::LambdaFunctions
2601                {
2602                    crate::lambda::functions::prev_filter_focus(self);
2603                }
2604            }
2605            Action::ToggleFilterCheckbox => {
2606                if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
2607                    crate::ec2::actions::toggle_filter_checkbox(self);
2608                } else if self.mode == Mode::InsightsInput
2609                    || (self.mode == Mode::Normal
2610                        && self.current_service == Service::CloudWatchInsights)
2611                {
2612                    use crate::app::InsightsFocus;
2613                    if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2614                        && self.insights_state.insights.show_dropdown
2615                        && !self.insights_state.insights.log_group_matches.is_empty()
2616                    {
2617                        let selected_idx = self.insights_state.insights.dropdown_selected;
2618                        if let Some(group_name) = self
2619                            .insights_state
2620                            .insights
2621                            .log_group_matches
2622                            .get(selected_idx)
2623                        {
2624                            let group_name = group_name.clone();
2625                            if let Some(pos) = self
2626                                .insights_state
2627                                .insights
2628                                .selected_log_groups
2629                                .iter()
2630                                .position(|g| g == &group_name)
2631                            {
2632                                self.insights_state.insights.selected_log_groups.remove(pos);
2633                            } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2634                                self.insights_state
2635                                    .insights
2636                                    .selected_log_groups
2637                                    .push(group_name);
2638                            }
2639                        }
2640                    }
2641                } else if self.mode == Mode::FilterInput
2642                    && self.current_service == Service::CloudFormationStacks
2643                {
2644                    crate::cfn::actions::toggle_filter_checkbox(self);
2645                } else if self.mode == Mode::FilterInput
2646                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2647                {
2648                    match self.log_groups_state.input_focus {
2649                        InputFocus::Checkbox("ExactMatch") => {
2650                            self.log_groups_state.exact_match = !self.log_groups_state.exact_match
2651                        }
2652                        InputFocus::Checkbox("ShowExpired") => {
2653                            self.log_groups_state.show_expired = !self.log_groups_state.show_expired
2654                        }
2655                        _ => {}
2656                    }
2657                } else if self.mode == Mode::EventFilterInput
2658                    && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
2659                {
2660                    self.log_groups_state.relative_unit =
2661                        self.log_groups_state.relative_unit.next();
2662                }
2663            }
2664            Action::CycleSortColumn => {
2665                if self.view_mode == ViewMode::Detail
2666                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2667                {
2668                    self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
2669                        StreamSort::Name => StreamSort::CreationTime,
2670                        StreamSort::CreationTime => StreamSort::LastEventTime,
2671                        StreamSort::LastEventTime => StreamSort::Name,
2672                    };
2673                }
2674            }
2675            Action::ToggleSortDirection => {
2676                if self.view_mode == ViewMode::Detail
2677                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
2678                {
2679                    self.log_groups_state.stream_sort_desc =
2680                        !self.log_groups_state.stream_sort_desc;
2681                }
2682            }
2683            Action::ScrollUp => {
2684                if self.mode == Mode::ErrorModal {
2685                    self.error_scroll = self.error_scroll.saturating_sub(1);
2686                } else if self.current_service == Service::CloudTrailEvents
2687                    && self.cloudtrail_state.current_event.is_some()
2688                {
2689                    self.cloudtrail_state.event_json_scroll =
2690                        self.cloudtrail_state.event_json_scroll.saturating_sub(10);
2691                } else if self.current_service == Service::LambdaFunctions {
2692                    crate::lambda::functions::scroll_up(self);
2693                } else if self.current_service == Service::Ec2Instances {
2694                    crate::ec2::actions::scroll_up(self);
2695                } else if self.current_service == Service::SqsQueues
2696                    && self.sqs_state.current_queue.is_some()
2697                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2698                    && !self.sqs_state.is_metrics_loading()
2699                {
2700                    self.sqs_state.set_monitoring_scroll(
2701                        self.sqs_state.monitoring_scroll().saturating_sub(1),
2702                    );
2703                } else if self.current_service == Service::EfsFileSystems
2704                    && self.efs_state.current_file_system.is_some()
2705                    && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
2706                        || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
2707                    && !self.efs_state.is_metrics_loading()
2708                {
2709                    crate::efs::actions::scroll_up_detail(self);
2710                } else if self.view_mode == ViewMode::PolicyView {
2711                    crate::iam::actions::scroll_up_policy_view(self);
2712                } else if self.current_service == Service::IamRoles
2713                    && self.iam_state.current_role.is_some()
2714                    && self.iam_state.role_tab == RoleTab::TrustRelationships
2715                {
2716                    crate::iam::actions::scroll_up_trust_policy(self);
2717                } else if self.view_mode == ViewMode::Events {
2718                    if self.log_groups_state.event_scroll_offset == 0
2719                        && self.log_groups_state.has_older_events
2720                    {
2721                        self.log_groups_state.loading = true;
2722                    } else {
2723                        self.log_groups_state.event_scroll_offset =
2724                            self.log_groups_state.event_scroll_offset.saturating_sub(1);
2725                    }
2726                } else if self.view_mode == ViewMode::InsightsResults {
2727                    self.insights_state.insights.results_selected = self
2728                        .insights_state
2729                        .insights
2730                        .results_selected
2731                        .saturating_sub(1);
2732                } else if self.view_mode == ViewMode::Detail {
2733                    self.log_groups_state.selected_stream =
2734                        self.log_groups_state.selected_stream.saturating_sub(1);
2735                    self.log_groups_state.expanded_stream = None;
2736                } else if self.view_mode == ViewMode::List
2737                    && self.current_service == Service::CloudWatchLogGroups
2738                {
2739                    self.log_groups_state.log_groups.selected =
2740                        self.log_groups_state.log_groups.selected.saturating_sub(1);
2741                    self.log_groups_state.log_groups.snap_to_page();
2742                } else if self.current_service == Service::EcrRepositories {
2743                    crate::ecr::actions::scroll_up(self);
2744                }
2745            }
2746            Action::ScrollDown => {
2747                if self.mode == Mode::ErrorModal {
2748                    if let Some(error_msg) = &self.error_message {
2749                        let lines = error_msg.lines().count();
2750                        let max_scroll = lines.saturating_sub(1);
2751                        self.error_scroll = (self.error_scroll + 1).min(max_scroll);
2752                    }
2753                } else if self.current_service == Service::CloudTrailEvents
2754                    && self.cloudtrail_state.current_event.is_some()
2755                {
2756                    if let Some(event) = &self.cloudtrail_state.current_event {
2757                        let lines = event.cloud_trail_event_json.lines().count();
2758                        let max_scroll = lines.saturating_sub(1);
2759                        self.cloudtrail_state.event_json_scroll =
2760                            (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
2761                    }
2762                } else if self.current_service == Service::SqsQueues
2763                    && self.sqs_state.current_queue.is_some()
2764                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2765                {
2766                    self.sqs_state
2767                        .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
2768                } else if self.current_service == Service::EfsFileSystems
2769                    && self.efs_state.current_file_system.is_some()
2770                    && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
2771                        || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
2772                {
2773                    crate::efs::actions::scroll_down_detail(self);
2774                } else if self.view_mode == ViewMode::PolicyView {
2775                    crate::iam::actions::scroll_down_policy_view(self);
2776                } else if self.current_service == Service::IamRoles
2777                    && self.iam_state.current_role.is_some()
2778                    && self.iam_state.role_tab == RoleTab::TrustRelationships
2779                {
2780                    crate::iam::actions::scroll_down_trust_policy(self);
2781                } else if self.view_mode == ViewMode::Events {
2782                    let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
2783                    if self.log_groups_state.event_scroll_offset >= max_scroll {
2784                        // At the end, do nothing
2785                    } else {
2786                        self.log_groups_state.event_scroll_offset =
2787                            (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
2788                    }
2789                } else if self.view_mode == ViewMode::InsightsResults {
2790                    let max = self
2791                        .insights_state
2792                        .insights
2793                        .query_results
2794                        .len()
2795                        .saturating_sub(1);
2796                    self.insights_state.insights.results_selected =
2797                        (self.insights_state.insights.results_selected + 1).min(max);
2798                } else if self.view_mode == ViewMode::Detail {
2799                    let filtered_streams = filtered_log_streams(self);
2800                    let max = filtered_streams.len().saturating_sub(1);
2801                    self.log_groups_state.selected_stream =
2802                        (self.log_groups_state.selected_stream + 1).min(max);
2803                } else if self.view_mode == ViewMode::List
2804                    && self.current_service == Service::CloudWatchLogGroups
2805                {
2806                    let filtered_groups = filtered_log_groups(self);
2807                    self.log_groups_state
2808                        .log_groups
2809                        .next_item(filtered_groups.len());
2810                } else if self.current_service == Service::EcrRepositories {
2811                    crate::ecr::actions::scroll_down(self);
2812                }
2813            }
2814
2815            Action::MeasureLatency => {
2816                if self.mode == Mode::RegionPicker {
2817                    self.measure_region_latencies();
2818                }
2819            }
2820            Action::Refresh => {
2821                if self.mode == Mode::ProfilePicker {
2822                    self.log_groups_state.loading = true;
2823                } else if self.mode == Mode::RegionPicker {
2824                    // Ctrl+R in region picker just refreshes — latency is via Ctrl+L
2825                } else if self.mode == Mode::SessionPicker {
2826                    self.sessions = Session::list_all().unwrap_or_default();
2827                } else if self.current_service == Service::CloudWatchAlarms
2828                    && self.alarms_state.current_alarm.is_some()
2829                {
2830                    crate::cw::actions::alarms_refresh(self);
2831                } else if self.current_service == Service::CloudWatchInsights
2832                    && !self.insights_state.insights.selected_log_groups.is_empty()
2833                {
2834                    crate::cw::actions::insights_refresh(self);
2835                } else if self.current_service == Service::LambdaFunctions {
2836                    crate::lambda::functions::refresh(self);
2837                } else if self.current_service == Service::LambdaApplications {
2838                    crate::lambda::applications::refresh(self);
2839                } else if matches!(
2840                    self.view_mode,
2841                    ViewMode::Events | ViewMode::Detail | ViewMode::List
2842                ) {
2843                    self.log_groups_state.loading = true;
2844                }
2845            }
2846            Action::Yank => {
2847                if self.mode == Mode::ErrorModal {
2848                    // Copy error message
2849                    if let Some(error) = &self.error_message {
2850                        copy_to_clipboard(error);
2851                    }
2852                } else if self.view_mode == ViewMode::Events {
2853                    if let Some(event) = self
2854                        .log_groups_state
2855                        .log_events
2856                        .get(self.log_groups_state.event_scroll_offset)
2857                    {
2858                        copy_to_clipboard(&event.message);
2859                    }
2860                } else if self.current_service == Service::EcrRepositories {
2861                    crate::ecr::actions::yank(self);
2862                } else if self.current_service == Service::KmsKeys {
2863                    crate::kms::actions::yank(self);
2864                } else if self.current_service == Service::EfsFileSystems {
2865                    crate::efs::actions::yank(self);
2866                } else if self.current_service == Service::FsxFileSystems {
2867                    crate::fsx::actions::yank(self);
2868                } else if self.current_service == Service::LambdaFunctions {
2869                    crate::lambda::functions::yank(self);
2870                } else if self.current_service == Service::CloudFormationStacks {
2871                    crate::cfn::actions::yank(self);
2872                } else if self.current_service == Service::CloudTrailEvents
2873                    && self.cloudtrail_state.current_event.is_some()
2874                {
2875                    crate::cloudtrail::actions::yank(self);
2876                } else if self.current_service == Service::IamUsers {
2877                    crate::iam::actions::yank_users(self);
2878                } else if self.current_service == Service::IamRoles {
2879                    crate::iam::actions::yank_roles(self);
2880                } else if self.current_service == Service::IamUserGroups {
2881                    crate::iam::actions::yank_groups(self);
2882                } else if self.current_service == Service::SqsQueues {
2883                    if self.sqs_state.current_queue.is_some() {
2884                        // In queue detail view - copy queue ARN
2885                        if let Some(queue) = self
2886                            .sqs_state
2887                            .queues
2888                            .items
2889                            .iter()
2890                            .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
2891                        {
2892                            let arn = format!(
2893                                "arn:aws:sqs:{}:{}:{}",
2894                                extract_region(&queue.url),
2895                                extract_account_id(&queue.url),
2896                                queue.name
2897                            );
2898                            copy_to_clipboard(&arn);
2899                        }
2900                    } else {
2901                        // In list view - copy selected queue ARN
2902                        let filtered_queues = filtered_queues(
2903                            &self.sqs_state.queues.items,
2904                            &self.sqs_state.queues.filter,
2905                        );
2906                        if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
2907                            let arn = format!(
2908                                "arn:aws:sqs:{}:{}:{}",
2909                                extract_region(&queue.url),
2910                                extract_account_id(&queue.url),
2911                                queue.name
2912                            );
2913                            copy_to_clipboard(&arn);
2914                        }
2915                    }
2916                } else if self.current_service == Service::ApiGatewayApis {
2917                    crate::apig::actions::yank(self);
2918                }
2919            }
2920            Action::CopyToClipboard => {
2921                // Request snapshot - will be captured after next render
2922                self.snapshot_requested = true;
2923            }
2924            Action::RetryLoad => {
2925                self.error_message = None;
2926                self.mode = Mode::Normal;
2927                self.log_groups_state.loading = true;
2928            }
2929            Action::ApplyFilter => {
2930                if self.mode == Mode::FilterInput
2931                    && self.current_service == Service::SqsQueues
2932                    && self.sqs_state.input_focus == InputFocus::Dropdown("SubscriptionRegion")
2933                {
2934                    let regions = AwsRegion::all();
2935                    if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
2936                        self.sqs_state.subscription_region_filter = region.code.to_string();
2937                    }
2938                    self.mode = Mode::Normal;
2939                } else if self.mode == Mode::FilterInput
2940                    && self.current_service == Service::CloudTrailEvents
2941                {
2942                    // Apply filter: set API-level EventName filter and reload
2943                    let filter_text = self.cloudtrail_state.table.filter.trim().to_string();
2944                    self.cloudtrail_state.active_event_name_filter = if filter_text.is_empty() {
2945                        None
2946                    } else {
2947                        Some(filter_text)
2948                    };
2949                    // Clear existing items and trigger reload from API
2950                    self.cloudtrail_state.table.items.clear();
2951                    self.cloudtrail_state.table.next_token = None;
2952                    self.cloudtrail_state.table.selected = 0;
2953                    self.cloudtrail_state.table.scroll_offset = 0;
2954                    self.cloudtrail_state.table.loading = true;
2955                    self.mode = Mode::Normal;
2956                } else if self.mode == Mode::InsightsInput {
2957                    use crate::app::InsightsFocus;
2958                    if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2959                        && self.insights_state.insights.show_dropdown
2960                    {
2961                        // First toggle the currently highlighted entry (select it)
2962                        let selected_idx = self.insights_state.insights.dropdown_selected;
2963                        if let Some(group_name) = self
2964                            .insights_state
2965                            .insights
2966                            .log_group_matches
2967                            .get(selected_idx)
2968                            .cloned()
2969                        {
2970                            if let Some(pos) = self
2971                                .insights_state
2972                                .insights
2973                                .selected_log_groups
2974                                .iter()
2975                                .position(|g| g == &group_name)
2976                            {
2977                                self.insights_state.insights.selected_log_groups.remove(pos);
2978                            } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2979                                self.insights_state
2980                                    .insights
2981                                    .selected_log_groups
2982                                    .push(group_name);
2983                            }
2984                        }
2985                        // Close dropdown and clear search
2986                        self.insights_state.insights.show_dropdown = false;
2987                        self.insights_state.insights.log_group_search.clear();
2988                        self.insights_state.insights.log_group_matches.clear();
2989                        self.mode = Mode::Normal;
2990                    }
2991                } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
2992                    if let Ok(page) = self.page_input.parse::<usize>() {
2993                        self.go_to_page(page);
2994                    }
2995                    self.page_input.clear();
2996                } else {
2997                    self.mode = Mode::Normal;
2998                    self.log_groups_state.filter_mode = false;
2999                }
3000            }
3001            Action::ToggleExactMatch => {
3002                if self.view_mode == ViewMode::Detail
3003                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3004                {
3005                    self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
3006                }
3007            }
3008            Action::ToggleShowExpired => {
3009                if self.view_mode == ViewMode::Detail
3010                    && self.log_groups_state.detail_tab == DetailTab::LogStreams
3011                {
3012                    self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
3013                }
3014            }
3015            Action::GoBack => {
3016                // ServicePicker: close if we have tabs
3017                if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
3018                    self.mode = Mode::Normal;
3019                    self.service_picker.filter.clear();
3020                }
3021                // API Gateway: go back from API detail to list
3022                else if self.current_service == Service::ApiGatewayApis
3023                    && self.apig_state.current_api.is_some()
3024                {
3025                    crate::apig::actions::go_back(self);
3026                }
3027                // S3: pop navigation stack first, then exit bucket
3028                else if self.current_service == Service::S3Buckets
3029                    && self.s3_state.current_bucket.is_some()
3030                {
3031                    crate::s3::actions::go_back(self);
3032                }
3033                // ECR: go back from images to repositories
3034                else if self.current_service == Service::EcrRepositories
3035                    && self.ecr_state.current_repository.is_some()
3036                {
3037                    crate::ecr::actions::go_back(self);
3038                } else if self.current_service == Service::EfsFileSystems
3039                    && self.efs_state.current_file_system.is_some()
3040                {
3041                    crate::efs::actions::go_back(self);
3042                } else if self.current_service == Service::FsxFileSystems
3043                    && self.fsx_state.current_file_system.is_some()
3044                {
3045                    crate::fsx::actions::go_back(self);
3046                }
3047                // EC2: go back from instance detail to list
3048                else if self.current_service == Service::Ec2Instances
3049                    && self.ec2_state.current_instance.is_some()
3050                {
3051                    crate::ec2::actions::go_back(self);
3052                }
3053                // SQS: go back from queue detail to list
3054                else if self.current_service == Service::SqsQueues
3055                    && self.sqs_state.current_queue.is_some()
3056                {
3057                    self.sqs_state.current_queue = None;
3058                }
3059                // IAM: go back
3060                else if self.current_service == Service::IamUsers
3061                    && self.iam_state.current_user.is_some()
3062                {
3063                    crate::iam::actions::go_back_users(self);
3064                } else if self.current_service == Service::IamUserGroups
3065                    && self.iam_state.current_group.is_some()
3066                {
3067                    crate::iam::actions::go_back_groups(self);
3068                } else if self.current_service == Service::IamRoles {
3069                    crate::iam::actions::go_back_roles(self);
3070                }
3071                // Lambda: go back from version/alias/function detail
3072                else if self.current_service == Service::LambdaFunctions
3073                    && (self.lambda_state.current_version.is_some()
3074                        || self.lambda_state.current_alias.is_some()
3075                        || self.lambda_state.current_function.is_some())
3076                {
3077                    crate::lambda::functions::go_back(self);
3078                }
3079                // CloudWatch Alarms: go back from alarm detail to list
3080                else if self.current_service == Service::CloudWatchAlarms
3081                    && self.alarms_state.current_alarm.is_some()
3082                {
3083                    crate::cw::actions::alarms_go_back(self);
3084                }
3085                // Lambda Applications: go back from application detail to list
3086                else if self.current_service == Service::LambdaApplications
3087                    && self.lambda_application_state.current_application.is_some()
3088                {
3089                    crate::lambda::applications::go_back(self);
3090                }
3091                // CloudFormation: go back from stack detail to list
3092                else if self.current_service == Service::CloudFormationStacks
3093                    && self.cfn_state.current_stack.is_some()
3094                {
3095                    crate::cfn::actions::go_back(self);
3096                }
3097                // CloudTrail: go back from event detail to list
3098                else if self.current_service == Service::CloudTrailEvents
3099                    && self.cloudtrail_state.current_event.is_some()
3100                {
3101                    crate::cloudtrail::actions::go_back(self);
3102                }
3103                // From insights results -> collapse if expanded, otherwise back to sidebar
3104                else if self.view_mode == ViewMode::InsightsResults {
3105                    if self.insights_state.insights.expanded_result.is_some() {
3106                        self.insights_state.insights.expanded_result = None;
3107                    }
3108                }
3109                // From alarms view -> collapse if expanded
3110                else if self.current_service == Service::CloudWatchAlarms {
3111                    crate::cw::actions::alarms_go_back_list(self);
3112                }
3113                // From EC2 instances view -> always collapse
3114                else if self.current_service == Service::Ec2Instances {
3115                    crate::ec2::actions::collapse_row(self);
3116                }
3117                // From events view -> collapse if expanded, otherwise back to detail view
3118                else if self.view_mode == ViewMode::Events {
3119                    if self.log_groups_state.expanded_event.is_some() {
3120                        self.log_groups_state.expanded_event = None;
3121                    } else {
3122                        self.view_mode = ViewMode::Detail;
3123                        self.log_groups_state.event_filter.clear();
3124                    }
3125                }
3126                // From detail view -> back to list view
3127                else if self.view_mode == ViewMode::Detail {
3128                    self.view_mode = ViewMode::List;
3129                    self.log_groups_state.stream_filter.clear();
3130                    self.log_groups_state.exact_match = false;
3131                    self.log_groups_state.show_expired = false;
3132                }
3133            }
3134            Action::OpenInConsole | Action::OpenInBrowser => {
3135                let url = self.get_console_url();
3136                let _ = webbrowser::open(&url);
3137            }
3138            Action::ShowHelp => {
3139                self.mode = Mode::HelpModal;
3140            }
3141            Action::OpenRegionPicker => {
3142                self.region_filter.clear();
3143                self.region_picker_selected = 0;
3144                self.mode = Mode::RegionPicker;
3145            }
3146            Action::OpenProfilePicker => {
3147                self.profile_filter.clear();
3148                self.profile_picker_selected = 0;
3149                self.available_profiles = Self::load_aws_profiles();
3150                self.mode = Mode::ProfilePicker;
3151            }
3152            Action::OpenCalendar => {
3153                self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
3154                self.calendar_selecting = CalendarField::StartDate;
3155                self.mode = Mode::CalendarPicker;
3156            }
3157            Action::CloseCalendar => {
3158                self.mode = Mode::Normal;
3159                self.calendar_date = None;
3160            }
3161            Action::CalendarPrevDay => {
3162                if let Some(date) = self.calendar_date {
3163                    self.calendar_date = date.checked_sub(time::Duration::days(1));
3164                }
3165            }
3166            Action::CalendarNextDay => {
3167                if let Some(date) = self.calendar_date {
3168                    self.calendar_date = date.checked_add(time::Duration::days(1));
3169                }
3170            }
3171            Action::CalendarPrevWeek => {
3172                if let Some(date) = self.calendar_date {
3173                    self.calendar_date = date.checked_sub(time::Duration::weeks(1));
3174                }
3175            }
3176            Action::CalendarNextWeek => {
3177                if let Some(date) = self.calendar_date {
3178                    self.calendar_date = date.checked_add(time::Duration::weeks(1));
3179                }
3180            }
3181            Action::CalendarPrevMonth => {
3182                if let Some(date) = self.calendar_date {
3183                    self.calendar_date = Some(if date.month() == time::Month::January {
3184                        date.replace_month(time::Month::December)
3185                            .unwrap()
3186                            .replace_year(date.year() - 1)
3187                            .unwrap()
3188                    } else {
3189                        date.replace_month(date.month().previous()).unwrap()
3190                    });
3191                }
3192            }
3193            Action::CalendarNextMonth => {
3194                if let Some(date) = self.calendar_date {
3195                    self.calendar_date = Some(if date.month() == time::Month::December {
3196                        date.replace_month(time::Month::January)
3197                            .unwrap()
3198                            .replace_year(date.year() + 1)
3199                            .unwrap()
3200                    } else {
3201                        date.replace_month(date.month().next()).unwrap()
3202                    });
3203                }
3204            }
3205            Action::CalendarSelect => {
3206                if let Some(date) = self.calendar_date {
3207                    let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
3208                        .unix_timestamp()
3209                        * 1000;
3210                    match self.calendar_selecting {
3211                        CalendarField::StartDate => {
3212                            self.log_groups_state.start_time = Some(timestamp);
3213                            self.calendar_selecting = CalendarField::EndDate;
3214                        }
3215                        CalendarField::EndDate => {
3216                            self.log_groups_state.end_time = Some(timestamp);
3217                            self.mode = Mode::Normal;
3218                            self.calendar_date = None;
3219                        }
3220                    }
3221                }
3222            }
3223        }
3224    }
3225
3226    pub fn filtered_services(&self) -> Vec<&'static str> {
3227        let mut services = if self.service_picker.filter.is_empty() {
3228            self.service_picker.services.clone()
3229        } else {
3230            self.service_picker
3231                .services
3232                .iter()
3233                .filter(|s| {
3234                    s.to_lowercase()
3235                        .contains(&self.service_picker.filter.to_lowercase())
3236                })
3237                .copied()
3238                .collect()
3239        };
3240        services.sort();
3241        services
3242    }
3243
3244    pub fn breadcrumbs(&self) -> String {
3245        if !self.service_selected {
3246            return String::new();
3247        }
3248
3249        let mut parts = vec![];
3250
3251        match self.current_service {
3252            Service::CloudWatchLogGroups => {
3253                parts.push("CloudWatch".to_string());
3254                parts.push("Log groups".to_string());
3255
3256                if self.view_mode != ViewMode::List {
3257                    if let Some(group) = selected_log_group(self) {
3258                        parts.push(group.name.clone());
3259                    }
3260                }
3261
3262                if self.view_mode == ViewMode::Events {
3263                    if let Some(stream) = self
3264                        .log_groups_state
3265                        .log_streams
3266                        .get(self.log_groups_state.selected_stream)
3267                    {
3268                        parts.push(stream.name.clone());
3269                    }
3270                }
3271            }
3272            Service::CloudWatchInsights => {
3273                parts.push("CloudWatch".to_string());
3274                parts.push("Logs Insights".to_string());
3275            }
3276            Service::CloudWatchAlarms => {
3277                parts.extend(crate::cw::actions::alarms_breadcrumb());
3278            }
3279            Service::CloudTrailEvents => {
3280                parts.extend(crate::cloudtrail::actions::breadcrumb(self));
3281            }
3282            Service::S3Buckets => {
3283                parts.extend(crate::s3::actions::breadcrumb(self));
3284            }
3285            Service::SqsQueues => {
3286                parts.extend(crate::sqs::actions::breadcrumb());
3287            }
3288            Service::EcrRepositories => {
3289                parts.extend(crate::ecr::actions::breadcrumb(self));
3290            }
3291            Service::KmsKeys => {
3292                parts.extend(crate::kms::actions::breadcrumb(self));
3293            }
3294            Service::EfsFileSystems => {
3295                parts.extend(crate::efs::actions::breadcrumb(self));
3296            }
3297            Service::FsxFileSystems => {
3298                parts.extend(crate::fsx::actions::breadcrumb(self));
3299            }
3300            Service::LambdaFunctions => {
3301                parts.extend(crate::lambda::functions::breadcrumb(self));
3302            }
3303            Service::LambdaApplications => {
3304                parts.extend(crate::lambda::applications::breadcrumb());
3305            }
3306            Service::CloudFormationStacks => {
3307                parts.extend(crate::cfn::actions::breadcrumb(self));
3308            }
3309            Service::IamUsers => {
3310                parts.extend(crate::iam::actions::breadcrumb_users());
3311            }
3312            Service::IamRoles => {
3313                parts.extend(crate::iam::actions::breadcrumb_roles(self));
3314            }
3315            Service::IamUserGroups => {
3316                parts.extend(crate::iam::actions::breadcrumb_groups(self));
3317            }
3318            Service::Ec2Instances => {
3319                parts.extend(crate::ec2::actions::breadcrumb(self));
3320            }
3321            Service::ApiGatewayApis => {
3322                parts.extend(crate::apig::actions::breadcrumb(self));
3323            }
3324        }
3325
3326        parts.join(" > ")
3327    }
3328
3329    pub fn update_current_tab_breadcrumb(&mut self) {
3330        if !self.tabs.is_empty() {
3331            self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
3332        }
3333    }
3334
3335    pub fn get_console_url(&self) -> String {
3336        use crate::cw;
3337
3338        match self.current_service {
3339            Service::CloudWatchLogGroups => {
3340                if self.view_mode == ViewMode::Events {
3341                    if let Some(group) = selected_log_group(self) {
3342                        if let Some(stream) = self
3343                            .log_groups_state
3344                            .log_streams
3345                            .get(self.log_groups_state.selected_stream)
3346                        {
3347                            return cw::logs::console_url_stream(
3348                                &self.config.region,
3349                                &group.name,
3350                                &stream.name,
3351                            );
3352                        }
3353                    }
3354                } else if self.view_mode == ViewMode::Detail {
3355                    if let Some(group) = selected_log_group(self) {
3356                        return cw::logs::console_url_detail(&self.config.region, &group.name);
3357                    }
3358                }
3359                cw::logs::console_url_list(&self.config.region)
3360            }
3361            Service::CloudWatchInsights => cw::insights::console_url(
3362                &self.config.region,
3363                &self.config.account_id,
3364                &self.insights_state.insights.query_text,
3365                &self.insights_state.insights.selected_log_groups,
3366            ),
3367            Service::CloudWatchAlarms => {
3368                // If drilled into alarm detail use that name; otherwise use selected row
3369                let alarm_name = if let Some(name) = self.alarms_state.current_alarm.as_deref() {
3370                    Some(name.to_string())
3371                } else {
3372                    // In list view — get the currently selected alarm name
3373                    let page_size = self.alarms_state.table.page_size.value();
3374                    let current_page = self.alarms_state.table.selected / page_size;
3375                    let start = current_page * page_size;
3376                    let filtered: Vec<_> = self.alarms_state.table.items.iter().collect();
3377                    let relative = self.alarms_state.table.selected.saturating_sub(start);
3378                    filtered.get(start + relative).map(|a| a.name.clone())
3379                };
3380                cw::alarms::console_url(&self.config.region, alarm_name.as_deref())
3381            }
3382            Service::CloudTrailEvents => crate::cloudtrail::actions::console_url(self),
3383            Service::S3Buckets => crate::s3::actions::console_url(self),
3384            Service::SqsQueues => crate::sqs::actions::console_url(self),
3385            Service::EcrRepositories => crate::ecr::actions::console_url(self),
3386            Service::KmsKeys => crate::kms::actions::console_url(self),
3387            Service::EfsFileSystems => crate::efs::actions::console_url(self),
3388            Service::FsxFileSystems => crate::fsx::actions::console_url(self),
3389            Service::LambdaFunctions => crate::lambda::functions::console_url(self),
3390            Service::LambdaApplications => crate::lambda::applications::console_url(self),
3391            Service::CloudFormationStacks => crate::cfn::actions::console_url(self),
3392            Service::IamUsers => crate::iam::actions::console_url_users(self),
3393            Service::IamRoles => crate::iam::actions::console_url_roles(self),
3394            Service::IamUserGroups => crate::iam::actions::console_url_groups(self),
3395            Service::Ec2Instances => crate::ec2::actions::console_url(self),
3396            Service::ApiGatewayApis => crate::apig::actions::console_url(self),
3397        }
3398    }
3399
3400    pub fn calculate_total_bucket_rows(&self) -> usize {
3401        calculate_total_bucket_rows(self)
3402    }
3403
3404    pub fn calculate_total_object_rows(&self) -> usize {
3405        calculate_total_object_rows(self)
3406    }
3407
3408    fn get_column_selector_max(&self) -> usize {
3409        if self.current_service == Service::ApiGatewayApis {
3410            crate::apig::actions::column_selector_max(self)
3411        } else if self.current_service == Service::S3Buckets
3412            && self.s3_state.current_bucket.is_none()
3413        {
3414            crate::s3::actions::column_selector_max(self)
3415        } else if self.view_mode == ViewMode::Events {
3416            self.cw_log_event_column_ids.len() - 1
3417        } else if self.view_mode == ViewMode::Detail {
3418            self.cw_log_stream_column_ids.len() + 6
3419        } else if self.current_service == Service::CloudWatchAlarms {
3420            crate::cw::actions::alarms_column_selector_max(self)
3421        } else if self.current_service == Service::CloudTrailEvents {
3422            crate::cloudtrail::actions::column_selector_max(self)
3423        } else if self.current_service == Service::Ec2Instances {
3424            crate::ec2::actions::column_selector_max(self)
3425        } else if self.current_service == Service::EcrRepositories {
3426            crate::ecr::actions::column_selector_max(self)
3427        } else if self.current_service == Service::KmsKeys {
3428            crate::kms::actions::column_selector_max(self)
3429        } else if self.current_service == Service::EfsFileSystems {
3430            crate::efs::actions::column_selector_max(self)
3431        } else if self.current_service == Service::FsxFileSystems {
3432            crate::fsx::actions::column_selector_max(self)
3433        } else if self.current_service == Service::SqsQueues {
3434            crate::sqs::actions::column_selector_max(self)
3435        } else if self.current_service == Service::LambdaFunctions {
3436            crate::lambda::functions::column_selector_max(self)
3437        } else if self.current_service == Service::LambdaApplications {
3438            crate::lambda::applications::column_selector_max(self)
3439        } else if self.current_service == Service::CloudFormationStacks {
3440            crate::cfn::actions::column_selector_max(self)
3441        } else if self.current_service == Service::IamUsers {
3442            crate::iam::actions::column_selector_max_users(self)
3443        } else if self.current_service == Service::IamRoles {
3444            crate::iam::actions::column_selector_max_roles(self)
3445        } else {
3446            self.cw_log_group_column_ids.len() + 6
3447        }
3448    }
3449
3450    fn get_column_count(&self) -> usize {
3451        if self.current_service == Service::ApiGatewayApis {
3452            crate::apig::actions::column_count(self)
3453        } else if self.current_service == Service::S3Buckets
3454            && self.s3_state.current_bucket.is_none()
3455        {
3456            crate::s3::actions::column_count(self)
3457        } else if self.view_mode == ViewMode::Events {
3458            self.cw_log_event_column_ids.len()
3459        } else if self.view_mode == ViewMode::Detail {
3460            self.cw_log_stream_column_ids.len()
3461        } else if self.current_service == Service::CloudWatchAlarms {
3462            crate::cw::actions::alarms_column_count(self)
3463        } else if self.current_service == Service::CloudTrailEvents {
3464            crate::cloudtrail::actions::column_count(self)
3465        } else if self.current_service == Service::Ec2Instances {
3466            crate::ec2::actions::column_count(self)
3467        } else if self.current_service == Service::EcrRepositories {
3468            crate::ecr::actions::column_count(self)
3469        } else if self.current_service == Service::KmsKeys {
3470            crate::kms::actions::column_count(self)
3471        } else if self.current_service == Service::EfsFileSystems {
3472            crate::efs::actions::column_count(self)
3473        } else if self.current_service == Service::FsxFileSystems {
3474            crate::fsx::actions::column_count(self)
3475        } else if self.current_service == Service::SqsQueues {
3476            crate::sqs::actions::column_count(self)
3477        } else if self.current_service == Service::LambdaFunctions {
3478            crate::lambda::functions::column_count(self)
3479        } else if self.current_service == Service::LambdaApplications {
3480            crate::lambda::applications::column_count(self)
3481        } else if self.current_service == Service::CloudFormationStacks {
3482            crate::cfn::actions::column_count(self)
3483        } else if self.current_service == Service::IamUsers {
3484            crate::iam::actions::column_count_users(self)
3485        } else if self.current_service == Service::IamRoles {
3486            crate::iam::actions::column_count_roles(self)
3487        } else {
3488            self.cw_log_group_column_ids.len()
3489        }
3490    }
3491
3492    fn is_blank_row_index(&self, idx: usize) -> bool {
3493        let column_count = self.get_column_count();
3494        // Blank row is at: header(0) + columns(1..=column_count) + blank(column_count+1)
3495        idx == column_count + 1
3496    }
3497
3498    fn next_item(&mut self) {
3499        match self.mode {
3500            Mode::FilterInput => {
3501                if self.current_service == Service::S3Buckets
3502                    && crate::s3::actions::is_pagination_focused(self)
3503                {
3504                    crate::s3::actions::page_down_filter_input(self);
3505                } else if self.current_service == Service::CloudTrailEvents
3506                    && self.cloudtrail_state.input_focus == InputFocus::Pagination
3507                {
3508                    // Navigate to next page
3509                    let page_size = self.cloudtrail_state.table.page_size.value();
3510                    let total_items = self.cloudtrail_state.table.items.len();
3511                    let current_page = self.cloudtrail_state.table.selected / page_size;
3512                    let total_pages = total_items.div_ceil(page_size);
3513                    if current_page + 1 < total_pages {
3514                        self.cloudtrail_state.table.selected = (current_page + 1) * page_size;
3515                    }
3516                } else if self.current_service == Service::CloudFormationStacks {
3517                    crate::cfn::actions::next_item_filter_input(self);
3518                } else if self.current_service == Service::IamUsers
3519                    && self.iam_state.current_user.is_some()
3520                {
3521                    crate::iam::actions::next_item_filter_input_users(self);
3522                } else if self.current_service == Service::IamRoles
3523                    && self.iam_state.current_role.is_some()
3524                    && self.iam_state.role_tab == RoleTab::Permissions
3525                {
3526                    crate::iam::actions::next_item_filter_input_roles(self);
3527                } else if self.current_service == Service::Ec2Instances {
3528                    crate::ec2::actions::toggle_state_filter_next(self);
3529                } else if self.current_service == Service::SqsQueues {
3530                    crate::sqs::actions::next_item_filter_input(self);
3531                }
3532            }
3533            Mode::RegionPicker => {
3534                let filtered = self.get_filtered_regions();
3535                if !filtered.is_empty() {
3536                    self.region_picker_selected =
3537                        (self.region_picker_selected + 1).min(filtered.len() - 1);
3538                }
3539            }
3540            Mode::ProfilePicker => {
3541                let filtered = self.get_filtered_profiles();
3542                if !filtered.is_empty() {
3543                    self.profile_picker_selected =
3544                        (self.profile_picker_selected + 1).min(filtered.len() - 1);
3545                }
3546            }
3547            Mode::SessionPicker => {
3548                let filtered = self.get_filtered_sessions();
3549                if !filtered.is_empty() {
3550                    self.session_picker_selected =
3551                        (self.session_picker_selected + 1).min(filtered.len() - 1);
3552                }
3553            }
3554            Mode::InsightsInput => {
3555                crate::cw::actions::insights_next_item_dropdown(self);
3556            }
3557            Mode::ColumnSelector => {
3558                let max = self.get_column_selector_max();
3559                let mut next_idx = (self.column_selector_index + 1).min(max);
3560                // Skip blank row
3561                if self.is_blank_row_index(next_idx) {
3562                    next_idx = (next_idx + 1).min(max);
3563                }
3564                self.column_selector_index = next_idx;
3565            }
3566            Mode::ServicePicker => {
3567                let filtered = self.filtered_services();
3568                if !filtered.is_empty() {
3569                    self.service_picker.selected =
3570                        (self.service_picker.selected + 1).min(filtered.len() - 1);
3571                }
3572            }
3573            Mode::TabPicker => {
3574                let filtered = self.get_filtered_tabs();
3575                if !filtered.is_empty() {
3576                    self.tab_picker_selected =
3577                        (self.tab_picker_selected + 1).min(filtered.len() - 1);
3578                }
3579            }
3580            Mode::Normal => {
3581                if !self.service_selected {
3582                    let filtered = self.filtered_services();
3583                    if !filtered.is_empty() {
3584                        self.service_picker.selected =
3585                            (self.service_picker.selected + 1).min(filtered.len() - 1);
3586                    }
3587                } else if self.current_service == Service::S3Buckets {
3588                    crate::s3::actions::next_item(self);
3589                } else if self.view_mode == ViewMode::InsightsResults {
3590                    crate::cw::actions::insights_next_item_results(self);
3591                } else if self.current_service == Service::CloudWatchInsights
3592                    && self.insights_state.insights.show_dropdown
3593                {
3594                    // Navigate dropdown in Normal mode
3595                    crate::cw::actions::insights_next_item_dropdown(self);
3596                } else if self.current_service == Service::ApiGatewayApis {
3597                    crate::apig::actions::next_item(self);
3598                } else if self.current_service == Service::SqsQueues
3599                    && self.sqs_state.current_queue.is_some()
3600                    && (self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
3601                        || (self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3602                            && !self.sqs_state.is_metrics_loading()))
3603                {
3604                    crate::sqs::actions::scroll_down_detail(self);
3605                } else if self.view_mode == ViewMode::Events {
3606                    let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3607                    if self.log_groups_state.event_scroll_offset < max_scroll {
3608                        self.log_groups_state.event_scroll_offset =
3609                            (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3610                    }
3611                } else if self.current_service == Service::CloudWatchLogGroups {
3612                    crate::cw::actions::logs_next_item(self);
3613                } else if self.current_service == Service::CloudWatchAlarms {
3614                    crate::cw::actions::alarms_next_item(self);
3615                } else if self.current_service == Service::CloudTrailEvents {
3616                    crate::cloudtrail::actions::next_item(self);
3617                } else if self.current_service == Service::Ec2Instances {
3618                    crate::ec2::actions::next_item(self);
3619                } else if self.current_service == Service::EcrRepositories {
3620                    crate::ecr::actions::next_item(self);
3621                } else if self.current_service == Service::KmsKeys {
3622                    crate::kms::actions::next_item(self);
3623                } else if self.current_service == Service::EfsFileSystems {
3624                    crate::efs::actions::next_item(self);
3625                } else if self.current_service == Service::FsxFileSystems {
3626                    crate::fsx::actions::next_item(self);
3627                } else if self.current_service == Service::SqsQueues {
3628                    crate::sqs::actions::next_item(self);
3629                } else if self.current_service == Service::LambdaFunctions {
3630                    crate::lambda::functions::next_item(self);
3631                } else if self.current_service == Service::LambdaApplications {
3632                    crate::lambda::applications::next_item(self);
3633                } else if self.current_service == Service::CloudFormationStacks {
3634                    crate::cfn::actions::next_item(self);
3635                } else if self.current_service == Service::IamUsers {
3636                    crate::iam::actions::next_item_users(self);
3637                } else if self.current_service == Service::IamRoles {
3638                    crate::iam::actions::next_item_roles(self);
3639                } else if self.current_service == Service::IamUserGroups {
3640                    crate::iam::actions::next_item_groups(self);
3641                }
3642            }
3643            _ => {}
3644        }
3645    }
3646
3647    fn prev_item(&mut self) {
3648        match self.mode {
3649            Mode::FilterInput => {
3650                if self.current_service == Service::S3Buckets
3651                    && crate::s3::actions::is_pagination_focused(self)
3652                {
3653                    crate::s3::actions::page_up_filter_input(self);
3654                } else if self.current_service == Service::CloudTrailEvents
3655                    && self.cloudtrail_state.input_focus == InputFocus::Pagination
3656                {
3657                    // Navigate to previous page
3658                    let page_size = self.cloudtrail_state.table.page_size.value();
3659                    let current_page = self.cloudtrail_state.table.selected / page_size;
3660                    if current_page > 0 {
3661                        self.cloudtrail_state.table.selected = (current_page - 1) * page_size;
3662                    }
3663                } else if self.current_service == Service::CloudFormationStacks {
3664                    crate::cfn::actions::prev_item_filter_input(self);
3665                } else if self.current_service == Service::IamUsers
3666                    && self.iam_state.current_user.is_some()
3667                {
3668                    crate::iam::actions::prev_item_filter_input_users(self);
3669                } else if self.current_service == Service::IamRoles
3670                    && self.iam_state.current_role.is_some()
3671                    && self.iam_state.role_tab == RoleTab::Permissions
3672                {
3673                    crate::iam::actions::prev_item_filter_input_roles(self);
3674                } else if self.current_service == Service::Ec2Instances {
3675                    crate::ec2::actions::toggle_state_filter_prev(self);
3676                } else if self.current_service == Service::SqsQueues {
3677                    use crate::ui::sqs::SUBSCRIPTION_REGION;
3678                    if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
3679                        self.sqs_state.subscription_region_selected = self
3680                            .sqs_state
3681                            .subscription_region_selected
3682                            .saturating_sub(1);
3683                        self.sqs_state.subscriptions.reset();
3684                    }
3685                }
3686            }
3687            Mode::RegionPicker => {
3688                self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
3689            }
3690            Mode::ProfilePicker => {
3691                self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
3692            }
3693            Mode::SessionPicker => {
3694                self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
3695            }
3696            Mode::InsightsInput => {
3697                crate::cw::actions::insights_prev_item_dropdown(self);
3698            }
3699            Mode::ColumnSelector => {
3700                let mut prev_idx = self.column_selector_index.saturating_sub(1);
3701                // Skip blank row
3702                if self.is_blank_row_index(prev_idx) {
3703                    prev_idx = prev_idx.saturating_sub(1);
3704                }
3705                self.column_selector_index = prev_idx;
3706            }
3707            Mode::ServicePicker => {
3708                self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3709            }
3710            Mode::TabPicker => {
3711                self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
3712            }
3713            Mode::Normal => {
3714                if !self.service_selected {
3715                    self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3716                } else if self.current_service == Service::S3Buckets {
3717                    crate::s3::actions::prev_item(self);
3718                } else if self.current_service == Service::ApiGatewayApis {
3719                    crate::apig::actions::prev_item(self);
3720                } else if self.view_mode == ViewMode::InsightsResults {
3721                    if self.insights_state.insights.results_selected > 0 {
3722                        self.insights_state.insights.results_selected -= 1;
3723                    }
3724                } else if self.current_service == Service::CloudWatchInsights
3725                    && self.insights_state.insights.show_dropdown
3726                {
3727                    crate::cw::actions::insights_prev_item_dropdown(self);
3728                } else if self.view_mode == ViewMode::PolicyView {
3729                    crate::iam::actions::scroll_up_policy_view_one(self);
3730                } else if self.current_service == Service::SqsQueues
3731                    && self.sqs_state.current_queue.is_some()
3732                    && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
3733                {
3734                } else if self.current_service == Service::LambdaFunctions
3735                    && self.lambda_state.current_function.is_some()
3736                    && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3737                    && !self.lambda_state.is_metrics_loading()
3738                {
3739                    crate::lambda::functions::scroll_up(self);
3740                } else if self.current_service == Service::SqsQueues
3741                    && self.sqs_state.current_queue.is_some()
3742                    && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3743                    && !self.sqs_state.is_metrics_loading()
3744                {
3745                    self.sqs_state.set_monitoring_scroll(
3746                        self.sqs_state.monitoring_scroll().saturating_sub(1),
3747                    );
3748                } else if self.view_mode == ViewMode::Events {
3749                    if self.log_groups_state.event_scroll_offset == 0 {
3750                        if self.log_groups_state.has_older_events {
3751                            self.log_groups_state.loading = true;
3752                        }
3753                        // Don't move if at position 0
3754                    } else {
3755                        self.log_groups_state.event_scroll_offset =
3756                            self.log_groups_state.event_scroll_offset.saturating_sub(1);
3757                    }
3758                } else if self.current_service == Service::CloudWatchLogGroups {
3759                    crate::cw::actions::logs_prev_item(self);
3760                } else if self.current_service == Service::CloudWatchAlarms {
3761                    crate::cw::actions::alarms_prev_item(self);
3762                } else if self.current_service == Service::CloudTrailEvents {
3763                    crate::cloudtrail::actions::prev_item(self);
3764                } else if self.current_service == Service::Ec2Instances {
3765                    if self.ec2_state.current_instance.is_some()
3766                        && self.ec2_state.detail_tab == Ec2DetailTab::Tags
3767                    {
3768                        self.ec2_state.tags.prev_item();
3769                    } else {
3770                        self.ec2_state.table.prev_item();
3771                    }
3772                } else if self.current_service == Service::EcrRepositories {
3773                    crate::ecr::actions::prev_item(self);
3774                } else if self.current_service == Service::KmsKeys {
3775                    crate::kms::actions::prev_item(self);
3776                } else if self.current_service == Service::EfsFileSystems {
3777                    crate::efs::actions::prev_item(self);
3778                } else if self.current_service == Service::FsxFileSystems {
3779                    crate::fsx::actions::prev_item(self);
3780                } else if self.current_service == Service::SqsQueues {
3781                    crate::sqs::actions::prev_item(self);
3782                } else if self.current_service == Service::LambdaFunctions {
3783                    crate::lambda::functions::prev_item(self);
3784                } else if self.current_service == Service::LambdaApplications {
3785                    crate::lambda::applications::prev_item(self);
3786                } else if self.current_service == Service::CloudFormationStacks {
3787                    crate::cfn::actions::prev_item(self);
3788                } else if self.current_service == Service::IamUsers {
3789                    crate::iam::actions::prev_item_users(self);
3790                } else if self.current_service == Service::IamRoles {
3791                    crate::iam::actions::prev_item_roles(self);
3792                } else if self.current_service == Service::IamUserGroups {
3793                    crate::iam::actions::prev_item_groups(self);
3794                }
3795            }
3796            _ => {}
3797        }
3798    }
3799
3800    fn page_down(&mut self) {
3801        if self.current_service == Service::CloudTrailEvents
3802            && self.cloudtrail_state.current_event.is_some()
3803        {
3804            crate::cloudtrail::actions::page_down_fast(self);
3805        } else if self.mode == Mode::ColumnSelector {
3806            let max = self.get_column_selector_max();
3807            let mut next_idx = (self.column_selector_index + 10).min(max);
3808            // Skip blank row if we land on it
3809            if self.is_blank_row_index(next_idx) {
3810                next_idx = (next_idx + 1).min(max);
3811            }
3812            self.column_selector_index = next_idx;
3813        } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
3814            crate::s3::actions::page_down_filter_input(self);
3815        } else if self.mode == Mode::FilterInput
3816            && self.current_service == Service::CloudFormationStacks
3817        {
3818            crate::cfn::actions::page_down_filter_input(self);
3819        } else if self.mode == Mode::FilterInput
3820            && self.current_service == Service::IamRoles
3821            && self.iam_state.current_role.is_none()
3822        {
3823            crate::iam::actions::page_down_filter_input_roles(self);
3824        } else if self.mode == Mode::FilterInput
3825            && self.current_service == Service::CloudWatchAlarms
3826        {
3827            crate::cw::actions::alarms_page_down_filter_input(self);
3828        } else if self.mode == Mode::FilterInput
3829            && self.current_service == Service::CloudTrailEvents
3830        {
3831            crate::cloudtrail::actions::page_down_filter_input(self);
3832        } else if self.mode == Mode::FilterInput
3833            && self.current_service == Service::CloudWatchLogGroups
3834        {
3835            crate::cw::actions::logs_page_down_filter_input(self);
3836        } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
3837        {
3838            crate::lambda::functions::page_down_filter_input(self);
3839        } else if self.mode == Mode::FilterInput
3840            && self.current_service == Service::LambdaApplications
3841        {
3842            crate::lambda::applications::page_down_filter_input(self);
3843        } else if self.mode == Mode::FilterInput
3844            && self.current_service == Service::EcrRepositories
3845            && self.ecr_state.current_repository.is_none()
3846        {
3847            crate::ecr::actions::page_down_filter_input(self);
3848        } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys {
3849            crate::kms::actions::page_down_filter_input(self);
3850        } else if self.mode == Mode::FilterInput && self.current_service == Service::EfsFileSystems
3851        {
3852            crate::efs::actions::page_down_filter_input(self);
3853        } else if self.mode == Mode::FilterInput && self.current_service == Service::FsxFileSystems
3854        {
3855            crate::fsx::actions::page_down_filter_input(self);
3856        } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
3857            crate::iam::actions::page_down_filter_input_policy_view(self);
3858        } else if self.view_mode == ViewMode::PolicyView {
3859            crate::iam::actions::scroll_down_policy_view(self);
3860        } else if self.current_service == Service::CloudFormationStacks
3861            && self.cfn_state.current_stack.is_some()
3862            && self.cfn_state.detail_tab == CfnDetailTab::Template
3863        {
3864            crate::cfn::actions::scroll_down_template_fast(self);
3865        } else if self.current_service == Service::LambdaFunctions
3866            && self.lambda_state.current_function.is_some()
3867            && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3868            && !self.lambda_state.is_metrics_loading()
3869        {
3870            crate::lambda::functions::scroll_down(self);
3871        } else if self.current_service == Service::Ec2Instances {
3872            crate::ec2::actions::scroll_down(self);
3873        } else if self.current_service == Service::SqsQueues
3874            && self.sqs_state.current_queue.is_some()
3875        {
3876            crate::sqs::actions::scroll_down_fast(self);
3877        } else if self.current_service == Service::EfsFileSystems
3878            && self.efs_state.current_file_system.is_some()
3879            && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
3880                || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
3881            && !self.efs_state.is_metrics_loading()
3882        {
3883            crate::efs::actions::scroll_down_detail(self);
3884        } else if self.current_service == Service::IamRoles
3885            && self.iam_state.current_role.is_some()
3886            && self.iam_state.role_tab == RoleTab::TrustRelationships
3887        {
3888            crate::iam::actions::scroll_down_trust_policy(self);
3889        } else if self.current_service == Service::IamRoles
3890            && self.iam_state.current_role.is_some()
3891            && self.iam_state.role_tab == RoleTab::RevokeSessions
3892        {
3893            crate::iam::actions::scroll_down_revoke_sessions(self);
3894        } else if self.mode == Mode::Normal {
3895            if self.current_service == Service::S3Buckets {
3896                crate::s3::actions::page_down_normal(self);
3897            } else if self.current_service == Service::CloudWatchLogGroups
3898                && self.view_mode == ViewMode::List
3899            {
3900                crate::cw::actions::logs_page_down_normal_list(self);
3901            } else if self.current_service == Service::CloudWatchLogGroups
3902                && self.view_mode == ViewMode::Detail
3903            {
3904                crate::cw::actions::logs_page_down_normal_detail(self);
3905            } else if self.view_mode == ViewMode::Events {
3906                crate::cw::actions::logs_page_down_events(self);
3907            } else if self.view_mode == ViewMode::InsightsResults {
3908                crate::cw::actions::insights_page_down(self);
3909            } else if self.current_service == Service::CloudWatchAlarms {
3910                crate::cw::actions::alarms_page_down_normal(self);
3911            } else if self.current_service == Service::CloudTrailEvents {
3912                crate::cloudtrail::actions::page_down_normal(self);
3913            } else if self.current_service == Service::Ec2Instances {
3914                crate::ec2::actions::page_down_normal(self);
3915            } else if self.current_service == Service::EcrRepositories {
3916                crate::ecr::actions::page_down_normal(self);
3917            } else if self.current_service == Service::KmsKeys {
3918                crate::kms::actions::page_down_normal(self);
3919            } else if self.current_service == Service::EfsFileSystems {
3920                crate::efs::actions::page_down_normal(self);
3921            } else if self.current_service == Service::FsxFileSystems {
3922                crate::fsx::actions::page_down_normal(self);
3923            } else if self.current_service == Service::SqsQueues {
3924                crate::sqs::actions::page_down_normal(self);
3925            } else if self.current_service == Service::LambdaFunctions {
3926                crate::lambda::functions::page_down_normal(self);
3927            } else if self.current_service == Service::LambdaApplications {
3928                crate::lambda::applications::page_down_normal(self);
3929            } else if self.current_service == Service::CloudFormationStacks {
3930                crate::cfn::actions::page_down_normal(self);
3931            } else if self.current_service == Service::IamUsers {
3932                crate::iam::actions::page_down_normal_users(self);
3933            } else if self.current_service == Service::IamRoles {
3934                crate::iam::actions::page_down_normal_roles(self);
3935            } else if self.current_service == Service::IamUserGroups {
3936                crate::iam::actions::page_down_normal_groups(self);
3937            }
3938        }
3939    }
3940
3941    fn page_up(&mut self) {
3942        if self.current_service == Service::CloudTrailEvents
3943            && self.cloudtrail_state.current_event.is_some()
3944        {
3945            crate::cloudtrail::actions::page_up_fast(self);
3946        } else if self.mode == Mode::ColumnSelector {
3947            let mut prev_idx = self.column_selector_index.saturating_sub(10);
3948            // Skip blank row if we land on it
3949            if self.is_blank_row_index(prev_idx) {
3950                prev_idx = prev_idx.saturating_sub(1);
3951            }
3952            self.column_selector_index = prev_idx;
3953        } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
3954            crate::s3::actions::page_up_filter_input(self);
3955        } else if self.mode == Mode::FilterInput
3956            && self.current_service == Service::CloudFormationStacks
3957        {
3958            crate::cfn::actions::page_up_filter_input(self);
3959        } else if self.mode == Mode::FilterInput
3960            && self.current_service == Service::IamRoles
3961            && self.iam_state.current_role.is_none()
3962        {
3963            crate::iam::actions::page_up_filter_input_roles(self);
3964        } else if self.mode == Mode::FilterInput
3965            && self.current_service == Service::CloudWatchAlarms
3966        {
3967            crate::cw::actions::alarms_page_up_filter_input(self);
3968        } else if self.mode == Mode::FilterInput
3969            && self.current_service == Service::CloudTrailEvents
3970        {
3971            crate::cloudtrail::actions::page_up_filter_input(self);
3972        } else if self.mode == Mode::FilterInput
3973            && self.current_service == Service::CloudWatchLogGroups
3974        {
3975            crate::cw::actions::logs_page_up_filter_input(self);
3976        } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
3977        {
3978            crate::lambda::functions::page_up_filter_input(self);
3979        } else if self.mode == Mode::FilterInput
3980            && self.current_service == Service::LambdaApplications
3981        {
3982            crate::lambda::applications::page_up_filter_input(self);
3983        } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys {
3984            crate::kms::actions::page_up_filter_input(self);
3985        } else if self.mode == Mode::FilterInput && self.current_service == Service::EfsFileSystems
3986        {
3987            crate::efs::actions::page_up_filter_input(self);
3988        } else if self.mode == Mode::FilterInput && self.current_service == Service::FsxFileSystems
3989        {
3990            crate::fsx::actions::page_up_filter_input(self);
3991        } else if self.mode == Mode::FilterInput
3992            && self.current_service == Service::EcrRepositories
3993            && self.ecr_state.current_repository.is_none()
3994        {
3995            crate::ecr::actions::page_up_filter_input(self);
3996        } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
3997            crate::iam::actions::page_up_filter_input_policy_view(self);
3998        } else if self.view_mode == ViewMode::PolicyView {
3999            crate::iam::actions::scroll_up_policy_view(self);
4000        } else if self.current_service == Service::CloudFormationStacks
4001            && self.cfn_state.current_stack.is_some()
4002            && self.cfn_state.detail_tab == CfnDetailTab::Template
4003        {
4004            crate::cfn::actions::scroll_up_template_fast(self);
4005        } else if self.current_service == Service::SqsQueues
4006            && self.sqs_state.current_queue.is_some()
4007        {
4008            crate::sqs::actions::scroll_up_fast(self);
4009        } else if self.current_service == Service::EfsFileSystems
4010            && self.efs_state.current_file_system.is_some()
4011            && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
4012                || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
4013            && !self.efs_state.is_metrics_loading()
4014        {
4015            crate::efs::actions::scroll_up_detail(self);
4016        } else if self.current_service == Service::IamRoles
4017            && self.iam_state.current_role.is_some()
4018            && self.iam_state.role_tab == RoleTab::TrustRelationships
4019        {
4020            crate::iam::actions::scroll_up_trust_policy(self);
4021        } else if self.current_service == Service::IamRoles
4022            && self.iam_state.current_role.is_some()
4023            && self.iam_state.role_tab == RoleTab::RevokeSessions
4024        {
4025            crate::iam::actions::scroll_up_revoke_sessions(self);
4026        } else if self.mode == Mode::Normal {
4027            if self.current_service == Service::S3Buckets {
4028                crate::s3::actions::page_up_normal(self);
4029            } else if self.current_service == Service::CloudWatchLogGroups
4030                && self.view_mode == ViewMode::List
4031            {
4032                crate::cw::actions::logs_page_up_normal_list(self);
4033            } else if self.current_service == Service::CloudWatchLogGroups
4034                && self.view_mode == ViewMode::Detail
4035            {
4036                crate::cw::actions::logs_page_up_normal_detail(self);
4037            } else if self.view_mode == ViewMode::Events {
4038                crate::cw::actions::logs_page_up_events(self);
4039            } else if self.view_mode == ViewMode::InsightsResults {
4040                crate::cw::actions::insights_page_up(self);
4041            } else if self.current_service == Service::CloudWatchAlarms {
4042                crate::cw::actions::alarms_page_up_normal(self);
4043            } else if self.current_service == Service::CloudTrailEvents {
4044                crate::cloudtrail::actions::page_up_normal(self);
4045            } else if self.current_service == Service::KmsKeys {
4046                crate::kms::actions::page_up_normal(self);
4047            } else if self.current_service == Service::EfsFileSystems {
4048                crate::efs::actions::page_up_normal(self);
4049            } else if self.current_service == Service::FsxFileSystems {
4050                crate::fsx::actions::page_up_normal(self);
4051            } else if self.current_service == Service::Ec2Instances {
4052                crate::ec2::actions::page_up_normal(self);
4053            } else if self.current_service == Service::EcrRepositories {
4054                crate::ecr::actions::page_up_normal(self);
4055            } else if self.current_service == Service::SqsQueues {
4056                self.sqs_state.queues.page_up();
4057            } else if self.current_service == Service::LambdaFunctions {
4058                crate::lambda::functions::page_up_normal(self);
4059            } else if self.current_service == Service::LambdaApplications {
4060                crate::lambda::applications::page_up_normal(self);
4061            } else if self.current_service == Service::CloudFormationStacks {
4062                crate::cfn::actions::page_up_normal(self);
4063            } else if self.current_service == Service::IamUsers {
4064                crate::iam::actions::page_up_normal_users(self);
4065            } else if self.current_service == Service::IamRoles {
4066                crate::iam::actions::page_up_normal_roles(self);
4067            }
4068        }
4069    }
4070
4071    fn next_pane(&mut self) {
4072        if self.current_service == Service::S3Buckets {
4073            crate::s3::actions::expand_row(self);
4074        } else if self.view_mode == ViewMode::InsightsResults {
4075            // Right arrow scrolls horizontally by 1 column
4076            let max_cols = self
4077                .insights_state
4078                .insights
4079                .query_results
4080                .first()
4081                .map(|r| r.len())
4082                .unwrap_or(0);
4083            if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
4084                self.insights_state.insights.results_horizontal_scroll += 1;
4085            }
4086        } else if self.current_service == Service::CloudWatchLogGroups
4087            || self.view_mode == ViewMode::Events
4088        {
4089            crate::cw::actions::logs_expand_row(self);
4090        } else if self.current_service == Service::CloudWatchAlarms {
4091            crate::cw::actions::alarms_expand_row(self);
4092        } else if self.current_service == Service::Ec2Instances {
4093            crate::ec2::actions::expand_row(self);
4094        } else if self.current_service == Service::EcrRepositories {
4095            crate::ecr::actions::next_pane(self);
4096        } else if self.current_service == Service::KmsKeys {
4097            crate::kms::actions::expand_row(self);
4098        } else if self.current_service == Service::EfsFileSystems {
4099            crate::efs::actions::expand_row(self);
4100        } else if self.current_service == Service::FsxFileSystems {
4101            crate::fsx::actions::expand_row(self);
4102        } else if self.current_service == Service::SqsQueues {
4103            crate::sqs::actions::expand_row(self);
4104        } else if self.current_service == Service::LambdaFunctions {
4105            crate::lambda::functions::expand_row(self);
4106        } else if self.current_service == Service::LambdaApplications {
4107            crate::lambda::applications::expand_row(self);
4108        } else if self.current_service == Service::CloudFormationStacks
4109            && self.cfn_state.current_stack.is_none()
4110        {
4111            self.cfn_state.table.toggle_expand();
4112        } else if self.current_service == Service::CloudFormationStacks
4113            && self.cfn_state.current_stack.is_some()
4114            && self.cfn_state.detail_tab == CfnDetailTab::Parameters
4115        {
4116            self.cfn_state.parameters.toggle_expand();
4117        } else if self.current_service == Service::CloudFormationStacks
4118            && self.cfn_state.current_stack.is_some()
4119            && self.cfn_state.detail_tab == CfnDetailTab::Outputs
4120        {
4121            self.cfn_state.outputs.toggle_expand();
4122        } else if self.current_service == Service::CloudFormationStacks
4123            && self.cfn_state.current_stack.is_some()
4124            && self.cfn_state.detail_tab == CfnDetailTab::Resources
4125        {
4126            self.cfn_state.resources.toggle_expand();
4127        } else if self.current_service == Service::IamUsers {
4128            crate::iam::actions::expand_row_users(self);
4129        } else if self.current_service == Service::IamRoles {
4130            crate::iam::actions::expand_row_roles(self);
4131        } else if self.current_service == Service::IamUserGroups {
4132            crate::iam::actions::expand_row_groups(self);
4133        }
4134    }
4135
4136    fn go_to_page(&mut self, page: usize) {
4137        if page == 0 {
4138            return;
4139        }
4140
4141        match self.current_service {
4142            Service::CloudWatchAlarms => {
4143                crate::cw::actions::alarms_go_to_page(self, page);
4144            }
4145            Service::CloudTrailEvents => {
4146                let page_size = self.cloudtrail_state.table.page_size.value();
4147                let filtered_count = self.cloudtrail_state.table.items.len();
4148                let max_page = (filtered_count / page_size) + 1; // Allow one page beyond loaded
4149
4150                // Only navigate if page is within valid range
4151                if page <= max_page {
4152                    let target = (page - 1) * page_size;
4153                    self.cloudtrail_state.table.scroll_offset = target;
4154                    self.cloudtrail_state.table.selected = target;
4155                    self.cloudtrail_state.table.expanded_item = None; // Reset expansion
4156                }
4157                // Otherwise do nothing (ignore invalid page numbers)
4158            }
4159            Service::CloudWatchLogGroups => {
4160                crate::cw::actions::logs_go_to_page(self, page);
4161            }
4162            Service::EcrRepositories => {
4163                crate::ecr::actions::go_to_page(self, page);
4164            }
4165            Service::KmsKeys => {
4166                crate::kms::actions::go_to_page(self, page);
4167            }
4168            Service::EfsFileSystems => {
4169                crate::efs::actions::go_to_page(self, page);
4170            }
4171            Service::FsxFileSystems => {
4172                crate::fsx::actions::go_to_page(self, page);
4173            }
4174            Service::SqsQueues => {
4175                crate::sqs::actions::go_to_page(self, page);
4176            }
4177            Service::S3Buckets => {
4178                crate::s3::actions::go_to_page(self, page);
4179            }
4180            Service::LambdaFunctions => {
4181                crate::lambda::functions::go_to_page(self, page);
4182            }
4183            Service::LambdaApplications => {
4184                crate::lambda::applications::go_to_page(self, page);
4185            }
4186            Service::CloudFormationStacks => {
4187                crate::cfn::actions::go_to_page(self, page);
4188            }
4189            Service::IamUsers => {
4190                crate::iam::actions::go_to_page_users(self, page);
4191            }
4192            Service::IamRoles => {
4193                crate::iam::actions::go_to_page_roles(self, page);
4194            }
4195            _ => {}
4196        }
4197    }
4198
4199    fn prev_pane(&mut self) {
4200        if self.current_service == Service::S3Buckets {
4201            crate::s3::actions::prev_pane(self);
4202        } else if self.view_mode == ViewMode::InsightsResults {
4203            // Left arrow scrolls horizontally by 1 column
4204            self.insights_state.insights.results_horizontal_scroll = self
4205                .insights_state
4206                .insights
4207                .results_horizontal_scroll
4208                .saturating_sub(1);
4209        } else if self.current_service == Service::CloudWatchLogGroups
4210            || self.view_mode == ViewMode::Events
4211        {
4212            crate::cw::actions::logs_prev_pane(self);
4213        } else if self.current_service == Service::CloudWatchAlarms {
4214            crate::cw::actions::alarms_prev_pane(self);
4215        } else if self.current_service == Service::Ec2Instances {
4216            crate::ec2::actions::prev_pane(self);
4217        } else if self.current_service == Service::ApiGatewayApis {
4218            crate::apig::actions::prev_pane(self);
4219        } else if self.current_service == Service::EcrRepositories {
4220            crate::ecr::actions::prev_pane(self);
4221        } else if self.current_service == Service::KmsKeys {
4222            crate::kms::actions::prev_pane(self);
4223        } else if self.current_service == Service::EfsFileSystems {
4224            crate::efs::actions::prev_pane(self);
4225        } else if self.current_service == Service::FsxFileSystems {
4226            crate::fsx::actions::prev_pane(self);
4227        } else if self.current_service == Service::SqsQueues {
4228            crate::sqs::actions::prev_pane(self);
4229        } else if self.current_service == Service::LambdaFunctions {
4230            crate::lambda::functions::prev_pane(self);
4231        } else if self.current_service == Service::LambdaApplications {
4232            crate::lambda::applications::prev_pane(self);
4233        } else if self.current_service == Service::CloudFormationStacks {
4234            crate::cfn::actions::prev_pane(self);
4235        } else if self.current_service == Service::IamUsers {
4236            crate::iam::actions::prev_pane_users(self);
4237        } else if self.current_service == Service::IamRoles {
4238            crate::iam::actions::prev_pane_roles(self);
4239        } else if self.current_service == Service::IamUserGroups {
4240            crate::iam::actions::prev_pane_groups(self);
4241        }
4242    }
4243
4244    fn collapse_row(&mut self) {
4245        match self.current_service {
4246            Service::S3Buckets => {
4247                crate::s3::actions::collapse_row(self);
4248            }
4249            Service::CloudWatchLogGroups => {
4250                crate::cw::actions::logs_collapse_row(self);
4251            }
4252            Service::CloudWatchAlarms => crate::cw::actions::alarms_collapse_row(self),
4253            Service::Ec2Instances => {
4254                crate::ec2::actions::collapse_row(self);
4255            }
4256            Service::EcrRepositories => {
4257                crate::ecr::actions::collapse_row(self);
4258            }
4259            Service::LambdaFunctions => crate::lambda::functions::collapse_row(self),
4260            Service::LambdaApplications => crate::lambda::applications::prev_pane(self),
4261            Service::SqsQueues => crate::sqs::actions::collapse_row(self),
4262            Service::CloudFormationStacks => {
4263                crate::cfn::actions::collapse_row(self);
4264            }
4265            Service::IamUsers => {
4266                crate::iam::actions::collapse_row_users(self);
4267            }
4268            Service::IamRoles => {
4269                crate::iam::actions::collapse_row_roles(self);
4270            }
4271            Service::IamUserGroups => {
4272                crate::iam::actions::collapse_row_groups(self);
4273            }
4274            Service::ApiGatewayApis => {
4275                crate::apig::actions::collapse_row(self);
4276            }
4277            Service::KmsKeys => {
4278                crate::kms::actions::collapse_row(self);
4279            }
4280            Service::EfsFileSystems => {
4281                crate::efs::actions::collapse_row(self);
4282            }
4283            Service::FsxFileSystems => {
4284                crate::fsx::actions::collapse_row(self);
4285            }
4286            Service::CloudTrailEvents => {
4287                if self.cloudtrail_state.current_event.is_some()
4288                    && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
4289                {
4290                    // Collapse resources table column
4291                    self.cloudtrail_state.resources_expanded_index = None;
4292                } else {
4293                    self.cloudtrail_state.table.collapse();
4294                }
4295            }
4296            _ => {}
4297        }
4298    }
4299
4300    fn expand_row(&mut self) {
4301        match self.current_service {
4302            Service::S3Buckets => {
4303                crate::s3::actions::expand_row_left(self);
4304            }
4305            Service::ApiGatewayApis => {
4306                crate::apig::actions::expand_row(self);
4307            }
4308            Service::CloudTrailEvents => {
4309                if self.cloudtrail_state.current_event.is_some()
4310                    && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
4311                {
4312                    // Expand resources table column
4313                    self.cloudtrail_state.resources_expanded_index = Some(0);
4314                } else {
4315                    self.cloudtrail_state.table.expand();
4316                }
4317            }
4318            Service::CloudFormationStacks => {
4319                crate::cfn::actions::expand_row(self);
4320            }
4321            _ => {
4322                // For other services, Right arrow switches panes
4323                self.next_pane();
4324            }
4325        }
4326    }
4327
4328    fn select_item(&mut self) {
4329        if self.mode == Mode::RegionPicker {
4330            // If filter is active (INSERT mode), exit filter mode instead of selecting
4331            if self.region_filter_active {
4332                self.region_filter_active = false;
4333                return;
4334            }
4335            let filtered = self.get_filtered_regions();
4336            if let Some(region) = filtered.get(self.region_picker_selected) {
4337                // Save current session before changing region
4338                if !self.tabs.is_empty() {
4339                    let mut session = Session::new(
4340                        self.profile.clone(),
4341                        self.region.clone(),
4342                        self.config.account_id.clone(),
4343                        self.config.role_arn.clone(),
4344                    );
4345
4346                    for tab in &self.tabs {
4347                        session.tabs.push(SessionTab {
4348                            service: format!("{:?}", tab.service),
4349                            title: tab.title.clone(),
4350                            breadcrumb: tab.breadcrumb.clone(),
4351                            filter: None,
4352                            selected_item: None,
4353                        });
4354                    }
4355
4356                    let _ = session.save();
4357                }
4358
4359                self.region = region.code.to_string();
4360                self.config.region = region.code.to_string();
4361
4362                // Close all tabs - region change invalidates all data
4363                self.tabs.clear();
4364                self.current_tab = 0;
4365                self.service_selected = false;
4366
4367                self.mode = Mode::Normal;
4368            }
4369        } else if self.mode == Mode::ProfilePicker {
4370            let filtered = self.get_filtered_profiles();
4371            if let Some(profile) = filtered.get(self.profile_picker_selected) {
4372                let profile_name = profile.name.clone();
4373                let profile_region = profile.region.clone();
4374
4375                self.profile = profile_name.clone();
4376                std::env::set_var("AWS_PROFILE", &profile_name);
4377
4378                // Use profile's region if available
4379                if let Some(region) = profile_region {
4380                    self.region = region;
4381                }
4382
4383                self.mode = Mode::Normal;
4384                // Note: Changing profile requires reconnecting to AWS
4385            }
4386        } else if self.mode == Mode::ServicePicker {
4387            // If filter is active (INSERT mode), exit filter mode instead of selecting
4388            if self.service_picker.filter_active {
4389                self.service_picker.filter_active = false;
4390                return;
4391            }
4392
4393            let filtered = self.filtered_services();
4394            if let Some(&service) = filtered.get(self.service_picker.selected) {
4395                let new_service = match service {
4396                    "API Gateway › APIs" => Service::ApiGatewayApis,
4397                    "CloudWatch › Log Groups" => Service::CloudWatchLogGroups,
4398                    "CloudWatch › Logs Insights" => Service::CloudWatchInsights,
4399                    "CloudWatch › Alarms" => Service::CloudWatchAlarms,
4400                    "CloudTrail › Event History" => Service::CloudTrailEvents,
4401                    "CloudFormation › Stacks" => Service::CloudFormationStacks,
4402                    "EC2 › Instances" => Service::Ec2Instances,
4403                    "ECR › Repositories" => Service::EcrRepositories,
4404                    "KMS › Managed Keys" => Service::KmsKeys,
4405                    "EFS › File Systems" => Service::EfsFileSystems,
4406                    "FSx › File Systems" => Service::FsxFileSystems,
4407                    "IAM › Users" => Service::IamUsers,
4408                    "IAM › Roles" => Service::IamRoles,
4409                    "IAM › User Groups" => Service::IamUserGroups,
4410                    "Lambda › Functions" => Service::LambdaFunctions,
4411                    "Lambda › Applications" => Service::LambdaApplications,
4412                    "S3 › Buckets" => Service::S3Buckets,
4413                    "SQS › Queues" => Service::SqsQueues,
4414                    _ => return,
4415                };
4416
4417                // Create new tab
4418                self.tabs.push(Tab {
4419                    service: new_service,
4420                    title: service.to_string(),
4421                    breadcrumb: service.to_string(),
4422                });
4423                self.current_tab = self.tabs.len() - 1;
4424                self.current_service = new_service;
4425                self.view_mode = ViewMode::List;
4426                self.service_selected = true;
4427                self.mode = Mode::Normal;
4428            }
4429        } else if self.mode == Mode::TabPicker {
4430            let filtered = self.get_filtered_tabs();
4431            if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
4432                self.current_tab = idx;
4433                self.current_service = self.tabs[idx].service;
4434                self.mode = Mode::Normal;
4435                self.tab_filter.clear();
4436            }
4437        } else if self.mode == Mode::SessionPicker {
4438            let filtered = self.get_filtered_sessions();
4439            if let Some(&session) = filtered.get(self.session_picker_selected) {
4440                let session = session.clone();
4441
4442                // Load the selected session
4443                self.current_session = Some(session.clone());
4444                self.profile = session.profile.clone();
4445                self.region = session.region.clone();
4446                self.config.region = session.region.clone();
4447                self.config.account_id = session.account_id.clone();
4448                self.config.role_arn = session.role_arn.clone();
4449
4450                // Restore tabs
4451                self.tabs = session
4452                    .tabs
4453                    .iter()
4454                    .map(|st| Tab {
4455                        service: match st.service.as_str() {
4456                            "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
4457                            "CloudWatchInsights" => Service::CloudWatchInsights,
4458                            "CloudWatchAlarms" => Service::CloudWatchAlarms,
4459                            "S3Buckets" => Service::S3Buckets,
4460                            "CloudTrailEvents" => Service::CloudTrailEvents,
4461                            "SqsQueues" => Service::SqsQueues,
4462                            _ => Service::CloudWatchLogGroups,
4463                        },
4464                        title: st.title.clone(),
4465                        breadcrumb: st.breadcrumb.clone(),
4466                    })
4467                    .collect();
4468
4469                if !self.tabs.is_empty() {
4470                    self.current_tab = 0;
4471                    self.current_service = self.tabs[0].service;
4472                    self.service_selected = true;
4473                }
4474
4475                self.mode = Mode::Normal;
4476            }
4477        } else if self.mode == Mode::InsightsInput {
4478            // In InsightsInput mode, behavior depends on focus
4479            use crate::app::InsightsFocus;
4480            match self.insights_state.insights.insights_focus {
4481                InsightsFocus::Query => {
4482                    // Add newline to query
4483                    self.insights_state.insights.query_text.push('\n');
4484                    self.insights_state.insights.query_cursor_line += 1;
4485                    self.insights_state.insights.query_cursor_col = 0;
4486                }
4487                InsightsFocus::LogGroupSearch => {
4488                    // Toggle dropdown — when opening with empty search, show all log groups
4489                    let was_open = self.insights_state.insights.show_dropdown;
4490                    self.insights_state.insights.show_dropdown = !was_open;
4491                    if !was_open {
4492                        // Opening: populate matches with all available log groups if no search text
4493                        if self.insights_state.insights.log_group_search.is_empty() {
4494                            self.insights_state.insights.log_group_matches = self
4495                                .log_groups_state
4496                                .log_groups
4497                                .items
4498                                .iter()
4499                                .take(50)
4500                                .map(|g| g.name.clone())
4501                                .collect();
4502                        }
4503                        self.insights_state.insights.dropdown_selected = 0;
4504                    }
4505                }
4506                _ => {}
4507            }
4508        } else if self.mode == Mode::Normal {
4509            // If no service selected, select from service picker
4510            if !self.service_selected {
4511                let filtered = self.filtered_services();
4512                if let Some(&service) = filtered.get(self.service_picker.selected) {
4513                    match service {
4514                        "CloudWatch › Log Groups" => {
4515                            self.current_service = Service::CloudWatchLogGroups;
4516                            self.view_mode = ViewMode::List;
4517                            self.service_selected = true;
4518                        }
4519                        "CloudWatch › Logs Insights" => {
4520                            self.current_service = Service::CloudWatchInsights;
4521                            self.view_mode = ViewMode::InsightsResults;
4522                            self.service_selected = true;
4523                        }
4524                        "CloudWatch › Alarms" => {
4525                            self.current_service = Service::CloudWatchAlarms;
4526                            self.view_mode = ViewMode::List;
4527                            self.service_selected = true;
4528                        }
4529                        "S3 › Buckets" => {
4530                            self.current_service = Service::S3Buckets;
4531                            self.view_mode = ViewMode::List;
4532                            self.service_selected = true;
4533                        }
4534                        "EC2 › Instances" => {
4535                            self.current_service = Service::Ec2Instances;
4536                            self.view_mode = ViewMode::List;
4537                            self.service_selected = true;
4538                        }
4539                        "ECR › Repositories" => {
4540                            self.current_service = Service::EcrRepositories;
4541                            self.view_mode = ViewMode::List;
4542                            self.service_selected = true;
4543                        }
4544                        "Lambda › Functions" => {
4545                            self.current_service = Service::LambdaFunctions;
4546                            self.view_mode = ViewMode::List;
4547                            self.service_selected = true;
4548                        }
4549                        "Lambda › Applications" => {
4550                            self.current_service = Service::LambdaApplications;
4551                            self.view_mode = ViewMode::List;
4552                            self.service_selected = true;
4553                        }
4554                        _ => {}
4555                    }
4556                }
4557                return;
4558            }
4559
4560            // Select in content area
4561            if self.view_mode == ViewMode::InsightsResults {
4562                // Toggle expand for selected result
4563                if self.insights_state.insights.expanded_result
4564                    == Some(self.insights_state.insights.results_selected)
4565                {
4566                    self.insights_state.insights.expanded_result = None;
4567                } else {
4568                    self.insights_state.insights.expanded_result =
4569                        Some(self.insights_state.insights.results_selected);
4570                }
4571            } else if self.current_service == Service::S3Buckets {
4572                crate::s3::actions::select_item(self);
4573            } else if self.current_service == Service::ApiGatewayApis {
4574                crate::apig::actions::select_item(self);
4575            } else if self.current_service == Service::CloudFormationStacks {
4576                if self.cfn_state.current_stack.is_none() {
4577                    // Drill into stack detail view
4578                    let filtered_stacks = filtered_cloudformation_stacks(self);
4579                    if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
4580                        let stack_name = stack.name.clone();
4581                        let mut tags = stack.tags.clone();
4582                        tags.sort_by(|a, b| a.0.cmp(&b.0));
4583
4584                        self.cfn_state.current_stack = Some(stack_name);
4585                        self.cfn_state.tags.items = tags;
4586                        self.cfn_state.tags.reset();
4587                        self.cfn_state.table.loading = true;
4588                        self.update_current_tab_breadcrumb();
4589                    }
4590                }
4591            } else if self.current_service == Service::CloudWatchAlarms {
4592                crate::cw::actions::alarms_select_item(self);
4593            } else if self.current_service == Service::CloudTrailEvents {
4594                crate::cloudtrail::actions::select_item(self);
4595            } else if self.current_service == Service::EcrRepositories {
4596                crate::ecr::actions::select_item(self);
4597            } else if self.current_service == Service::KmsKeys {
4598                // KMS keys list — no drill-down yet; tab switch on Enter
4599            } else if self.current_service == Service::EfsFileSystems {
4600                crate::efs::actions::select_item(self);
4601            } else if self.current_service == Service::FsxFileSystems {
4602                crate::fsx::actions::select_item(self);
4603            } else if self.current_service == Service::Ec2Instances {
4604                crate::ec2::actions::select_item(self);
4605            } else if self.current_service == Service::SqsQueues {
4606                crate::sqs::actions::select_item(self);
4607            } else if self.current_service == Service::IamUsers {
4608                crate::iam::actions::select_item_users(self);
4609            } else if self.current_service == Service::IamRoles {
4610                crate::iam::actions::select_item_roles(self);
4611            } else if self.current_service == Service::IamUserGroups {
4612                crate::iam::actions::select_item_groups(self);
4613            } else if self.current_service == Service::LambdaFunctions {
4614                crate::lambda::functions::select_item(self);
4615            } else if self.current_service == Service::LambdaApplications {
4616                crate::lambda::applications::select_item(self);
4617            } else if self.current_service == Service::CloudWatchLogGroups {
4618                crate::cw::actions::logs_select_item(self);
4619            } else if self.current_service == Service::CloudWatchAlarms
4620                && self.view_mode != ViewMode::Detail
4621            {
4622                crate::cw::actions::alarms_select_expand(self);
4623            } else if self.current_service == Service::CloudWatchInsights {
4624                // In Normal mode, Enter always executes query
4625                if !self.insights_state.insights.selected_log_groups.is_empty() {
4626                    self.log_groups_state.loading = true;
4627                    self.insights_state.insights.query_completed = true;
4628                }
4629            }
4630        }
4631    }
4632
4633    pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
4634        self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
4635        Ok(())
4636    }
4637
4638    pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
4639        let alarm_data = self.alarms_client.list_alarms().await?;
4640        self.alarms_state.table.items = alarm_data
4641            .into_iter()
4642            .map(|d| Alarm {
4643                name: d.name,
4644                state: d.state,
4645                state_updated_timestamp: d.state_updated,
4646                description: d.description,
4647                metric_name: d.metric_name,
4648                namespace: d.namespace,
4649                statistic: d.statistic,
4650                period: d.period,
4651                comparison_operator: d.comparison,
4652                threshold: d.threshold,
4653                actions_enabled: d.actions_enabled,
4654                state_reason: d.state_reason,
4655                resource: d.resource,
4656                dimensions: d.dimensions,
4657                expression: d.expression,
4658                alarm_type: d.alarm_type,
4659                cross_account: d.cross_account,
4660                alarm_arn: d.alarm_arn,
4661                datapoints_to_alarm: d.datapoints_to_alarm,
4662                evaluation_periods: d.evaluation_periods,
4663                treat_missing_data: d.treat_missing_data,
4664                evaluate_low_sample_percentile: d.evaluate_low_sample_percentile,
4665                sub_metrics: d.sub_metrics,
4666            })
4667            .collect();
4668        Ok(())
4669    }
4670
4671    /// Fetch metric data for a metric math alarm using GetMetricData.
4672    pub async fn load_alarm_metric_math_data(&mut self, alarm_name: &str) -> anyhow::Result<()> {
4673        if let Some(alarm) = self
4674            .alarms_state
4675            .table
4676            .items
4677            .iter()
4678            .find(|a| a.name == alarm_name)
4679        {
4680            if alarm.sub_metrics.is_empty() {
4681                return Ok(());
4682            }
4683            let sub_metrics = alarm.sub_metrics.clone();
4684            let end_time = chrono::Utc::now();
4685            let start_time = end_time - chrono::TimeDelta::hours(72);
4686            let data = self
4687                .alarms_client
4688                .get_metric_math_data(&sub_metrics, start_time, end_time)
4689                .await?;
4690            self.alarms_state.metric_data = data;
4691        }
4692        Ok(())
4693    }
4694
4695    pub async fn load_cloudtrail_events(&mut self) -> anyhow::Result<()> {
4696        let filter = self.cloudtrail_state.active_event_name_filter.clone();
4697        let (events, next_token) = self
4698            .cloudtrail_client
4699            .lookup_events(None, None, filter)
4700            .await?;
4701        self.cloudtrail_state.table.items = events
4702            .into_iter()
4703            .map(
4704                |(
4705                    event_name,
4706                    event_time,
4707                    username,
4708                    event_source,
4709                    resource_type,
4710                    resource_name,
4711                    read_only,
4712                    aws_region,
4713                    event_id,
4714                    access_key_id,
4715                    source_ip_address,
4716                    error_code,
4717                    request_id,
4718                    event_type,
4719                    cloud_trail_event_json,
4720                )| CloudTrailEvent {
4721                    event_name,
4722                    event_time,
4723                    username,
4724                    event_source,
4725                    resource_type,
4726                    resource_name,
4727                    read_only,
4728                    aws_region,
4729                    event_id,
4730                    access_key_id,
4731                    source_ip_address,
4732                    error_code,
4733                    request_id,
4734                    event_type,
4735                    cloud_trail_event_json,
4736                },
4737            )
4738            .collect();
4739        self.cloudtrail_state.table.next_token = next_token;
4740        Ok(())
4741    }
4742
4743    pub async fn load_more_cloudtrail_events(&mut self) -> anyhow::Result<()> {
4744        if let Some(token) = self.cloudtrail_state.table.next_token.clone() {
4745            let filter = self.cloudtrail_state.active_event_name_filter.clone();
4746            // Just load the next batch of events
4747            let (events, next_token) = self
4748                .cloudtrail_client
4749                .lookup_events(None, Some(token), filter)
4750                .await?;
4751            self.cloudtrail_state
4752                .table
4753                .items
4754                .extend(events.into_iter().map(
4755                    |(
4756                        event_name,
4757                        event_time,
4758                        username,
4759                        event_source,
4760                        resource_type,
4761                        resource_name,
4762                        read_only,
4763                        aws_region,
4764                        event_id,
4765                        access_key_id,
4766                        source_ip_address,
4767                        error_code,
4768                        request_id,
4769                        event_type,
4770                        cloud_trail_event_json,
4771                    )| CloudTrailEvent {
4772                        event_name,
4773                        event_time,
4774                        username,
4775                        event_source,
4776                        resource_type,
4777                        resource_name,
4778                        read_only,
4779                        aws_region,
4780                        event_id,
4781                        access_key_id,
4782                        source_ip_address,
4783                        error_code,
4784                        request_id,
4785                        event_type,
4786                        cloud_trail_event_json,
4787                    },
4788                ));
4789            self.cloudtrail_state.table.next_token = next_token;
4790        }
4791        Ok(())
4792    }
4793
4794    pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
4795        if let Some(bucket_name) = &self.s3_state.current_bucket {
4796            // Get or fetch bucket region
4797            let bucket_region = if let Some(bucket) = self
4798                .s3_state
4799                .buckets
4800                .items
4801                .iter_mut()
4802                .find(|b| &b.name == bucket_name)
4803            {
4804                if bucket.region.is_empty() {
4805                    // Fetch the actual region
4806                    let region = self.s3_client.get_bucket_location(bucket_name).await?;
4807                    bucket.region = region.clone();
4808                    region
4809                } else {
4810                    bucket.region.clone()
4811                }
4812            } else {
4813                self.config.region.clone()
4814            };
4815
4816            let prefix = self
4817                .s3_state
4818                .prefix_stack
4819                .last()
4820                .cloned()
4821                .unwrap_or_default();
4822            let objects = self
4823                .s3_client
4824                .list_objects(bucket_name, &bucket_region, &prefix)
4825                .await?;
4826            self.s3_state.objects = objects
4827                .into_iter()
4828                .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
4829                    key,
4830                    size,
4831                    last_modified: modified,
4832                    is_prefix,
4833                    storage_class,
4834                })
4835                .collect();
4836            self.s3_state.selected_object = 0;
4837        }
4838        Ok(())
4839    }
4840
4841    pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
4842        let stored_region = self
4843            .s3_state
4844            .buckets
4845            .items
4846            .iter()
4847            .find(|b| b.name == bucket_name)
4848            .and_then(|b| {
4849                if b.region.is_empty() {
4850                    None
4851                } else {
4852                    Some(b.region.clone())
4853                }
4854            });
4855
4856        let bucket_region = match stored_region {
4857            Some(r) => r,
4858            None => {
4859                let r = self.s3_client.get_bucket_location(&bucket_name).await?;
4860                // Cache the discovered region back into the bucket
4861                if let Some(b) = self
4862                    .s3_state
4863                    .buckets
4864                    .items
4865                    .iter_mut()
4866                    .find(|b| b.name == bucket_name)
4867                {
4868                    b.region = r.clone();
4869                }
4870                r
4871            }
4872        };
4873        let objects = self
4874            .s3_client
4875            .list_objects(&bucket_name, &bucket_region, "")
4876            .await?;
4877        let preview: Vec<S3Object> = objects
4878            .into_iter()
4879            .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
4880                key,
4881                size,
4882                last_modified: modified,
4883                is_prefix,
4884                storage_class,
4885            })
4886            .collect();
4887        self.s3_state
4888            .bucket_preview
4889            .insert(bucket_name.clone(), preview);
4890        self.after_bucket_preview_loaded(&bucket_name);
4891        Ok(())
4892    }
4893
4894    /// Called after a bucket's preview is loaded. If the bucket is the currently
4895    /// selected bucket (selection is on the bucket row itself), advance selection
4896    /// to the first child row so the user lands inside the bucket automatically.
4897    pub fn after_bucket_preview_loaded(&mut self, bucket_name: &str) {
4898        if self.s3_state.current_bucket.is_some() {
4899            return; // inside a bucket view, not the bucket list
4900        }
4901        // Find the row index of this bucket in the current view
4902        let mut row_idx = 0usize;
4903        for b in &self.s3_state.buckets.items {
4904            if b.name == bucket_name {
4905                // Only advance if selection is sitting on this exact bucket row
4906                if self.s3_state.selected_row == row_idx {
4907                    if let Some(preview) = self.s3_state.bucket_preview.get(bucket_name) {
4908                        if !preview.is_empty() {
4909                            self.s3_state.selected_row = row_idx + 1;
4910                            let visible = self.s3_state.bucket_visible_rows.get();
4911                            if self.s3_state.selected_row
4912                                >= self.s3_state.bucket_scroll_offset + visible
4913                            {
4914                                self.s3_state.bucket_scroll_offset =
4915                                    self.s3_state.selected_row.saturating_sub(visible - 1);
4916                            }
4917                        }
4918                    }
4919                }
4920                return;
4921            }
4922            row_idx += 1;
4923            // Account for this bucket's loaded children
4924            if self.s3_state.expanded_prefixes.contains(&b.name) {
4925                if let Some(p) = self.s3_state.bucket_preview.get(&b.name) {
4926                    row_idx += p.len();
4927                }
4928            }
4929        }
4930    }
4931
4932    pub async fn load_prefix_preview(
4933        &mut self,
4934        bucket_name: String,
4935        prefix: String,
4936    ) -> anyhow::Result<()> {
4937        let stored_region = self
4938            .s3_state
4939            .buckets
4940            .items
4941            .iter()
4942            .find(|b| b.name == bucket_name)
4943            .and_then(|b| {
4944                if b.region.is_empty() {
4945                    None
4946                } else {
4947                    Some(b.region.clone())
4948                }
4949            });
4950
4951        let bucket_region = match stored_region {
4952            Some(r) => r,
4953            None => {
4954                let r = self.s3_client.get_bucket_location(&bucket_name).await?;
4955                if let Some(b) = self
4956                    .s3_state
4957                    .buckets
4958                    .items
4959                    .iter_mut()
4960                    .find(|b| b.name == bucket_name)
4961                {
4962                    b.region = r.clone();
4963                }
4964                r
4965            }
4966        };
4967        let objects = self
4968            .s3_client
4969            .list_objects(&bucket_name, &bucket_region, &prefix)
4970            .await?;
4971        let preview: Vec<S3Object> = objects
4972            .into_iter()
4973            .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
4974                key,
4975                size,
4976                last_modified: modified,
4977                is_prefix,
4978                storage_class,
4979            })
4980            .collect();
4981        self.s3_state.prefix_preview.insert(prefix, preview);
4982        Ok(())
4983    }
4984
4985    pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
4986        let repos = match self.ecr_state.tab {
4987            EcrTab::Private => self.ecr_client.list_private_repositories().await?,
4988            EcrTab::Public => self.ecr_client.list_public_repositories().await?,
4989        };
4990
4991        self.ecr_state.repositories.items = repos
4992            .into_iter()
4993            .map(|r| EcrRepository {
4994                name: r.name,
4995                uri: r.uri,
4996                created_at: r.created_at,
4997                tag_immutability: r.tag_immutability,
4998                encryption_type: r.encryption_type,
4999            })
5000            .collect();
5001
5002        self.ecr_state
5003            .repositories
5004            .items
5005            .sort_by(|a, b| a.name.cmp(&b.name));
5006        Ok(())
5007    }
5008
5009    pub async fn load_kms_keys(&mut self) -> anyhow::Result<()> {
5010        let keys = self.kms_client.list_keys().await?;
5011        self.kms_state.keys.items = keys
5012            .into_iter()
5013            .map(|k| KmsKey {
5014                key_id: k.key_id,
5015                key_arn: k.key_arn,
5016                alias: k.alias,
5017                description: k.description,
5018                key_state: k.key_state,
5019                key_usage: k.key_usage,
5020                key_spec: k.key_spec,
5021                key_manager: k.key_manager,
5022                creation_date: k.creation_date,
5023                expiration_date: k.expiration_date,
5024                deletion_date: k.deletion_date,
5025                custom_key_store_id: k.custom_key_store_id,
5026                origin: k.origin,
5027                multi_region: k.multi_region,
5028                enabled: k.enabled,
5029            })
5030            .collect();
5031        Ok(())
5032    }
5033
5034    pub async fn load_efs_file_systems(&mut self) -> anyhow::Result<()> {
5035        let file_systems = self.efs_client.list_file_systems().await?;
5036        self.efs_state.file_systems.items = file_systems
5037            .into_iter()
5038            .map(|f| EfsFileSystem {
5039                file_system_id: f.file_system_id,
5040                file_system_arn: f.file_system_arn,
5041                name: f.name,
5042                creation_token: f.creation_token,
5043                encrypted: f.encrypted,
5044                kms_key_id: f.kms_key_id,
5045                total_size: f.total_size,
5046                size_in_standard: f.size_in_standard,
5047                size_in_ia: f.size_in_ia,
5048                size_in_archive: f.size_in_archive,
5049                provisioned_throughput: f.provisioned_throughput,
5050                throughput_mode: f.throughput_mode,
5051                life_cycle_state: f.life_cycle_state,
5052                number_of_mount_targets: f.number_of_mount_targets,
5053                owner_id: f.owner_id,
5054                creation_time: f.creation_time,
5055                performance_mode: f.performance_mode,
5056                availability_zone: f.availability_zone,
5057                replication_overwrite_protection: f.replication_overwrite_protection,
5058                dns_name: f.dns_name,
5059                tags: f.tags,
5060                total_size_bytes: f.total_size_bytes,
5061                size_in_standard_bytes: f.size_in_standard_bytes,
5062                size_in_ia_bytes: f.size_in_ia_bytes,
5063                size_in_archive_bytes: f.size_in_archive_bytes,
5064            })
5065            .collect();
5066        Ok(())
5067    }
5068
5069    pub async fn load_fsx_file_systems(&mut self) -> anyhow::Result<()> {
5070        let file_systems = self.fsx_client.list_file_systems().await?;
5071        self.fsx_state.file_systems.items = file_systems;
5072        Ok(())
5073    }
5074
5075    pub async fn load_fsx_backups(&mut self, file_system_id: &str) -> anyhow::Result<()> {
5076        let backups = self.fsx_client.describe_backups(file_system_id).await?;
5077        self.fsx_state.backups.items = backups;
5078        self.fsx_state.backups.selected = 0;
5079        self.fsx_state.backups.expanded_item = None;
5080        Ok(())
5081    }
5082
5083    pub async fn load_apis(&mut self) -> anyhow::Result<()> {
5084        let apis = self.apig_client.list_rest_apis().await?;
5085
5086        self.apig_state.apis.items = apis
5087            .into_iter()
5088            .map(|a| crate::apig::api::RestApi {
5089                id: a.id,
5090                name: a.name,
5091                description: a.description,
5092                created_date: a.created_date,
5093                api_key_source: a.api_key_source,
5094                endpoint_configuration: a.endpoint_configuration,
5095                protocol_type: a.protocol_type,
5096                disable_execute_api_endpoint: a.disable_execute_api_endpoint,
5097                status: a.status,
5098            })
5099            .collect();
5100
5101        self.apig_state
5102            .apis
5103            .items
5104            .sort_by(|a, b| a.name.cmp(&b.name));
5105        Ok(())
5106    }
5107
5108    pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
5109        let instances = self.ec2_client.list_instances().await?;
5110
5111        self.ec2_state.table.items = instances
5112            .into_iter()
5113            .map(|i| Ec2Instance {
5114                instance_id: i.instance_id,
5115                name: i.name,
5116                state: i.state,
5117                instance_type: i.instance_type,
5118                availability_zone: i.availability_zone,
5119                public_ipv4_dns: i.public_ipv4_dns,
5120                public_ipv4_address: i.public_ipv4_address,
5121                elastic_ip: i.elastic_ip,
5122                ipv6_ips: i.ipv6_ips,
5123                monitoring: i.monitoring,
5124                security_groups: i.security_groups,
5125                key_name: i.key_name,
5126                launch_time: i.launch_time,
5127                platform_details: i.platform_details,
5128                status_checks: i.status_checks,
5129                alarm_status: i.alarm_status,
5130                private_dns_name: i.private_dns_name,
5131                private_ip_address: i.private_ip_address,
5132                security_group_ids: String::new(),
5133                owner_id: String::new(),
5134                volume_id: String::new(),
5135                root_device_name: String::new(),
5136                root_device_type: String::new(),
5137                ebs_optimized: String::new(),
5138                image_id: String::new(),
5139                kernel_id: String::new(),
5140                ramdisk_id: String::new(),
5141                ami_launch_index: String::new(),
5142                reservation_id: String::new(),
5143                vpc_id: String::new(),
5144                subnet_ids: String::new(),
5145                instance_lifecycle: String::new(),
5146                architecture: String::new(),
5147                virtualization_type: String::new(),
5148                platform: String::new(),
5149                iam_instance_profile_arn: String::new(),
5150                tenancy: String::new(),
5151                affinity: String::new(),
5152                host_id: String::new(),
5153                placement_group: String::new(),
5154                partition_number: String::new(),
5155                capacity_reservation_id: String::new(),
5156                state_transition_reason_code: String::new(),
5157                state_transition_reason_message: String::new(),
5158                stop_hibernation_behavior: String::new(),
5159                outpost_arn: String::new(),
5160                product_codes: String::new(),
5161                availability_zone_id: String::new(),
5162                imdsv2: String::new(),
5163                usage_operation: String::new(),
5164                managed: String::new(),
5165                operator: String::new(),
5166            })
5167            .collect();
5168
5169        // Sort by launch time descending by default
5170        self.ec2_state
5171            .table
5172            .items
5173            .sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
5174        Ok(())
5175    }
5176
5177    pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
5178        if let Some(repo_name) = &self.ecr_state.current_repository {
5179            if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
5180                let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
5181
5182                self.ecr_state.images.items = images
5183                    .into_iter()
5184                    .map(|i| EcrImage {
5185                        tag: i.tag,
5186                        artifact_type: i.artifact_type,
5187                        pushed_at: i.pushed_at,
5188                        size_bytes: i.size_bytes,
5189                        uri: i.uri,
5190                        digest: i.digest,
5191                        last_pull_time: i.last_pull_time,
5192                    })
5193                    .collect();
5194
5195                self.ecr_state
5196                    .images
5197                    .items
5198                    .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
5199            }
5200        }
5201        Ok(())
5202    }
5203
5204    pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
5205        let stacks = self
5206            .cloudformation_client
5207            .list_stacks(self.cfn_state.view_nested)
5208            .await?;
5209
5210        let mut stacks: Vec<CfnStack> = stacks
5211            .into_iter()
5212            .map(|s| CfnStack {
5213                name: s.name,
5214                stack_id: s.stack_id,
5215                status: s.status,
5216                created_time: s.created_time,
5217                updated_time: s.updated_time,
5218                deleted_time: s.deleted_time,
5219                drift_status: s.drift_status,
5220                last_drift_check_time: s.last_drift_check_time,
5221                status_reason: s.status_reason,
5222                description: s.description,
5223                detailed_status: String::new(),
5224                root_stack: s.root_stack,
5225                parent_stack: s.parent_stack,
5226                termination_protection: false,
5227                iam_role: String::new(),
5228                tags: Vec::new(),
5229                stack_policy: String::new(),
5230                rollback_monitoring_time: String::new(),
5231                rollback_alarms: Vec::new(),
5232                notification_arns: Vec::new(),
5233            })
5234            .collect();
5235
5236        // Sort by created_time DESC
5237        stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
5238
5239        self.cfn_state.table.items = stacks;
5240
5241        Ok(())
5242    }
5243
5244    pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
5245        let template = self.cloudformation_client.get_template(stack_name).await?;
5246        self.cfn_state.template_body = template;
5247        self.cfn_state.template_scroll = 0;
5248        Ok(())
5249    }
5250
5251    pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
5252        let mut parameters = self
5253            .cloudformation_client
5254            .get_stack_parameters(stack_name)
5255            .await?;
5256        parameters.sort_by(|a, b| a.key.cmp(&b.key));
5257        self.cfn_state.parameters.items = parameters;
5258        self.cfn_state.parameters.reset();
5259        Ok(())
5260    }
5261
5262    pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
5263        let outputs = self
5264            .cloudformation_client
5265            .get_stack_outputs(stack_name)
5266            .await?;
5267        self.cfn_state.outputs.items = outputs;
5268        self.cfn_state.outputs.reset();
5269        Ok(())
5270    }
5271
5272    pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
5273        let resources = self
5274            .cloudformation_client
5275            .get_stack_resources(stack_name)
5276            .await?;
5277        self.cfn_state.resources.items = resources;
5278        self.cfn_state.resources.reset();
5279        Ok(())
5280    }
5281
5282    pub async fn load_cfn_events(&mut self, stack_name: &str) -> anyhow::Result<()> {
5283        let events = self
5284            .cloudformation_client
5285            .list_stack_events(stack_name)
5286            .await?;
5287        self.cfn_state.events.items = events;
5288        self.cfn_state.events.reset();
5289        Ok(())
5290    }
5291
5292    pub async fn load_cfn_change_sets(&mut self, stack_name: &str) -> anyhow::Result<()> {
5293        let change_sets = self
5294            .cloudformation_client
5295            .list_change_sets(stack_name)
5296            .await?;
5297        self.cfn_state.change_sets.items = change_sets;
5298        self.cfn_state.change_sets.reset();
5299        Ok(())
5300    }
5301
5302    pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
5303        // Load attached (managed) policies
5304        let attached_policies = self
5305            .iam_client
5306            .list_attached_role_policies(role_name)
5307            .await
5308            .map_err(|e| anyhow::anyhow!(e))?;
5309
5310        let mut policies: Vec<IamPolicy> = Vec::new();
5311        for p in attached_policies {
5312            let (creation_time, edited_time) = if let Some(arn) = p.policy_arn() {
5313                self.iam_client
5314                    .get_policy_metadata(arn)
5315                    .await
5316                    .unwrap_or_else(|_| ("-".to_string(), "-".to_string()))
5317            } else {
5318                ("-".to_string(), "-".to_string())
5319            };
5320            policies.push(IamPolicy {
5321                policy_name: p.policy_name().unwrap_or("").to_string(),
5322                policy_type: "Managed".to_string(),
5323                attached_via: "Direct".to_string(),
5324                attached_entities: "-".to_string(),
5325                description: "-".to_string(),
5326                creation_time,
5327                edited_time,
5328                policy_arn: p.policy_arn().map(|s| s.to_string()),
5329            });
5330        }
5331
5332        // Load inline policies
5333        let inline_policy_names = self
5334            .iam_client
5335            .list_role_policies(role_name)
5336            .await
5337            .map_err(|e| anyhow::anyhow!(e))?;
5338
5339        for policy_name in inline_policy_names {
5340            policies.push(IamPolicy {
5341                policy_name,
5342                policy_type: "Inline".to_string(),
5343                attached_via: "Direct".to_string(),
5344                attached_entities: "-".to_string(),
5345                description: "-".to_string(),
5346                creation_time: "-".to_string(),
5347                edited_time: "-".to_string(),
5348                policy_arn: None,
5349            });
5350        }
5351
5352        self.iam_state.policies.items = policies;
5353
5354        Ok(())
5355    }
5356
5357    pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
5358        let attached_policies = self
5359            .iam_client
5360            .list_attached_group_policies(group_name)
5361            .await
5362            .map_err(|e| anyhow::anyhow!(e))?;
5363
5364        let mut policies: Vec<IamPolicy> = attached_policies
5365            .into_iter()
5366            .map(|p| IamPolicy {
5367                policy_name: p.policy_name().unwrap_or("").to_string(),
5368                policy_type: "AWS managed".to_string(),
5369                attached_via: "Direct".to_string(),
5370                attached_entities: "-".to_string(),
5371                description: "-".to_string(),
5372                creation_time: "-".to_string(),
5373                edited_time: "-".to_string(),
5374                policy_arn: p.policy_arn().map(|s| s.to_string()),
5375            })
5376            .collect();
5377
5378        let inline_policy_names = self
5379            .iam_client
5380            .list_group_policies(group_name)
5381            .await
5382            .map_err(|e| anyhow::anyhow!(e))?;
5383
5384        for policy_name in inline_policy_names {
5385            policies.push(IamPolicy {
5386                policy_name,
5387                policy_type: "Inline".to_string(),
5388                attached_via: "Direct".to_string(),
5389                attached_entities: "-".to_string(),
5390                description: "-".to_string(),
5391                creation_time: "-".to_string(),
5392                edited_time: "-".to_string(),
5393                policy_arn: None,
5394            });
5395        }
5396
5397        self.iam_state.policies.items = policies;
5398
5399        Ok(())
5400    }
5401
5402    pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
5403        let users = self
5404            .iam_client
5405            .get_group_users(group_name)
5406            .await
5407            .map_err(|e| anyhow::anyhow!(e))?;
5408
5409        let group_users: Vec<IamGroupUser> = users
5410            .into_iter()
5411            .map(|u| {
5412                let creation_time = {
5413                    let dt = u.create_date();
5414                    let timestamp = dt.secs();
5415                    let datetime =
5416                        chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
5417                    datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
5418                };
5419
5420                IamGroupUser {
5421                    user_name: u.user_name().to_string(),
5422                    groups: String::new(),
5423                    last_activity: String::new(),
5424                    creation_time,
5425                }
5426            })
5427            .collect();
5428
5429        self.iam_state.group_users.items = group_users;
5430
5431        Ok(())
5432    }
5433
5434    pub async fn load_policy_document(
5435        &mut self,
5436        role_name: &str,
5437        policy_name: &str,
5438    ) -> anyhow::Result<()> {
5439        // Find the policy to get its ARN and type
5440        let policy = self
5441            .iam_state
5442            .policies
5443            .items
5444            .iter()
5445            .find(|p| p.policy_name == policy_name)
5446            .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
5447
5448        let document = if let Some(policy_arn) = &policy.policy_arn {
5449            // Managed policy - use get_policy_version
5450            self.iam_client
5451                .get_policy_version(policy_arn)
5452                .await
5453                .map_err(|e| anyhow::anyhow!(e))?
5454        } else {
5455            // Inline policy - use get_role_policy
5456            self.iam_client
5457                .get_role_policy(role_name, policy_name)
5458                .await
5459                .map_err(|e| anyhow::anyhow!(e))?
5460        };
5461
5462        self.iam_state.policy_document = document;
5463
5464        Ok(())
5465    }
5466
5467    pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
5468        let document = self
5469            .iam_client
5470            .get_role(role_name)
5471            .await
5472            .map_err(|e| anyhow::anyhow!(e))?;
5473
5474        self.iam_state.trust_policy_document = document;
5475
5476        Ok(())
5477    }
5478
5479    pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
5480        // TODO: Implement real AWS API call to get service last accessed details
5481        self.iam_state.last_accessed_services.items = vec![];
5482        self.iam_state.last_accessed_services.selected = 0;
5483
5484        Ok(())
5485    }
5486
5487    pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
5488        let tags = self
5489            .iam_client
5490            .list_role_tags(role_name)
5491            .await
5492            .map_err(|e| anyhow::anyhow!(e))?;
5493        self.iam_state.tags.items = tags
5494            .into_iter()
5495            .map(|(k, v)| IamRoleTag { key: k, value: v })
5496            .collect();
5497        self.iam_state.tags.reset();
5498        Ok(())
5499    }
5500
5501    pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
5502        let tags = self
5503            .iam_client
5504            .list_user_tags(user_name)
5505            .await
5506            .map_err(|e| anyhow::anyhow!(e))?;
5507        self.iam_state.user_tags.items = tags
5508            .into_iter()
5509            .map(|(k, v)| IamUserTag { key: k, value: v })
5510            .collect();
5511        self.iam_state.user_tags.reset();
5512        Ok(())
5513    }
5514
5515    pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
5516        if let Some(group) = self
5517            .log_groups_state
5518            .log_groups
5519            .items
5520            .get(self.log_groups_state.log_groups.selected)
5521        {
5522            self.log_groups_state.log_streams =
5523                self.cloudwatch_client.list_log_streams(&group.name).await?;
5524            self.log_groups_state.selected_stream = 0;
5525        }
5526        Ok(())
5527    }
5528
5529    pub async fn load_log_group_tags(&mut self) -> anyhow::Result<()> {
5530        if let Some(group) = self
5531            .log_groups_state
5532            .log_groups
5533            .items
5534            .get(self.log_groups_state.log_groups.selected)
5535        {
5536            // Use log_group_arn if available, otherwise construct from name
5537            let arn = if let Some(arn) = &group.log_group_arn {
5538                arn.clone()
5539            } else if let Some(arn) = &group.arn {
5540                arn.clone()
5541            } else {
5542                // Construct ARN from log group name
5543                let account_id = if self.config.account_id.is_empty() {
5544                    "*"
5545                } else {
5546                    &self.config.account_id
5547                };
5548                format!(
5549                    "arn:aws:logs:{}:{}:log-group:{}",
5550                    self.config.region, account_id, group.name
5551                )
5552            };
5553
5554            let tags = self.cloudwatch_client.list_tags_for_log_group(&arn).await?;
5555            self.log_groups_state.tags.items = tags;
5556            self.log_groups_state.tags.selected = 0;
5557            self.log_groups_state.tags.scroll_offset = 0;
5558        }
5559        Ok(())
5560    }
5561
5562    pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
5563        if let Some(group) = self
5564            .log_groups_state
5565            .log_groups
5566            .items
5567            .get(self.log_groups_state.log_groups.selected)
5568        {
5569            if let Some(stream) = self
5570                .log_groups_state
5571                .log_streams
5572                .get(self.log_groups_state.selected_stream)
5573            {
5574                // Calculate time range from relative date picker
5575                let (start_time, end_time) =
5576                    if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
5577                        let now = chrono::Utc::now().timestamp_millis();
5578                        let duration_ms = match self.log_groups_state.relative_unit {
5579                            TimeUnit::Minutes => amount * 60 * 1000,
5580                            TimeUnit::Hours => amount * 60 * 60 * 1000,
5581                            TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
5582                            TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
5583                        };
5584                        (Some(now - duration_ms), Some(now))
5585                    } else {
5586                        (None, None)
5587                    };
5588
5589                let (mut events, has_more, token) = self
5590                    .cloudwatch_client
5591                    .get_log_events(
5592                        &group.name,
5593                        &stream.name,
5594                        self.log_groups_state.next_backward_token.clone(),
5595                        start_time,
5596                        end_time,
5597                    )
5598                    .await?;
5599
5600                if self.log_groups_state.next_backward_token.is_some() {
5601                    // Prepend older events - keep selection at top
5602                    events.append(&mut self.log_groups_state.log_events);
5603                    self.log_groups_state.event_scroll_offset = 0;
5604                } else {
5605                    // Initial load - start at first event
5606                    self.log_groups_state.event_scroll_offset = 0;
5607                }
5608
5609                self.log_groups_state.log_events = events;
5610                self.log_groups_state.has_older_events =
5611                    has_more && self.log_groups_state.log_events.len() >= 25;
5612                self.log_groups_state.next_backward_token = token;
5613                self.log_groups_state.selected_event = 0;
5614            }
5615        }
5616        Ok(())
5617    }
5618
5619    pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
5620        if self.insights_state.insights.selected_log_groups.is_empty() {
5621            return Err(anyhow::anyhow!(
5622                "No log groups selected. Please select at least one log group."
5623            ));
5624        }
5625
5626        let now = chrono::Utc::now().timestamp_millis();
5627        let amount = self
5628            .insights_state
5629            .insights
5630            .insights_relative_amount
5631            .parse::<i64>()
5632            .unwrap_or(1);
5633        let duration_ms = match self.insights_state.insights.insights_relative_unit {
5634            TimeUnit::Minutes => amount * 60 * 1000,
5635            TimeUnit::Hours => amount * 60 * 60 * 1000,
5636            TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
5637            TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
5638        };
5639        let start_time = now - duration_ms;
5640
5641        let query_id = self
5642            .cloudwatch_client
5643            .start_query(
5644                self.insights_state.insights.selected_log_groups.clone(),
5645                self.insights_state.insights.query_text.trim().to_string(),
5646                start_time,
5647                now,
5648            )
5649            .await?;
5650
5651        // Poll for results
5652        for _ in 0..60 {
5653            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
5654            let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
5655
5656            if status == "Complete" {
5657                self.insights_state.insights.query_results = results;
5658                self.insights_state.insights.query_completed = true;
5659                self.insights_state.insights.results_selected = 0;
5660                self.insights_state.insights.expanded_result = None;
5661                self.view_mode = ViewMode::InsightsResults;
5662                return Ok(());
5663            } else if status == "Failed" || status == "Cancelled" {
5664                return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
5665            }
5666        }
5667
5668        Err(anyhow::anyhow!("Query timeout"))
5669    }
5670
5671    /// Toggle column visibility, ensuring at least one column remains visible
5672    pub(crate) fn toggle_column_visibility<T: PartialEq + Copy>(
5673        visible_columns: &mut Vec<T>,
5674        _all_columns: &[T],
5675        column_to_toggle: T,
5676    ) {
5677        if let Some(pos) = visible_columns.iter().position(|c| c == &column_to_toggle) {
5678            // Only remove if more than one column is visible
5679            if visible_columns.len() > 1 {
5680                visible_columns.remove(pos);
5681            }
5682        } else {
5683            visible_columns.push(column_to_toggle);
5684        }
5685    }
5686}
5687
5688impl CloudWatchInsightsState {
5689    fn new() -> Self {
5690        Self {
5691            insights: InsightsState::default(),
5692            loading: false,
5693        }
5694    }
5695}
5696
5697impl CloudWatchAlarmsState {
5698    fn new() -> Self {
5699        Self {
5700            table: TableState::new(),
5701            current_alarm: None,
5702            alarm_tab: AlarmTab::AllAlarms,
5703            detail_tab: AlarmDetailTab::Details,
5704            view_as: AlarmViewMode::Table,
5705            wrap_lines: false,
5706            sort_column: "Last state update".to_string(),
5707            sort_direction: SortDirection::Asc,
5708            input_focus: InputFocus::Filter,
5709            metric_data: Vec::new(),
5710            metrics_loading: false,
5711        }
5712    }
5713}
5714
5715impl ServicePickerState {
5716    fn new() -> Self {
5717        Self {
5718            filter: String::new(),
5719            filter_active: false,
5720            selected: 0,
5721            services: vec![
5722                "API Gateway › APIs",
5723                "CloudWatch › Log Groups",
5724                "CloudWatch › Logs Insights",
5725                "CloudWatch › Alarms",
5726                "CloudTrail › Event History",
5727                "CloudFormation › Stacks",
5728                "EC2 › Instances",
5729                "ECR › Repositories",
5730                "IAM › Users",
5731                "IAM › Roles",
5732                "IAM › User Groups",
5733                "KMS › Managed Keys",
5734                "EFS › File Systems",
5735                "FSx › File Systems",
5736                "Lambda › Functions",
5737                "Lambda › Applications",
5738                "S3 › Buckets",
5739                "SQS › Queues",
5740            ],
5741        }
5742    }
5743}
5744
5745#[cfg(test)]
5746mod test_helpers {
5747    use super::*;
5748
5749    // Test helper functions to reduce boilerplate
5750    pub fn test_app() -> App {
5751        App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
5752    }
5753
5754    pub fn test_app_no_region() -> App {
5755        App::new_without_client("test".to_string(), None)
5756    }
5757}
5758
5759#[cfg(test)]
5760mod tests {
5761    use super::*;
5762    use crate::keymap::Action;
5763    use test_helpers::*;
5764
5765    #[test]
5766    fn test_next_tab_cycles_forward() {
5767        let mut app = test_app();
5768        app.tabs = vec![
5769            Tab {
5770                service: Service::CloudWatchLogGroups,
5771                title: "CloudWatch › Log Groups".to_string(),
5772                breadcrumb: "CloudWatch › Log Groups".to_string(),
5773            },
5774            Tab {
5775                service: Service::CloudWatchInsights,
5776                title: "CloudWatch › Logs Insights".to_string(),
5777                breadcrumb: "CloudWatch › Logs Insights".to_string(),
5778            },
5779            Tab {
5780                service: Service::CloudWatchAlarms,
5781                title: "CloudWatch › Alarms".to_string(),
5782                breadcrumb: "CloudWatch › Alarms".to_string(),
5783            },
5784        ];
5785        app.current_tab = 0;
5786
5787        app.handle_action(Action::NextTab);
5788        assert_eq!(app.current_tab, 1);
5789        assert_eq!(app.current_service, Service::CloudWatchInsights);
5790
5791        app.handle_action(Action::NextTab);
5792        assert_eq!(app.current_tab, 2);
5793        assert_eq!(app.current_service, Service::CloudWatchAlarms);
5794
5795        // Should wrap around
5796        app.handle_action(Action::NextTab);
5797        assert_eq!(app.current_tab, 0);
5798        assert_eq!(app.current_service, Service::CloudWatchLogGroups);
5799    }
5800
5801    #[test]
5802    fn test_prev_tab_cycles_backward() {
5803        let mut app = test_app();
5804        app.tabs = vec![
5805            Tab {
5806                service: Service::CloudWatchLogGroups,
5807                title: "CloudWatch › Log Groups".to_string(),
5808                breadcrumb: "CloudWatch › Log Groups".to_string(),
5809            },
5810            Tab {
5811                service: Service::CloudWatchInsights,
5812                title: "CloudWatch › Logs Insights".to_string(),
5813                breadcrumb: "CloudWatch › Logs Insights".to_string(),
5814            },
5815            Tab {
5816                service: Service::CloudWatchAlarms,
5817                title: "CloudWatch › Alarms".to_string(),
5818                breadcrumb: "CloudWatch › Alarms".to_string(),
5819            },
5820        ];
5821        app.current_tab = 2;
5822
5823        app.handle_action(Action::PrevTab);
5824        assert_eq!(app.current_tab, 1);
5825        assert_eq!(app.current_service, Service::CloudWatchInsights);
5826
5827        app.handle_action(Action::PrevTab);
5828        assert_eq!(app.current_tab, 0);
5829        assert_eq!(app.current_service, Service::CloudWatchLogGroups);
5830
5831        // Should wrap around
5832        app.handle_action(Action::PrevTab);
5833        assert_eq!(app.current_tab, 2);
5834        assert_eq!(app.current_service, Service::CloudWatchAlarms);
5835    }
5836
5837    #[test]
5838    fn test_close_tab_removes_current() {
5839        let mut app = test_app();
5840        app.tabs = vec![
5841            Tab {
5842                service: Service::CloudWatchLogGroups,
5843                title: "CloudWatch › Log Groups".to_string(),
5844                breadcrumb: "CloudWatch › Log Groups".to_string(),
5845            },
5846            Tab {
5847                service: Service::CloudWatchInsights,
5848                title: "CloudWatch › Logs Insights".to_string(),
5849                breadcrumb: "CloudWatch › Logs Insights".to_string(),
5850            },
5851            Tab {
5852                service: Service::CloudWatchAlarms,
5853                title: "CloudWatch › Alarms".to_string(),
5854                breadcrumb: "CloudWatch › Alarms".to_string(),
5855            },
5856        ];
5857        app.current_tab = 1;
5858        app.service_selected = true;
5859
5860        app.handle_action(Action::CloseTab);
5861        assert_eq!(app.tabs.len(), 2);
5862        assert_eq!(app.current_tab, 1);
5863        assert_eq!(app.current_service, Service::CloudWatchAlarms);
5864    }
5865
5866    #[test]
5867    fn test_close_last_tab_exits_service() {
5868        let mut app = test_app();
5869        app.tabs = vec![Tab {
5870            service: Service::CloudWatchLogGroups,
5871            title: "CloudWatch › Log Groups".to_string(),
5872            breadcrumb: "CloudWatch › Log Groups".to_string(),
5873        }];
5874        app.current_tab = 0;
5875        app.service_selected = true;
5876
5877        app.handle_action(Action::CloseTab);
5878        assert_eq!(app.tabs.len(), 0);
5879        assert!(!app.service_selected);
5880        assert_eq!(app.current_tab, 0);
5881    }
5882
5883    #[test]
5884    fn test_close_service_removes_current_tab() {
5885        let mut app = test_app();
5886        app.tabs = vec![
5887            Tab {
5888                service: Service::CloudWatchLogGroups,
5889                title: "CloudWatch › Log Groups".to_string(),
5890                breadcrumb: "CloudWatch › Log Groups".to_string(),
5891            },
5892            Tab {
5893                service: Service::CloudWatchInsights,
5894                title: "CloudWatch › Logs Insights".to_string(),
5895                breadcrumb: "CloudWatch › Logs Insights".to_string(),
5896            },
5897            Tab {
5898                service: Service::CloudWatchAlarms,
5899                title: "CloudWatch › Alarms".to_string(),
5900                breadcrumb: "CloudWatch › Alarms".to_string(),
5901            },
5902        ];
5903        app.current_tab = 1;
5904        app.service_selected = true;
5905
5906        app.handle_action(Action::CloseService);
5907
5908        // Tab should be removed
5909        assert_eq!(app.tabs.len(), 2);
5910        // Should switch to next tab (Alarms at index 1)
5911        assert_eq!(app.current_tab, 1);
5912        assert_eq!(app.current_service, Service::CloudWatchAlarms);
5913        // Should stay in service mode, NOT show service picker
5914        assert!(app.service_selected);
5915        assert_eq!(app.mode, Mode::Normal);
5916    }
5917
5918    #[test]
5919    fn test_close_service_last_tab_shows_picker() {
5920        let mut app = test_app();
5921        app.tabs = vec![Tab {
5922            service: Service::CloudWatchLogGroups,
5923            title: "CloudWatch › Log Groups".to_string(),
5924            breadcrumb: "CloudWatch › Log Groups".to_string(),
5925        }];
5926        app.current_tab = 0;
5927        app.service_selected = true;
5928
5929        app.handle_action(Action::CloseService);
5930
5931        // Tab should be removed
5932        assert_eq!(app.tabs.len(), 0);
5933        // Should show service picker
5934        assert!(!app.service_selected);
5935        assert_eq!(app.mode, Mode::ServicePicker);
5936    }
5937
5938    #[test]
5939    fn test_open_tab_picker_with_tabs() {
5940        let mut app = test_app();
5941        app.tabs = vec![
5942            Tab {
5943                service: Service::CloudWatchLogGroups,
5944                title: "CloudWatch › Log Groups".to_string(),
5945                breadcrumb: "CloudWatch › Log Groups".to_string(),
5946            },
5947            Tab {
5948                service: Service::CloudWatchInsights,
5949                title: "CloudWatch › Logs Insights".to_string(),
5950                breadcrumb: "CloudWatch › Logs Insights".to_string(),
5951            },
5952        ];
5953        app.current_tab = 1;
5954
5955        app.handle_action(Action::OpenTabPicker);
5956        assert_eq!(app.mode, Mode::TabPicker);
5957        assert_eq!(app.tab_picker_selected, 1);
5958    }
5959
5960    #[test]
5961    fn test_open_tab_picker_without_tabs() {
5962        let mut app = test_app();
5963        app.tabs = vec![];
5964
5965        app.handle_action(Action::OpenTabPicker);
5966        assert_eq!(app.mode, Mode::Normal);
5967    }
5968
5969    #[test]
5970    fn test_pending_key_state() {
5971        let mut app = test_app();
5972        assert_eq!(app.pending_key, None);
5973
5974        app.pending_key = Some('g');
5975        assert_eq!(app.pending_key, Some('g'));
5976    }
5977
5978    #[test]
5979    fn test_tab_breadcrumb_updates() {
5980        let mut app = test_app();
5981        app.tabs = vec![Tab {
5982            service: Service::CloudWatchLogGroups,
5983            title: "CloudWatch › Log Groups".to_string(),
5984            breadcrumb: "CloudWatch > Log groups".to_string(),
5985        }];
5986        app.current_tab = 0;
5987        app.service_selected = true;
5988        app.current_service = Service::CloudWatchLogGroups;
5989
5990        // Initial breadcrumb
5991        assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
5992
5993        // Add a log group and update breadcrumb
5994        app.log_groups_state
5995            .log_groups
5996            .items
5997            .push(rusticity_core::LogGroup {
5998                name: "/aws/lambda/test".to_string(),
5999                creation_time: None,
6000                stored_bytes: Some(1024),
6001                retention_days: None,
6002                log_class: None,
6003                arn: None,
6004                log_group_arn: None,
6005                deletion_protection_enabled: None,
6006            });
6007        app.log_groups_state.log_groups.reset();
6008        app.view_mode = ViewMode::Detail;
6009        app.update_current_tab_breadcrumb();
6010
6011        // Breadcrumb should now include log group
6012        assert_eq!(
6013            app.tabs[0].breadcrumb,
6014            "CloudWatch > Log groups > /aws/lambda/test"
6015        );
6016    }
6017
6018    #[test]
6019    fn test_s3_bucket_column_selector_navigation() {
6020        let mut app = test_app();
6021        app.current_service = Service::S3Buckets;
6022        app.mode = Mode::ColumnSelector;
6023        app.column_selector_index = 0;
6024
6025        // Should navigate through 3 S3 bucket columns (0, 1, 2)
6026        app.handle_action(Action::NextItem);
6027        assert_eq!(app.column_selector_index, 1);
6028
6029        app.handle_action(Action::NextItem);
6030        assert_eq!(app.column_selector_index, 2);
6031
6032        app.handle_action(Action::NextItem);
6033        assert_eq!(app.column_selector_index, 3);
6034
6035        // Should not go beyond max (now includes page size options: 3 columns + 6 = 9)
6036        for _ in 0..10 {
6037            app.handle_action(Action::NextItem);
6038        }
6039        assert_eq!(app.column_selector_index, 9);
6040
6041        // Navigate back
6042        app.handle_action(Action::PrevItem);
6043        assert_eq!(app.column_selector_index, 8);
6044
6045        app.handle_action(Action::PrevItem);
6046        assert_eq!(app.column_selector_index, 7);
6047
6048        // Should not go below 0
6049        for _ in 0..10 {
6050            app.handle_action(Action::PrevItem);
6051        }
6052        assert_eq!(app.column_selector_index, 0);
6053    }
6054
6055    #[test]
6056    fn test_cloudwatch_alarms_state_initialized() {
6057        let app = test_app();
6058
6059        // Alarms state should be initialized
6060        assert_eq!(app.alarms_state.table.items.len(), 0);
6061        assert_eq!(app.alarms_state.table.selected, 0);
6062        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
6063        assert!(!app.alarms_state.table.loading);
6064        assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
6065        assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
6066    }
6067
6068    #[test]
6069    fn test_cloudwatch_alarms_service_selection() {
6070        let mut app = test_app();
6071
6072        // Switch to alarms service
6073        app.current_service = Service::CloudWatchAlarms;
6074        app.service_selected = true;
6075
6076        assert_eq!(app.current_service, Service::CloudWatchAlarms);
6077        assert!(app.service_selected);
6078    }
6079
6080    #[test]
6081    fn test_cloudwatch_alarms_column_preferences() {
6082        let app = test_app();
6083
6084        // Should have alarm columns defined
6085        assert!(!app.cw_alarm_column_ids.is_empty());
6086        assert!(!app.cw_alarm_visible_column_ids.is_empty());
6087
6088        // Default visible columns
6089        assert!(app
6090            .cw_alarm_visible_column_ids
6091            .contains(&AlarmColumn::Name.id()));
6092        assert!(app
6093            .cw_alarm_visible_column_ids
6094            .contains(&AlarmColumn::State.id()));
6095    }
6096
6097    #[test]
6098    fn test_s3_bucket_navigation_without_expansion() {
6099        let mut app = test_app();
6100        app.current_service = Service::S3Buckets;
6101        app.service_selected = true;
6102        app.mode = Mode::Normal;
6103
6104        // Add 3 buckets
6105        app.s3_state.buckets.items = vec![
6106            S3Bucket {
6107                name: "bucket1".to_string(),
6108                region: "us-east-1".to_string(),
6109                creation_date: "2024-01-01T00:00:00Z".to_string(),
6110            },
6111            S3Bucket {
6112                name: "bucket2".to_string(),
6113                region: "us-east-1".to_string(),
6114                creation_date: "2024-01-02T00:00:00Z".to_string(),
6115            },
6116            S3Bucket {
6117                name: "bucket3".to_string(),
6118                region: "us-east-1".to_string(),
6119                creation_date: "2024-01-03T00:00:00Z".to_string(),
6120            },
6121        ];
6122        app.s3_state.selected_row = 0;
6123
6124        // Navigate down
6125        app.handle_action(Action::NextItem);
6126        assert_eq!(app.s3_state.selected_row, 1);
6127
6128        app.handle_action(Action::NextItem);
6129        assert_eq!(app.s3_state.selected_row, 2);
6130
6131        // Should not go beyond last bucket
6132        app.handle_action(Action::NextItem);
6133        assert_eq!(app.s3_state.selected_row, 2);
6134
6135        // Navigate up
6136        app.handle_action(Action::PrevItem);
6137        assert_eq!(app.s3_state.selected_row, 1);
6138
6139        app.handle_action(Action::PrevItem);
6140        assert_eq!(app.s3_state.selected_row, 0);
6141
6142        // Should not go below 0
6143        app.handle_action(Action::PrevItem);
6144        assert_eq!(app.s3_state.selected_row, 0);
6145    }
6146
6147    #[test]
6148    fn test_s3_bucket_navigation_with_expansion() {
6149        let mut app = test_app();
6150        app.current_service = Service::S3Buckets;
6151        app.service_selected = true;
6152        app.mode = Mode::Normal;
6153
6154        // Add 2 buckets
6155        app.s3_state.buckets.items = vec![
6156            S3Bucket {
6157                name: "bucket1".to_string(),
6158                region: "us-east-1".to_string(),
6159                creation_date: "2024-01-01T00:00:00Z".to_string(),
6160            },
6161            S3Bucket {
6162                name: "bucket2".to_string(),
6163                region: "us-east-1".to_string(),
6164                creation_date: "2024-01-02T00:00:00Z".to_string(),
6165            },
6166        ];
6167
6168        // Expand bucket1 with 2 objects
6169        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6170        app.s3_state.bucket_preview.insert(
6171            "bucket1".to_string(),
6172            vec![
6173                S3Object {
6174                    key: "file1.txt".to_string(),
6175                    size: 100,
6176                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6177                    is_prefix: false,
6178                    storage_class: "STANDARD".to_string(),
6179                },
6180                S3Object {
6181                    key: "folder/".to_string(),
6182                    size: 0,
6183                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6184                    is_prefix: true,
6185                    storage_class: String::new(),
6186                },
6187            ],
6188        );
6189
6190        app.s3_state.selected_row = 0;
6191
6192        // Total rows: bucket1 (row 0) + file1.txt (row 1) + folder/ (row 2) + bucket2 (row 3) = 4 rows
6193        // Navigate through all rows
6194        app.handle_action(Action::NextItem);
6195        assert_eq!(app.s3_state.selected_row, 1); // file1.txt
6196
6197        app.handle_action(Action::NextItem);
6198        assert_eq!(app.s3_state.selected_row, 2); // folder/
6199
6200        app.handle_action(Action::NextItem);
6201        assert_eq!(app.s3_state.selected_row, 3); // bucket2
6202
6203        // Should not go beyond last row
6204        app.handle_action(Action::NextItem);
6205        assert_eq!(app.s3_state.selected_row, 3);
6206    }
6207
6208    #[test]
6209    fn test_s3_bucket_navigation_with_nested_expansion() {
6210        let mut app = test_app();
6211        app.current_service = Service::S3Buckets;
6212        app.service_selected = true;
6213        app.mode = Mode::Normal;
6214
6215        // Add 1 bucket
6216        app.s3_state.buckets.items = vec![S3Bucket {
6217            name: "bucket1".to_string(),
6218            region: "us-east-1".to_string(),
6219            creation_date: "2024-01-01T00:00:00Z".to_string(),
6220        }];
6221
6222        // Expand bucket1 with a folder
6223        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6224        app.s3_state.bucket_preview.insert(
6225            "bucket1".to_string(),
6226            vec![S3Object {
6227                key: "folder/".to_string(),
6228                size: 0,
6229                last_modified: "2024-01-01T00:00:00Z".to_string(),
6230                is_prefix: true,
6231                storage_class: String::new(),
6232            }],
6233        );
6234
6235        // Expand the folder with 2 nested objects
6236        app.s3_state.expanded_prefixes.insert("folder/".to_string());
6237        app.s3_state.prefix_preview.insert(
6238            "folder/".to_string(),
6239            vec![
6240                S3Object {
6241                    key: "folder/file1.txt".to_string(),
6242                    size: 100,
6243                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6244                    is_prefix: false,
6245                    storage_class: "STANDARD".to_string(),
6246                },
6247                S3Object {
6248                    key: "folder/file2.txt".to_string(),
6249                    size: 200,
6250                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6251                    is_prefix: false,
6252                    storage_class: "STANDARD".to_string(),
6253                },
6254            ],
6255        );
6256
6257        app.s3_state.selected_row = 0;
6258
6259        // Total rows: bucket1 (0) + folder/ (1) + file1.txt (2) + file2.txt (3) = 4 rows
6260        app.handle_action(Action::NextItem);
6261        assert_eq!(app.s3_state.selected_row, 1); // folder/
6262
6263        app.handle_action(Action::NextItem);
6264        assert_eq!(app.s3_state.selected_row, 2); // folder/file1.txt
6265
6266        app.handle_action(Action::NextItem);
6267        assert_eq!(app.s3_state.selected_row, 3); // folder/file2.txt
6268
6269        // Should not go beyond last row
6270        app.handle_action(Action::NextItem);
6271        assert_eq!(app.s3_state.selected_row, 3);
6272    }
6273
6274    #[test]
6275    fn test_calculate_total_bucket_rows() {
6276        let mut app = test_app();
6277
6278        // No buckets
6279        assert_eq!(app.calculate_total_bucket_rows(), 0);
6280
6281        // 2 buckets, no expansion
6282        app.s3_state.buckets.items = vec![
6283            S3Bucket {
6284                name: "bucket1".to_string(),
6285                region: "us-east-1".to_string(),
6286                creation_date: "2024-01-01T00:00:00Z".to_string(),
6287            },
6288            S3Bucket {
6289                name: "bucket2".to_string(),
6290                region: "us-east-1".to_string(),
6291                creation_date: "2024-01-02T00:00:00Z".to_string(),
6292            },
6293        ];
6294        assert_eq!(app.calculate_total_bucket_rows(), 2);
6295
6296        // Expand bucket1 with 3 objects
6297        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6298        app.s3_state.bucket_preview.insert(
6299            "bucket1".to_string(),
6300            vec![
6301                S3Object {
6302                    key: "file1.txt".to_string(),
6303                    size: 100,
6304                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6305                    is_prefix: false,
6306                    storage_class: "STANDARD".to_string(),
6307                },
6308                S3Object {
6309                    key: "file2.txt".to_string(),
6310                    size: 200,
6311                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6312                    is_prefix: false,
6313                    storage_class: "STANDARD".to_string(),
6314                },
6315                S3Object {
6316                    key: "folder/".to_string(),
6317                    size: 0,
6318                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6319                    is_prefix: true,
6320                    storage_class: String::new(),
6321                },
6322            ],
6323        );
6324        assert_eq!(app.calculate_total_bucket_rows(), 5); // 2 buckets + 3 objects
6325
6326        // Expand folder/ with 2 nested objects
6327        app.s3_state.expanded_prefixes.insert("folder/".to_string());
6328        app.s3_state.prefix_preview.insert(
6329            "folder/".to_string(),
6330            vec![
6331                S3Object {
6332                    key: "folder/nested1.txt".to_string(),
6333                    size: 50,
6334                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6335                    is_prefix: false,
6336                    storage_class: "STANDARD".to_string(),
6337                },
6338                S3Object {
6339                    key: "folder/nested2.txt".to_string(),
6340                    size: 75,
6341                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6342                    is_prefix: false,
6343                    storage_class: "STANDARD".to_string(),
6344                },
6345            ],
6346        );
6347        assert_eq!(app.calculate_total_bucket_rows(), 7); // 2 buckets + 3 objects + 2 nested
6348    }
6349
6350    #[test]
6351    fn test_calculate_total_object_rows() {
6352        let mut app = test_app();
6353        app.s3_state.current_bucket = Some("test-bucket".to_string());
6354
6355        // No objects
6356        assert_eq!(app.calculate_total_object_rows(), 0);
6357
6358        // 2 objects, no expansion
6359        app.s3_state.objects = vec![
6360            S3Object {
6361                key: "file1.txt".to_string(),
6362                size: 100,
6363                last_modified: "2024-01-01T00:00:00Z".to_string(),
6364                is_prefix: false,
6365                storage_class: "STANDARD".to_string(),
6366            },
6367            S3Object {
6368                key: "folder/".to_string(),
6369                size: 0,
6370                last_modified: "2024-01-01T00:00:00Z".to_string(),
6371                is_prefix: true,
6372                storage_class: String::new(),
6373            },
6374        ];
6375        assert_eq!(app.calculate_total_object_rows(), 2);
6376
6377        // Expand folder/ with 2 items
6378        app.s3_state.expanded_prefixes.insert("folder/".to_string());
6379        app.s3_state.prefix_preview.insert(
6380            "folder/".to_string(),
6381            vec![
6382                S3Object {
6383                    key: "folder/file2.txt".to_string(),
6384                    size: 200,
6385                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6386                    is_prefix: false,
6387                    storage_class: "STANDARD".to_string(),
6388                },
6389                S3Object {
6390                    key: "folder/subfolder/".to_string(),
6391                    size: 0,
6392                    last_modified: "2024-01-01T00:00:00Z".to_string(),
6393                    is_prefix: true,
6394                    storage_class: String::new(),
6395                },
6396            ],
6397        );
6398        assert_eq!(app.calculate_total_object_rows(), 4); // 2 + 2 nested
6399
6400        // Expand subfolder/ with 1 item (3rd level)
6401        app.s3_state
6402            .expanded_prefixes
6403            .insert("folder/subfolder/".to_string());
6404        app.s3_state.prefix_preview.insert(
6405            "folder/subfolder/".to_string(),
6406            vec![S3Object {
6407                key: "folder/subfolder/deep.txt".to_string(),
6408                size: 50,
6409                last_modified: "2024-01-01T00:00:00Z".to_string(),
6410                is_prefix: false,
6411                storage_class: "STANDARD".to_string(),
6412            }],
6413        );
6414        assert_eq!(app.calculate_total_object_rows(), 5); // 2 + 2 nested + 1 deep
6415    }
6416
6417    #[test]
6418    fn test_s3_object_navigation_with_deep_nesting() {
6419        let mut app = test_app();
6420        app.current_service = Service::S3Buckets;
6421        app.service_selected = true;
6422        app.mode = Mode::Normal;
6423        app.s3_state.current_bucket = Some("test-bucket".to_string());
6424
6425        // Add folder structure: folder1/ -> folder2/ -> file.txt
6426        app.s3_state.objects = vec![S3Object {
6427            key: "folder1/".to_string(),
6428            size: 0,
6429            last_modified: "2024-01-01T00:00:00Z".to_string(),
6430            is_prefix: true,
6431            storage_class: String::new(),
6432        }];
6433
6434        // Expand folder1/
6435        app.s3_state
6436            .expanded_prefixes
6437            .insert("folder1/".to_string());
6438        app.s3_state.prefix_preview.insert(
6439            "folder1/".to_string(),
6440            vec![S3Object {
6441                key: "folder1/folder2/".to_string(),
6442                size: 0,
6443                last_modified: "2024-01-01T00:00:00Z".to_string(),
6444                is_prefix: true,
6445                storage_class: String::new(),
6446            }],
6447        );
6448
6449        // Expand folder2/
6450        app.s3_state
6451            .expanded_prefixes
6452            .insert("folder1/folder2/".to_string());
6453        app.s3_state.prefix_preview.insert(
6454            "folder1/folder2/".to_string(),
6455            vec![S3Object {
6456                key: "folder1/folder2/file.txt".to_string(),
6457                size: 100,
6458                last_modified: "2024-01-01T00:00:00Z".to_string(),
6459                is_prefix: false,
6460                storage_class: "STANDARD".to_string(),
6461            }],
6462        );
6463
6464        app.s3_state.selected_object = 0;
6465
6466        // Total: folder1/ (0) + folder2/ (1) + file.txt (2) = 3 rows
6467        app.handle_action(Action::NextItem);
6468        assert_eq!(app.s3_state.selected_object, 1); // folder2/
6469
6470        app.handle_action(Action::NextItem);
6471        assert_eq!(app.s3_state.selected_object, 2); // file.txt
6472
6473        // Should not go beyond
6474        app.handle_action(Action::NextItem);
6475        assert_eq!(app.s3_state.selected_object, 2);
6476    }
6477
6478    #[test]
6479    fn test_s3_expand_nested_folder_in_objects_view() {
6480        let mut app = test_app();
6481        app.current_service = Service::S3Buckets;
6482        app.service_selected = true;
6483        app.mode = Mode::Normal;
6484        app.s3_state.current_bucket = Some("test-bucket".to_string());
6485
6486        // Add parent folder
6487        app.s3_state.objects = vec![S3Object {
6488            key: "parent/".to_string(),
6489            size: 0,
6490            last_modified: "2024-01-01T00:00:00Z".to_string(),
6491            is_prefix: true,
6492            storage_class: String::new(),
6493        }];
6494
6495        // Expand parent
6496        app.s3_state.expanded_prefixes.insert("parent/".to_string());
6497        app.s3_state.prefix_preview.insert(
6498            "parent/".to_string(),
6499            vec![S3Object {
6500                key: "parent/child/".to_string(),
6501                size: 0,
6502                last_modified: "2024-01-01T00:00:00Z".to_string(),
6503                is_prefix: true,
6504                storage_class: String::new(),
6505            }],
6506        );
6507
6508        // Select the nested folder (index 1)
6509        app.s3_state.selected_object = 1;
6510
6511        // Expand it (simulate pressing Enter/Right)
6512        app.handle_action(Action::NextPane);
6513
6514        // Should be expanded now
6515        assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
6516        assert!(app.s3_state.buckets.loading); // Should trigger load
6517    }
6518
6519    #[test]
6520    fn test_s3_drill_into_nested_folder() {
6521        let mut app = test_app();
6522        app.current_service = Service::S3Buckets;
6523        app.service_selected = true;
6524        app.mode = Mode::Normal;
6525        app.s3_state.current_bucket = Some("test-bucket".to_string());
6526
6527        // Add parent folder
6528        app.s3_state.objects = vec![S3Object {
6529            key: "parent/".to_string(),
6530            size: 0,
6531            last_modified: "2024-01-01T00:00:00Z".to_string(),
6532            is_prefix: true,
6533            storage_class: String::new(),
6534        }];
6535
6536        // Expand parent
6537        app.s3_state.expanded_prefixes.insert("parent/".to_string());
6538        app.s3_state.prefix_preview.insert(
6539            "parent/".to_string(),
6540            vec![S3Object {
6541                key: "parent/child/".to_string(),
6542                size: 0,
6543                last_modified: "2024-01-01T00:00:00Z".to_string(),
6544                is_prefix: true,
6545                storage_class: String::new(),
6546            }],
6547        );
6548
6549        // Select the nested folder (index 1)
6550        app.s3_state.selected_object = 1;
6551
6552        // Drill into it (simulate pressing Enter)
6553        app.handle_action(Action::Select);
6554
6555        // Should navigate into the folder
6556        assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
6557        assert!(app.s3_state.buckets.loading); // Should trigger load
6558    }
6559
6560    #[test]
6561    fn test_s3_esc_pops_navigation_stack() {
6562        let mut app = test_app();
6563        app.current_service = Service::S3Buckets;
6564        app.s3_state.current_bucket = Some("test-bucket".to_string());
6565        app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
6566
6567        // Press Esc - should pop from stack
6568        app.handle_action(Action::GoBack);
6569        assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
6570        assert!(app.s3_state.buckets.loading);
6571
6572        // Press Esc again - should pop to bucket root
6573        app.s3_state.buckets.loading = false;
6574        app.handle_action(Action::GoBack);
6575        assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
6576        assert!(app.s3_state.buckets.loading);
6577
6578        // Press Esc again - should exit bucket
6579        app.s3_state.buckets.loading = false;
6580        app.handle_action(Action::GoBack);
6581        assert_eq!(app.s3_state.current_bucket, None);
6582    }
6583
6584    #[test]
6585    fn test_s3_esc_from_bucket_root_exits() {
6586        let mut app = test_app();
6587        app.current_service = Service::S3Buckets;
6588        app.s3_state.current_bucket = Some("test-bucket".to_string());
6589        app.s3_state.prefix_stack = vec![];
6590
6591        // Press Esc from bucket root - should exit bucket
6592        app.handle_action(Action::GoBack);
6593        assert_eq!(app.s3_state.current_bucket, None);
6594        assert_eq!(app.s3_state.objects.len(), 0);
6595    }
6596
6597    #[test]
6598    fn test_s3_drill_into_nested_prefix_from_bucket_list() {
6599        let mut app = test_app();
6600        app.current_service = Service::S3Buckets;
6601        app.service_selected = true;
6602        app.mode = Mode::Normal;
6603
6604        // Setup bucket with nested preview
6605        app.s3_state.buckets.items = vec![S3Bucket {
6606            name: "test-bucket".to_string(),
6607            region: "us-east-1".to_string(),
6608            creation_date: "2024-01-01".to_string(),
6609        }];
6610
6611        // Expand bucket to show first-level prefix
6612        app.s3_state
6613            .expanded_prefixes
6614            .insert("test-bucket".to_string());
6615        app.s3_state.bucket_preview.insert(
6616            "test-bucket".to_string(),
6617            vec![S3Object {
6618                key: "parent/".to_string(),
6619                size: 0,
6620                last_modified: "2024-01-01".to_string(),
6621                is_prefix: true,
6622                storage_class: String::new(),
6623            }],
6624        );
6625
6626        // Expand parent to show nested prefix
6627        app.s3_state.expanded_prefixes.insert("parent/".to_string());
6628        app.s3_state.prefix_preview.insert(
6629            "parent/".to_string(),
6630            vec![S3Object {
6631                key: "parent/child/".to_string(),
6632                size: 0,
6633                last_modified: "2024-01-01".to_string(),
6634                is_prefix: true,
6635                storage_class: String::new(),
6636            }],
6637        );
6638
6639        // Select nested prefix (row 2: bucket, parent, nested)
6640        app.s3_state.selected_row = 2;
6641
6642        // Drill into nested prefix
6643        app.handle_action(Action::Select);
6644
6645        // Should have both parent and child in stack
6646        assert_eq!(
6647            app.s3_state.prefix_stack,
6648            vec!["parent/".to_string(), "parent/child/".to_string()]
6649        );
6650        assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
6651        assert!(app.s3_state.buckets.loading);
6652
6653        // Now press Esc - should go back to parent
6654        app.s3_state.buckets.loading = false;
6655        app.handle_action(Action::GoBack);
6656        assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
6657        assert!(app.s3_state.buckets.loading);
6658
6659        // Press Esc again - should go to bucket root
6660        app.s3_state.buckets.loading = false;
6661        app.handle_action(Action::GoBack);
6662        assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
6663        assert!(app.s3_state.buckets.loading);
6664
6665        // Press Esc again - should exit bucket
6666        app.s3_state.buckets.loading = false;
6667        app.handle_action(Action::GoBack);
6668        assert_eq!(app.s3_state.current_bucket, None);
6669    }
6670
6671    #[test]
6672    fn test_region_picker_fuzzy_filter() {
6673        let mut app = test_app();
6674        app.region_latencies.insert("us-east-1".to_string(), 10);
6675        app.region_filter = "vir".to_string();
6676        let filtered = app.get_filtered_regions();
6677        assert!(filtered.iter().any(|r| r.code == "us-east-1"));
6678    }
6679
6680    #[test]
6681    fn test_profile_picker_loads_profiles() {
6682        let profiles = App::load_aws_profiles();
6683        // Should at least have default profile or be empty if no config
6684        assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
6685    }
6686
6687    #[test]
6688    fn test_profile_with_region_uses_it() {
6689        let mut app = test_app_no_region();
6690        app.available_profiles = vec![AwsProfile {
6691            name: "test-profile".to_string(),
6692            region: Some("eu-west-1".to_string()),
6693            account: Some("123456789".to_string()),
6694            role_arn: None,
6695            source_profile: None,
6696        }];
6697        app.profile_picker_selected = 0;
6698        app.mode = Mode::ProfilePicker;
6699
6700        // Simulate selecting the profile
6701        let filtered = app.get_filtered_profiles();
6702        if let Some(profile) = filtered.first() {
6703            let profile_name = profile.name.clone();
6704            let profile_region = profile.region.clone();
6705
6706            app.profile = profile_name;
6707            if let Some(region) = profile_region {
6708                app.region = region;
6709            }
6710        }
6711
6712        assert_eq!(app.profile, "test-profile");
6713        assert_eq!(app.region, "eu-west-1");
6714    }
6715
6716    #[test]
6717    fn test_profile_without_region_keeps_unknown() {
6718        let mut app = test_app_no_region();
6719        let initial_region = app.region.clone();
6720
6721        app.available_profiles = vec![AwsProfile {
6722            name: "test-profile".to_string(),
6723            region: None,
6724            account: None,
6725            role_arn: None,
6726            source_profile: None,
6727        }];
6728        app.profile_picker_selected = 0;
6729        app.mode = Mode::ProfilePicker;
6730
6731        let filtered = app.get_filtered_profiles();
6732        if let Some(profile) = filtered.first() {
6733            let profile_name = profile.name.clone();
6734            let profile_region = profile.region.clone();
6735
6736            app.profile = profile_name;
6737            if let Some(region) = profile_region {
6738                app.region = region;
6739            }
6740        }
6741
6742        assert_eq!(app.profile, "test-profile");
6743        assert_eq!(app.region, initial_region); // Should keep initial region
6744    }
6745
6746    #[test]
6747    fn test_region_selection_closes_all_tabs() {
6748        let mut app = test_app();
6749
6750        // Add some tabs
6751        app.tabs.push(Tab {
6752            service: Service::CloudWatchLogGroups,
6753            title: "CloudWatch".to_string(),
6754            breadcrumb: "CloudWatch".to_string(),
6755        });
6756        app.tabs.push(Tab {
6757            service: Service::S3Buckets,
6758            title: "S3".to_string(),
6759            breadcrumb: "S3".to_string(),
6760        });
6761        app.service_selected = true;
6762        app.current_tab = 1;
6763
6764        // Add latency for region
6765        app.region_latencies.insert("eu-west-1".to_string(), 50);
6766
6767        // Simulate selecting a different region
6768        app.mode = Mode::RegionPicker;
6769        app.region_picker_selected = 0;
6770
6771        let filtered = app.get_filtered_regions();
6772        if let Some(region) = filtered.first() {
6773            app.region = region.code.to_string();
6774            app.tabs.clear();
6775            app.current_tab = 0;
6776            app.service_selected = false;
6777            app.mode = Mode::Normal;
6778        }
6779
6780        assert_eq!(app.tabs.len(), 0);
6781        assert_eq!(app.current_tab, 0);
6782        assert!(!app.service_selected);
6783        assert_eq!(app.region, "eu-west-1");
6784    }
6785
6786    #[test]
6787    fn test_region_picker_can_be_closed_without_selection() {
6788        let mut app = test_app();
6789        let initial_region = app.region.clone();
6790
6791        app.mode = Mode::RegionPicker;
6792
6793        // Close without selecting (Esc)
6794        app.mode = Mode::Normal;
6795
6796        // Region should not change
6797        assert_eq!(app.region, initial_region);
6798    }
6799
6800    #[test]
6801    fn test_session_filter_works() {
6802        let mut app = test_app();
6803
6804        app.sessions = vec![
6805            Session {
6806                id: "1".to_string(),
6807                timestamp: "2024-01-01".to_string(),
6808                profile: "prod-profile".to_string(),
6809                region: "us-east-1".to_string(),
6810                account_id: "123456789".to_string(),
6811                role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
6812                tabs: vec![],
6813            },
6814            Session {
6815                id: "2".to_string(),
6816                timestamp: "2024-01-02".to_string(),
6817                profile: "dev-profile".to_string(),
6818                region: "eu-west-1".to_string(),
6819                account_id: "987654321".to_string(),
6820                role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
6821                tabs: vec![],
6822            },
6823        ];
6824
6825        // Filter by profile
6826        app.session_filter = "prod".to_string();
6827        let filtered = app.get_filtered_sessions();
6828        assert_eq!(filtered.len(), 1);
6829        assert_eq!(filtered[0].profile, "prod-profile");
6830
6831        // Filter by region
6832        app.session_filter = "eu".to_string();
6833        let filtered = app.get_filtered_sessions();
6834        assert_eq!(filtered.len(), 1);
6835        assert_eq!(filtered[0].region, "eu-west-1");
6836
6837        // No filter
6838        app.session_filter.clear();
6839        let filtered = app.get_filtered_sessions();
6840        assert_eq!(filtered.len(), 2);
6841    }
6842
6843    #[test]
6844    fn test_profile_picker_shows_account() {
6845        let mut app = test_app_no_region();
6846        app.available_profiles = vec![AwsProfile {
6847            name: "test-profile".to_string(),
6848            region: Some("us-east-1".to_string()),
6849            account: Some("123456789".to_string()),
6850            role_arn: None,
6851            source_profile: None,
6852        }];
6853
6854        let filtered = app.get_filtered_profiles();
6855        assert_eq!(filtered.len(), 1);
6856        assert_eq!(filtered[0].account, Some("123456789".to_string()));
6857    }
6858
6859    #[test]
6860    fn test_profile_without_account() {
6861        let mut app = test_app_no_region();
6862        app.available_profiles = vec![AwsProfile {
6863            name: "test-profile".to_string(),
6864            region: Some("us-east-1".to_string()),
6865            account: None,
6866            role_arn: None,
6867            source_profile: None,
6868        }];
6869
6870        let filtered = app.get_filtered_profiles();
6871        assert_eq!(filtered.len(), 1);
6872        assert_eq!(filtered[0].account, None);
6873    }
6874
6875    #[test]
6876    fn test_profile_with_all_fields() {
6877        let mut app = test_app_no_region();
6878        app.available_profiles = vec![AwsProfile {
6879            name: "prod-profile".to_string(),
6880            region: Some("us-west-2".to_string()),
6881            account: Some("123456789".to_string()),
6882            role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
6883            source_profile: Some("base-profile".to_string()),
6884        }];
6885
6886        let filtered = app.get_filtered_profiles();
6887        assert_eq!(filtered.len(), 1);
6888        assert_eq!(filtered[0].name, "prod-profile");
6889        assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
6890        assert_eq!(filtered[0].account, Some("123456789".to_string()));
6891        assert_eq!(
6892            filtered[0].role_arn,
6893            Some("arn:aws:iam::123456789:role/AdminRole".to_string())
6894        );
6895        assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
6896    }
6897
6898    #[test]
6899    fn test_profile_filter_by_source_profile() {
6900        let mut app = test_app_no_region();
6901        app.available_profiles = vec![
6902            AwsProfile {
6903                name: "profile1".to_string(),
6904                region: None,
6905                account: None,
6906                role_arn: None,
6907                source_profile: Some("base".to_string()),
6908            },
6909            AwsProfile {
6910                name: "profile2".to_string(),
6911                region: None,
6912                account: None,
6913                role_arn: None,
6914                source_profile: Some("other".to_string()),
6915            },
6916        ];
6917
6918        app.profile_filter = "base".to_string();
6919        let filtered = app.get_filtered_profiles();
6920        assert_eq!(filtered.len(), 1);
6921        assert_eq!(filtered[0].name, "profile1");
6922    }
6923
6924    #[test]
6925    fn test_profile_filter_by_role() {
6926        let mut app = test_app_no_region();
6927        app.available_profiles = vec![
6928            AwsProfile {
6929                name: "admin-profile".to_string(),
6930                region: None,
6931                account: None,
6932                role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
6933                source_profile: None,
6934            },
6935            AwsProfile {
6936                name: "dev-profile".to_string(),
6937                region: None,
6938                account: None,
6939                role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
6940                source_profile: None,
6941            },
6942        ];
6943
6944        app.profile_filter = "Admin".to_string();
6945        let filtered = app.get_filtered_profiles();
6946        assert_eq!(filtered.len(), 1);
6947        assert_eq!(filtered[0].name, "admin-profile");
6948    }
6949
6950    #[test]
6951    fn test_profiles_sorted_by_name() {
6952        let mut app = test_app_no_region();
6953        app.available_profiles = vec![
6954            AwsProfile {
6955                name: "zebra-profile".to_string(),
6956                region: None,
6957                account: None,
6958                role_arn: None,
6959                source_profile: None,
6960            },
6961            AwsProfile {
6962                name: "alpha-profile".to_string(),
6963                region: None,
6964                account: None,
6965                role_arn: None,
6966                source_profile: None,
6967            },
6968            AwsProfile {
6969                name: "beta-profile".to_string(),
6970                region: None,
6971                account: None,
6972                role_arn: None,
6973                source_profile: None,
6974            },
6975        ];
6976
6977        let filtered = app.get_filtered_profiles();
6978        assert_eq!(filtered.len(), 3);
6979        assert_eq!(filtered[0].name, "alpha-profile");
6980        assert_eq!(filtered[1].name, "beta-profile");
6981        assert_eq!(filtered[2].name, "zebra-profile");
6982    }
6983
6984    #[test]
6985    fn test_profile_with_role_arn() {
6986        let mut app = test_app_no_region();
6987        app.available_profiles = vec![AwsProfile {
6988            name: "role-profile".to_string(),
6989            region: Some("us-east-1".to_string()),
6990            account: Some("123456789".to_string()),
6991            role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
6992            source_profile: None,
6993        }];
6994
6995        let filtered = app.get_filtered_profiles();
6996        assert_eq!(filtered.len(), 1);
6997        assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
6998    }
6999
7000    #[test]
7001    fn test_profile_with_user_arn() {
7002        let mut app = test_app_no_region();
7003        app.available_profiles = vec![AwsProfile {
7004            name: "user-profile".to_string(),
7005            region: Some("us-east-1".to_string()),
7006            account: Some("123456789".to_string()),
7007            role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
7008            source_profile: None,
7009        }];
7010
7011        let filtered = app.get_filtered_profiles();
7012        assert_eq!(filtered.len(), 1);
7013        assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
7014    }
7015
7016    #[test]
7017    fn test_filtered_profiles_also_sorted() {
7018        let mut app = test_app_no_region();
7019        app.available_profiles = vec![
7020            AwsProfile {
7021                name: "prod-zebra".to_string(),
7022                region: Some("us-east-1".to_string()),
7023                account: None,
7024                role_arn: None,
7025                source_profile: None,
7026            },
7027            AwsProfile {
7028                name: "prod-alpha".to_string(),
7029                region: Some("us-east-1".to_string()),
7030                account: None,
7031                role_arn: None,
7032                source_profile: None,
7033            },
7034            AwsProfile {
7035                name: "dev-profile".to_string(),
7036                region: Some("us-west-2".to_string()),
7037                account: None,
7038                role_arn: None,
7039                source_profile: None,
7040            },
7041        ];
7042
7043        app.profile_filter = "prod".to_string();
7044        let filtered = app.get_filtered_profiles();
7045        assert_eq!(filtered.len(), 2);
7046        assert_eq!(filtered[0].name, "prod-alpha");
7047        assert_eq!(filtered[1].name, "prod-zebra");
7048    }
7049
7050    #[test]
7051    fn test_profile_picker_has_all_columns() {
7052        let mut app = test_app_no_region();
7053        app.available_profiles = vec![AwsProfile {
7054            name: "test".to_string(),
7055            region: Some("us-east-1".to_string()),
7056            account: Some("123456789".to_string()),
7057            role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
7058            source_profile: Some("base".to_string()),
7059        }];
7060
7061        let filtered = app.get_filtered_profiles();
7062        assert_eq!(filtered.len(), 1);
7063        assert!(filtered[0].name == "test");
7064        assert!(filtered[0].region.is_some());
7065        assert!(filtered[0].account.is_some());
7066        assert!(filtered[0].role_arn.is_some());
7067        assert!(filtered[0].source_profile.is_some());
7068    }
7069
7070    #[test]
7071    fn test_session_picker_shows_tab_count() {
7072        let mut app = test_app_no_region();
7073        app.sessions = vec![Session {
7074            id: "1".to_string(),
7075            timestamp: "2024-01-01".to_string(),
7076            profile: "test".to_string(),
7077            region: "us-east-1".to_string(),
7078            account_id: "123".to_string(),
7079            role_arn: String::new(),
7080            tabs: vec![
7081                SessionTab {
7082                    service: "CloudWatch".to_string(),
7083                    title: "Logs".to_string(),
7084                    breadcrumb: String::new(),
7085                    filter: None,
7086                    selected_item: None,
7087                },
7088                SessionTab {
7089                    service: "S3".to_string(),
7090                    title: "Buckets".to_string(),
7091                    breadcrumb: String::new(),
7092                    filter: None,
7093                    selected_item: None,
7094                },
7095            ],
7096        }];
7097
7098        let filtered = app.get_filtered_sessions();
7099        assert_eq!(filtered.len(), 1);
7100        assert_eq!(filtered[0].tabs.len(), 2);
7101    }
7102
7103    #[test]
7104    fn test_start_background_data_fetch_loads_profiles() {
7105        let mut app = test_app_no_region();
7106        assert!(app.available_profiles.is_empty());
7107
7108        // Load profiles synchronously
7109        app.available_profiles = App::load_aws_profiles();
7110
7111        // Profiles should be loaded
7112        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7113    }
7114
7115    #[test]
7116    fn test_refresh_in_profile_picker() {
7117        let mut app = test_app_no_region();
7118        app.mode = Mode::ProfilePicker;
7119        app.available_profiles = vec![AwsProfile {
7120            name: "test".to_string(),
7121            region: None,
7122            account: None,
7123            role_arn: None,
7124            source_profile: None,
7125        }];
7126
7127        app.handle_action(Action::Refresh);
7128
7129        // Should set loading state — no Refreshing... message (removed: it leaked into other services)
7130        assert!(app.log_groups_state.loading);
7131        assert!(
7132            app.log_groups_state.loading_message.is_empty()
7133                || !app.log_groups_state.loading_message.contains("Refreshing"),
7134            "loading_message must not say 'Refreshing...' as it leaks into other services"
7135        );
7136    }
7137
7138    #[test]
7139    fn test_refresh_sets_loading_for_profile_picker() {
7140        let mut app = test_app_no_region();
7141        app.mode = Mode::ProfilePicker;
7142
7143        assert!(!app.log_groups_state.loading);
7144
7145        app.handle_action(Action::Refresh);
7146
7147        assert!(app.log_groups_state.loading);
7148    }
7149
7150    #[test]
7151    fn test_profiles_loaded_on_demand() {
7152        let mut app = test_app_no_region();
7153
7154        // Profiles not loaded by default
7155        assert!(app.available_profiles.is_empty());
7156
7157        // Load on demand
7158        app.available_profiles = App::load_aws_profiles();
7159
7160        // Now loaded
7161        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7162    }
7163
7164    #[test]
7165    fn test_profile_accounts_not_fetched_automatically() {
7166        let mut app = test_app_no_region();
7167        app.available_profiles = App::load_aws_profiles();
7168
7169        // Accounts should not be populated automatically
7170        for profile in &app.available_profiles {
7171            // Account may or may not be set depending on what's in config
7172            // But we're not fetching them automatically
7173            assert!(profile.account.is_none() || profile.account.is_some());
7174        }
7175    }
7176
7177    #[test]
7178    fn test_ctrl_r_triggers_account_fetch() {
7179        let mut app = test_app_no_region();
7180        app.mode = Mode::ProfilePicker;
7181        app.available_profiles = vec![AwsProfile {
7182            name: "test".to_string(),
7183            region: Some("us-east-1".to_string()),
7184            account: None,
7185            role_arn: None,
7186            source_profile: None,
7187        }];
7188
7189        // Before refresh, account is None
7190        assert!(app.available_profiles[0].account.is_none());
7191
7192        // Trigger refresh
7193        app.handle_action(Action::Refresh);
7194
7195        // Should set loading state (actual fetch happens in main.rs event loop)
7196        assert!(app.log_groups_state.loading);
7197    }
7198
7199    #[test]
7200    fn test_refresh_in_region_picker() {
7201        let mut app = test_app_no_region();
7202        app.mode = Mode::RegionPicker;
7203
7204        let initial_latencies = app.region_latencies.len();
7205        app.handle_action(Action::Refresh);
7206
7207        // Latencies should be cleared and remeasured
7208        assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
7209    }
7210
7211    #[test]
7212    fn test_refresh_in_session_picker() {
7213        let mut app = test_app_no_region();
7214        app.mode = Mode::SessionPicker;
7215        app.sessions = vec![];
7216
7217        app.handle_action(Action::Refresh);
7218
7219        // Sessions should be reloaded (may be empty if no saved sessions)
7220        assert!(app.sessions.is_empty() || !app.sessions.is_empty());
7221    }
7222
7223    #[test]
7224    fn test_session_picker_selection() {
7225        let mut app = test_app();
7226
7227        app.sessions = vec![Session {
7228            id: "1".to_string(),
7229            timestamp: "2024-01-01".to_string(),
7230            profile: "prod-profile".to_string(),
7231            region: "us-west-2".to_string(),
7232            account_id: "123456789".to_string(),
7233            role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
7234            tabs: vec![SessionTab {
7235                service: "CloudWatchLogGroups".to_string(),
7236                title: "Log Groups".to_string(),
7237                breadcrumb: "CloudWatch › Log Groups".to_string(),
7238                filter: Some("test".to_string()),
7239                selected_item: None,
7240            }],
7241        }];
7242
7243        app.mode = Mode::SessionPicker;
7244        app.session_picker_selected = 0;
7245
7246        // Simulate selecting the session
7247        app.handle_action(Action::Select);
7248
7249        assert_eq!(app.mode, Mode::Normal);
7250        assert_eq!(app.profile, "prod-profile");
7251        assert_eq!(app.region, "us-west-2");
7252        assert_eq!(app.config.account_id, "123456789");
7253        assert_eq!(app.tabs.len(), 1);
7254        assert_eq!(app.tabs[0].title, "Log Groups");
7255    }
7256
7257    #[test]
7258    fn test_save_session_creates_session() {
7259        let mut app =
7260            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7261        app.config.account_id = "123456789".to_string();
7262        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7263
7264        app.tabs.push(Tab {
7265            service: Service::CloudWatchLogGroups,
7266            title: "Log Groups".to_string(),
7267            breadcrumb: "CloudWatch › Log Groups".to_string(),
7268        });
7269
7270        app.save_current_session();
7271
7272        assert!(app.current_session.is_some());
7273        let session = app.current_session.clone().unwrap();
7274        assert_eq!(session.profile, "test-profile");
7275        assert_eq!(session.region, "us-east-1");
7276        assert_eq!(session.account_id, "123456789");
7277        assert_eq!(session.tabs.len(), 1);
7278
7279        // Cleanup
7280        let _ = session.delete();
7281    }
7282
7283    #[test]
7284    fn test_save_session_updates_existing() {
7285        let mut app =
7286            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7287        app.config.account_id = "123456789".to_string();
7288        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7289
7290        app.current_session = Some(Session {
7291            id: "existing".to_string(),
7292            timestamp: "2024-01-01".to_string(),
7293            profile: "test-profile".to_string(),
7294            region: "us-east-1".to_string(),
7295            account_id: "123456789".to_string(),
7296            role_arn: "arn:aws:iam::123456789:role/test".to_string(),
7297            tabs: vec![],
7298        });
7299
7300        app.tabs.push(Tab {
7301            service: Service::CloudWatchLogGroups,
7302            title: "Log Groups".to_string(),
7303            breadcrumb: "CloudWatch › Log Groups".to_string(),
7304        });
7305
7306        app.save_current_session();
7307
7308        let session = app.current_session.clone().unwrap();
7309        assert_eq!(session.id, "existing");
7310        assert_eq!(session.tabs.len(), 1);
7311
7312        // Cleanup
7313        let _ = session.delete();
7314    }
7315
7316    #[test]
7317    fn test_save_session_skips_empty_tabs() {
7318        let mut app =
7319            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7320        app.config.account_id = "123456789".to_string();
7321
7322        app.save_current_session();
7323
7324        assert!(app.current_session.is_none());
7325    }
7326
7327    #[test]
7328    fn test_save_session_deletes_when_tabs_closed() {
7329        let mut app =
7330            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7331        app.config.account_id = "123456789".to_string();
7332        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7333
7334        // Create a session with tabs
7335        app.current_session = Some(Session {
7336            id: "test_delete".to_string(),
7337            timestamp: "2024-01-01 10:00:00 UTC".to_string(),
7338            profile: "test-profile".to_string(),
7339            region: "us-east-1".to_string(),
7340            account_id: "123456789".to_string(),
7341            role_arn: "arn:aws:iam::123456789:role/test".to_string(),
7342            tabs: vec![],
7343        });
7344
7345        // Save with no tabs should delete session
7346        app.save_current_session();
7347
7348        assert!(app.current_session.is_none());
7349    }
7350
7351    #[test]
7352    fn test_closing_all_tabs_deletes_session() {
7353        let mut app =
7354            App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7355        app.config.account_id = "123456789".to_string();
7356        app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7357
7358        // Add a tab
7359        app.tabs.push(Tab {
7360            service: Service::CloudWatchLogGroups,
7361            title: "Log Groups".to_string(),
7362            breadcrumb: "CloudWatch › Log Groups".to_string(),
7363        });
7364
7365        // Create session
7366        app.save_current_session();
7367        assert!(app.current_session.is_some());
7368        let session_id = app.current_session.as_ref().unwrap().id.clone();
7369
7370        // Close all tabs
7371        app.tabs.clear();
7372
7373        // Save should delete session
7374        app.save_current_session();
7375        assert!(app.current_session.is_none());
7376
7377        // Cleanup - ensure session file is deleted
7378        let _ = Session::load(&session_id).map(|s| s.delete());
7379    }
7380
7381    #[test]
7382    fn test_credential_error_opens_profile_picker() {
7383        // Simulate what main.rs does on credential error
7384        let mut app = App::new_without_client("default".to_string(), None);
7385        let error_str = "Unable to load credentials from any source";
7386
7387        if error_str.contains("credentials") {
7388            app.available_profiles = App::load_aws_profiles();
7389            app.mode = Mode::ProfilePicker;
7390        }
7391
7392        assert_eq!(app.mode, Mode::ProfilePicker);
7393        // Should have loaded profiles
7394        assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7395    }
7396
7397    #[test]
7398    fn test_non_credential_error_shows_error_modal() {
7399        let mut app = App::new_without_client("default".to_string(), None);
7400        let error_str = "Network timeout";
7401
7402        if !error_str.contains("credentials") {
7403            app.error_message = Some(error_str.to_string());
7404            app.mode = Mode::ErrorModal;
7405        }
7406
7407        assert_eq!(app.mode, Mode::ErrorModal);
7408        assert!(app.error_message.is_some());
7409    }
7410
7411    #[tokio::test]
7412    async fn test_profile_selection_loads_credentials() {
7413        // Set a valid AWS profile if available
7414        std::env::set_var("AWS_PROFILE", "default");
7415
7416        // Try to create app with profile
7417        let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
7418
7419        if let Ok(app) = result {
7420            // If credentials are available, verify they're loaded
7421            assert!(!app.config.account_id.is_empty());
7422            assert!(!app.config.role_arn.is_empty());
7423            assert_eq!(app.profile, "default");
7424            assert_eq!(app.config.region, "us-east-1");
7425        }
7426        // If no credentials, test passes (can't test without real AWS creds)
7427    }
7428
7429    #[test]
7430    fn test_new_app_shows_service_picker_with_no_tabs() {
7431        let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
7432
7433        // Should start with no service selected
7434        assert!(!app.service_selected);
7435        // Should be in ServicePicker mode (service picker)
7436        assert_eq!(app.mode, Mode::ServicePicker);
7437        // Should have no tabs
7438        assert!(app.tabs.is_empty());
7439    }
7440
7441    #[tokio::test]
7442    async fn test_aws_profile_env_var_read_before_config_load() {
7443        // This test verifies the bug: AWS_PROFILE should be read and used
7444        std::env::set_var("AWS_PROFILE", "test-profile");
7445
7446        // Simulate what happens in App::new
7447        let profile_name = None
7448            .or_else(|| std::env::var("AWS_PROFILE").ok())
7449            .unwrap_or_else(|| "default".to_string());
7450
7451        // Should have read test-profile from env
7452        assert_eq!(profile_name, "test-profile");
7453
7454        // Now set it (redundant but that's what the code does)
7455        std::env::set_var("AWS_PROFILE", &profile_name);
7456
7457        // Verify it's still set
7458        assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
7459
7460        std::env::remove_var("AWS_PROFILE");
7461    }
7462
7463    #[test]
7464    fn test_next_preferences_cloudformation() {
7465        let mut app = test_app();
7466        app.current_service = Service::CloudFormationStacks;
7467        app.mode = Mode::ColumnSelector;
7468        app.column_selector_index = 0;
7469
7470        // Should jump to PageSize section
7471        let page_size_idx = app.cfn_column_ids.len() + 2;
7472        app.handle_action(Action::NextPreferences);
7473        assert_eq!(app.column_selector_index, page_size_idx);
7474
7475        // Should wrap back to Columns
7476        app.handle_action(Action::NextPreferences);
7477        assert_eq!(app.column_selector_index, 0);
7478    }
7479
7480    #[test]
7481    fn test_s3_preferences_tab_cycling() {
7482        let mut app = test_app();
7483        app.current_service = Service::S3Buckets;
7484        app.mode = Mode::ColumnSelector;
7485        app.column_selector_index = 0;
7486
7487        let page_size_idx = app.s3_bucket_column_ids.len() + 2;
7488
7489        // Tab should jump to PageSize
7490        app.handle_action(Action::NextPreferences);
7491        assert_eq!(app.column_selector_index, page_size_idx);
7492
7493        // Tab should wrap back to Columns
7494        app.handle_action(Action::NextPreferences);
7495        assert_eq!(app.column_selector_index, 0);
7496
7497        // Shift+Tab should jump to PageSize
7498        app.handle_action(Action::PrevPreferences);
7499        assert_eq!(app.column_selector_index, page_size_idx);
7500
7501        // Shift+Tab should wrap back to Columns
7502        app.handle_action(Action::PrevPreferences);
7503        assert_eq!(app.column_selector_index, 0);
7504    }
7505
7506    #[test]
7507    fn test_s3_filter_resets_selection() {
7508        let mut app = test_app();
7509        app.current_service = Service::S3Buckets;
7510        app.service_selected = true;
7511
7512        // Add some buckets
7513        app.s3_state.buckets.items = vec![
7514            S3Bucket {
7515                name: "bucket-1".to_string(),
7516                region: "us-east-1".to_string(),
7517                creation_date: "2023-01-01".to_string(),
7518            },
7519            S3Bucket {
7520                name: "bucket-2".to_string(),
7521                region: "us-east-1".to_string(),
7522                creation_date: "2023-01-02".to_string(),
7523            },
7524            S3Bucket {
7525                name: "other-bucket".to_string(),
7526                region: "us-east-1".to_string(),
7527                creation_date: "2023-01-03".to_string(),
7528            },
7529        ];
7530
7531        // Navigate to second bucket
7532        app.s3_state.selected_row = 1;
7533        app.s3_state.bucket_scroll_offset = 1;
7534
7535        // Apply filter
7536        app.mode = Mode::FilterInput;
7537        app.apply_filter_operation(|f| f.push_str("bucket-"));
7538
7539        // Selection should be reset
7540        assert_eq!(app.s3_state.selected_row, 0);
7541        assert_eq!(app.s3_state.bucket_scroll_offset, 0);
7542        assert_eq!(app.s3_state.buckets.filter, "bucket-");
7543    }
7544
7545    #[test]
7546    fn test_s3_navigation_respects_filter() {
7547        let mut app = test_app();
7548        app.current_service = Service::S3Buckets;
7549        app.service_selected = true;
7550        app.mode = Mode::Normal;
7551
7552        // Add buckets
7553        app.s3_state.buckets.items = vec![
7554            S3Bucket {
7555                name: "prod-bucket".to_string(),
7556                region: "us-east-1".to_string(),
7557                creation_date: "2023-01-01".to_string(),
7558            },
7559            S3Bucket {
7560                name: "dev-bucket".to_string(),
7561                region: "us-east-1".to_string(),
7562                creation_date: "2023-01-02".to_string(),
7563            },
7564            S3Bucket {
7565                name: "prod-logs".to_string(),
7566                region: "us-east-1".to_string(),
7567                creation_date: "2023-01-03".to_string(),
7568            },
7569        ];
7570
7571        // Filter to only "prod" buckets (2 results)
7572        app.s3_state.buckets.filter = "prod".to_string();
7573
7574        // Should start at 0
7575        assert_eq!(app.s3_state.selected_row, 0);
7576
7577        // Navigate down - should go to row 1 (prod-logs)
7578        app.handle_action(Action::NextItem);
7579        assert_eq!(app.s3_state.selected_row, 1);
7580
7581        // Navigate down again - should stay at 1 (max for 2 filtered results)
7582        app.handle_action(Action::NextItem);
7583        assert_eq!(app.s3_state.selected_row, 1);
7584
7585        // Navigate up - should go back to 0
7586        app.handle_action(Action::PrevItem);
7587        assert_eq!(app.s3_state.selected_row, 0);
7588    }
7589
7590    #[test]
7591    fn test_next_preferences_lambda_functions() {
7592        let mut app = test_app();
7593        app.current_service = Service::LambdaFunctions;
7594        app.mode = Mode::ColumnSelector;
7595        app.column_selector_index = 0;
7596
7597        let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
7598        app.handle_action(Action::NextPreferences);
7599        assert_eq!(app.column_selector_index, page_size_idx);
7600
7601        app.handle_action(Action::NextPreferences);
7602        assert_eq!(app.column_selector_index, 0);
7603    }
7604
7605    #[test]
7606    fn test_next_preferences_lambda_applications() {
7607        let mut app = test_app();
7608        app.current_service = Service::LambdaApplications;
7609        app.mode = Mode::ColumnSelector;
7610        app.column_selector_index = 0;
7611
7612        let page_size_idx = app.lambda_application_column_ids.len() + 2;
7613        app.handle_action(Action::NextPreferences);
7614        assert_eq!(app.column_selector_index, page_size_idx);
7615
7616        app.handle_action(Action::NextPreferences);
7617        assert_eq!(app.column_selector_index, 0);
7618    }
7619
7620    #[test]
7621    fn test_next_preferences_ecr_images() {
7622        let mut app = test_app();
7623        app.current_service = Service::EcrRepositories;
7624        app.ecr_state.current_repository = Some("test-repo".to_string());
7625        app.mode = Mode::ColumnSelector;
7626        app.column_selector_index = 0;
7627
7628        let page_size_idx = app.ecr_image_column_ids.len() + 2;
7629        app.handle_action(Action::NextPreferences);
7630        assert_eq!(app.column_selector_index, page_size_idx);
7631
7632        app.handle_action(Action::NextPreferences);
7633        assert_eq!(app.column_selector_index, 0);
7634    }
7635
7636    #[test]
7637    fn test_next_preferences_cloudwatch_log_groups() {
7638        let mut app = test_app();
7639        app.current_service = Service::CloudWatchLogGroups;
7640        app.view_mode = ViewMode::List;
7641        app.mode = Mode::ColumnSelector;
7642        app.column_selector_index = 0;
7643
7644        // Tab from Columns to PageSize
7645        let page_size_idx = app.cw_log_group_column_ids.len() + 2;
7646        app.handle_action(Action::NextPreferences);
7647        assert_eq!(app.column_selector_index, page_size_idx);
7648
7649        // Tab from PageSize back to Columns
7650        app.handle_action(Action::NextPreferences);
7651        assert_eq!(app.column_selector_index, 0);
7652
7653        // Shift+Tab from Columns to PageSize
7654        app.handle_action(Action::PrevPreferences);
7655        assert_eq!(app.column_selector_index, page_size_idx);
7656
7657        // Shift+Tab from PageSize back to Columns
7658        app.handle_action(Action::PrevPreferences);
7659        assert_eq!(app.column_selector_index, 0);
7660    }
7661
7662    #[test]
7663    fn test_next_preferences_cloudwatch_log_streams() {
7664        let mut app = test_app();
7665        app.current_service = Service::CloudWatchLogGroups;
7666        app.view_mode = ViewMode::Detail;
7667        app.mode = Mode::ColumnSelector;
7668        app.column_selector_index = 0;
7669
7670        // Tab from Columns to PageSize
7671        let page_size_idx = app.cw_log_stream_column_ids.len() + 2;
7672        app.handle_action(Action::NextPreferences);
7673        assert_eq!(app.column_selector_index, page_size_idx);
7674
7675        // Tab from PageSize back to Columns
7676        app.handle_action(Action::NextPreferences);
7677        assert_eq!(app.column_selector_index, 0);
7678    }
7679
7680    #[test]
7681    fn test_cloudformation_next_item() {
7682        let mut app = test_app();
7683        app.current_service = Service::CloudFormationStacks;
7684        app.service_selected = true;
7685        app.mode = Mode::Normal;
7686        app.cfn_state.status_filter = CfnStatusFilter::Complete;
7687        app.cfn_state.table.items = vec![
7688            CfnStack {
7689                name: "stack1".to_string(),
7690                stack_id: "id1".to_string(),
7691                status: "CREATE_COMPLETE".to_string(),
7692                created_time: "2024-01-01".to_string(),
7693                updated_time: String::new(),
7694                deleted_time: String::new(),
7695                drift_status: String::new(),
7696                last_drift_check_time: String::new(),
7697                status_reason: String::new(),
7698                description: String::new(),
7699                detailed_status: String::new(),
7700                root_stack: String::new(),
7701                parent_stack: String::new(),
7702                termination_protection: false,
7703                iam_role: String::new(),
7704                tags: Vec::new(),
7705                stack_policy: String::new(),
7706                rollback_monitoring_time: String::new(),
7707                rollback_alarms: Vec::new(),
7708                notification_arns: Vec::new(),
7709            },
7710            CfnStack {
7711                name: "stack2".to_string(),
7712                stack_id: "id2".to_string(),
7713                status: "UPDATE_COMPLETE".to_string(),
7714                created_time: "2024-01-02".to_string(),
7715                updated_time: String::new(),
7716                deleted_time: String::new(),
7717                drift_status: String::new(),
7718                last_drift_check_time: String::new(),
7719                status_reason: String::new(),
7720                description: String::new(),
7721                detailed_status: String::new(),
7722                root_stack: String::new(),
7723                parent_stack: String::new(),
7724                termination_protection: false,
7725                iam_role: String::new(),
7726                tags: Vec::new(),
7727                stack_policy: String::new(),
7728                rollback_monitoring_time: String::new(),
7729                rollback_alarms: Vec::new(),
7730                notification_arns: Vec::new(),
7731            },
7732        ];
7733        app.cfn_state.table.reset();
7734
7735        app.handle_action(Action::NextItem);
7736        assert_eq!(app.cfn_state.table.selected, 1);
7737
7738        app.handle_action(Action::NextItem);
7739        assert_eq!(app.cfn_state.table.selected, 1); // At max
7740    }
7741
7742    #[test]
7743    fn test_cloudformation_prev_item() {
7744        let mut app = test_app();
7745        app.current_service = Service::CloudFormationStacks;
7746        app.service_selected = true;
7747        app.mode = Mode::Normal;
7748        app.cfn_state.status_filter = CfnStatusFilter::Complete;
7749        app.cfn_state.table.items = vec![
7750            CfnStack {
7751                name: "stack1".to_string(),
7752                stack_id: "id1".to_string(),
7753                status: "CREATE_COMPLETE".to_string(),
7754                created_time: "2024-01-01".to_string(),
7755                updated_time: String::new(),
7756                deleted_time: String::new(),
7757                drift_status: String::new(),
7758                last_drift_check_time: String::new(),
7759                status_reason: String::new(),
7760                description: String::new(),
7761                detailed_status: String::new(),
7762                root_stack: String::new(),
7763                parent_stack: String::new(),
7764                termination_protection: false,
7765                iam_role: String::new(),
7766                tags: Vec::new(),
7767                stack_policy: String::new(),
7768                rollback_monitoring_time: String::new(),
7769                rollback_alarms: Vec::new(),
7770                notification_arns: Vec::new(),
7771            },
7772            CfnStack {
7773                name: "stack2".to_string(),
7774                stack_id: "id2".to_string(),
7775                status: "UPDATE_COMPLETE".to_string(),
7776                created_time: "2024-01-02".to_string(),
7777                updated_time: String::new(),
7778                deleted_time: String::new(),
7779                drift_status: String::new(),
7780                last_drift_check_time: String::new(),
7781                status_reason: String::new(),
7782                description: String::new(),
7783                detailed_status: String::new(),
7784                root_stack: String::new(),
7785                parent_stack: String::new(),
7786                termination_protection: false,
7787                iam_role: String::new(),
7788                tags: Vec::new(),
7789                stack_policy: String::new(),
7790                rollback_monitoring_time: String::new(),
7791                rollback_alarms: Vec::new(),
7792                notification_arns: Vec::new(),
7793            },
7794        ];
7795        app.cfn_state.table.selected = 1;
7796
7797        app.handle_action(Action::PrevItem);
7798        assert_eq!(app.cfn_state.table.selected, 0);
7799
7800        app.handle_action(Action::PrevItem);
7801        assert_eq!(app.cfn_state.table.selected, 0); // At min
7802    }
7803
7804    #[test]
7805    fn test_cloudformation_page_down() {
7806        let mut app = test_app();
7807        app.current_service = Service::CloudFormationStacks;
7808        app.service_selected = true;
7809        app.mode = Mode::Normal;
7810        app.cfn_state.status_filter = CfnStatusFilter::Complete;
7811
7812        // Create 20 stacks
7813        for i in 0..20 {
7814            app.cfn_state.table.items.push(CfnStack {
7815                name: format!("stack{}", i),
7816                stack_id: format!("id{}", i),
7817                status: "CREATE_COMPLETE".to_string(),
7818                created_time: format!("2024-01-{:02}", i + 1),
7819                updated_time: String::new(),
7820                deleted_time: String::new(),
7821                drift_status: String::new(),
7822                last_drift_check_time: String::new(),
7823                status_reason: String::new(),
7824                description: String::new(),
7825                detailed_status: String::new(),
7826                root_stack: String::new(),
7827                parent_stack: String::new(),
7828                termination_protection: false,
7829                iam_role: String::new(),
7830                tags: Vec::new(),
7831                stack_policy: String::new(),
7832                rollback_monitoring_time: String::new(),
7833                rollback_alarms: Vec::new(),
7834                notification_arns: Vec::new(),
7835            });
7836        }
7837        app.cfn_state.table.reset();
7838
7839        app.handle_action(Action::PageDown);
7840        assert_eq!(app.cfn_state.table.selected, 10);
7841
7842        app.handle_action(Action::PageDown);
7843        assert_eq!(app.cfn_state.table.selected, 19); // Clamped to max
7844    }
7845
7846    #[test]
7847    fn test_cloudformation_page_up() {
7848        let mut app = test_app();
7849        app.current_service = Service::CloudFormationStacks;
7850        app.service_selected = true;
7851        app.mode = Mode::Normal;
7852        app.cfn_state.status_filter = CfnStatusFilter::Complete;
7853
7854        // Create 20 stacks
7855        for i in 0..20 {
7856            app.cfn_state.table.items.push(CfnStack {
7857                name: format!("stack{}", i),
7858                stack_id: format!("id{}", i),
7859                status: "CREATE_COMPLETE".to_string(),
7860                created_time: format!("2024-01-{:02}", i + 1),
7861                updated_time: String::new(),
7862                deleted_time: String::new(),
7863                drift_status: String::new(),
7864                last_drift_check_time: String::new(),
7865                status_reason: String::new(),
7866                description: String::new(),
7867                detailed_status: String::new(),
7868                root_stack: String::new(),
7869                parent_stack: String::new(),
7870                termination_protection: false,
7871                iam_role: String::new(),
7872                tags: Vec::new(),
7873                stack_policy: String::new(),
7874                rollback_monitoring_time: String::new(),
7875                rollback_alarms: Vec::new(),
7876                notification_arns: Vec::new(),
7877            });
7878        }
7879        app.cfn_state.table.selected = 15;
7880
7881        app.handle_action(Action::PageUp);
7882        assert_eq!(app.cfn_state.table.selected, 5);
7883
7884        app.handle_action(Action::PageUp);
7885        assert_eq!(app.cfn_state.table.selected, 0); // Clamped to min
7886    }
7887
7888    #[test]
7889    fn test_cloudformation_filter_input() {
7890        let mut app = test_app();
7891        app.current_service = Service::CloudFormationStacks;
7892        app.service_selected = true;
7893        app.mode = Mode::Normal;
7894
7895        app.handle_action(Action::StartFilter);
7896        assert_eq!(app.mode, Mode::FilterInput);
7897
7898        // Directly set filter (character input is handled in event loop, not actions)
7899        app.cfn_state.table.filter = "test".to_string();
7900        assert_eq!(app.cfn_state.table.filter, "test");
7901    }
7902
7903    #[test]
7904    fn test_cloudformation_filter_applies() {
7905        let mut app = test_app();
7906        app.current_service = Service::CloudFormationStacks;
7907        app.cfn_state.status_filter = CfnStatusFilter::Complete;
7908        app.cfn_state.table.items = vec![
7909            CfnStack {
7910                name: "prod-stack".to_string(),
7911                stack_id: "id1".to_string(),
7912                status: "CREATE_COMPLETE".to_string(),
7913                created_time: "2024-01-01".to_string(),
7914                updated_time: String::new(),
7915                deleted_time: String::new(),
7916                drift_status: String::new(),
7917                last_drift_check_time: String::new(),
7918                status_reason: String::new(),
7919                description: "Production stack".to_string(),
7920                detailed_status: String::new(),
7921                root_stack: String::new(),
7922                parent_stack: String::new(),
7923                termination_protection: false,
7924                iam_role: String::new(),
7925                tags: Vec::new(),
7926                stack_policy: String::new(),
7927                rollback_monitoring_time: String::new(),
7928                rollback_alarms: Vec::new(),
7929                notification_arns: Vec::new(),
7930            },
7931            CfnStack {
7932                name: "dev-stack".to_string(),
7933                stack_id: "id2".to_string(),
7934                status: "UPDATE_COMPLETE".to_string(),
7935                created_time: "2024-01-02".to_string(),
7936                updated_time: String::new(),
7937                deleted_time: String::new(),
7938                drift_status: String::new(),
7939                last_drift_check_time: String::new(),
7940                status_reason: String::new(),
7941                description: "Development stack".to_string(),
7942                detailed_status: String::new(),
7943                root_stack: String::new(),
7944                parent_stack: String::new(),
7945                termination_protection: false,
7946                iam_role: String::new(),
7947                tags: Vec::new(),
7948                stack_policy: String::new(),
7949                rollback_monitoring_time: String::new(),
7950                rollback_alarms: Vec::new(),
7951                notification_arns: Vec::new(),
7952            },
7953        ];
7954        app.cfn_state.table.filter = "prod".to_string();
7955
7956        let filtered = filtered_cloudformation_stacks(&app);
7957        assert_eq!(filtered.len(), 1);
7958        assert_eq!(filtered[0].name, "prod-stack");
7959    }
7960
7961    #[test]
7962    fn test_cloudformation_right_arrow_expands() {
7963        let mut app = test_app();
7964        app.current_service = Service::CloudFormationStacks;
7965        app.service_selected = true;
7966        app.cfn_state.status_filter = CfnStatusFilter::Complete;
7967        app.cfn_state.table.items = vec![CfnStack {
7968            name: "test-stack".to_string(),
7969            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
7970                .to_string(),
7971            status: "CREATE_COMPLETE".to_string(),
7972            created_time: "2024-01-01".to_string(),
7973            updated_time: String::new(),
7974            deleted_time: String::new(),
7975            drift_status: String::new(),
7976            last_drift_check_time: String::new(),
7977            status_reason: String::new(),
7978            description: "Test stack".to_string(),
7979            detailed_status: String::new(),
7980            root_stack: String::new(),
7981            parent_stack: String::new(),
7982            termination_protection: false,
7983            iam_role: String::new(),
7984            tags: Vec::new(),
7985            stack_policy: String::new(),
7986            rollback_monitoring_time: String::new(),
7987            rollback_alarms: Vec::new(),
7988            notification_arns: Vec::new(),
7989        }];
7990        app.cfn_state.table.reset();
7991
7992        assert_eq!(app.cfn_state.table.expanded_item, None);
7993
7994        app.handle_action(Action::NextPane);
7995        assert_eq!(app.cfn_state.table.expanded_item, Some(0));
7996    }
7997
7998    #[test]
7999    fn test_cloudformation_left_arrow_collapses() {
8000        let mut app = test_app();
8001        app.current_service = Service::CloudFormationStacks;
8002        app.service_selected = true;
8003        app.cfn_state.status_filter = CfnStatusFilter::Complete;
8004        app.cfn_state.table.items = vec![CfnStack {
8005            name: "test-stack".to_string(),
8006            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8007                .to_string(),
8008            status: "CREATE_COMPLETE".to_string(),
8009            created_time: "2024-01-01".to_string(),
8010            updated_time: String::new(),
8011            deleted_time: String::new(),
8012            drift_status: String::new(),
8013            last_drift_check_time: String::new(),
8014            status_reason: String::new(),
8015            description: "Test stack".to_string(),
8016            detailed_status: String::new(),
8017            root_stack: String::new(),
8018            parent_stack: String::new(),
8019            termination_protection: false,
8020            iam_role: String::new(),
8021            tags: Vec::new(),
8022            stack_policy: String::new(),
8023            rollback_monitoring_time: String::new(),
8024            rollback_alarms: Vec::new(),
8025            notification_arns: Vec::new(),
8026        }];
8027        app.cfn_state.table.reset();
8028        app.cfn_state.table.expanded_item = Some(0);
8029
8030        app.handle_action(Action::PrevPane);
8031        assert_eq!(app.cfn_state.table.expanded_item, None);
8032    }
8033
8034    #[test]
8035    fn test_cloudformation_enter_drills_into_stack() {
8036        let mut app = test_app();
8037        app.current_service = Service::CloudFormationStacks;
8038        app.service_selected = true;
8039        app.mode = Mode::Normal;
8040        app.tabs = vec![Tab {
8041            service: Service::CloudFormationStacks,
8042            title: "CloudFormation › Stacks".to_string(),
8043            breadcrumb: "CloudFormation › Stacks".to_string(),
8044        }];
8045        app.current_tab = 0;
8046        app.cfn_state.status_filter = CfnStatusFilter::Complete;
8047        app.cfn_state.table.items = vec![CfnStack {
8048            name: "test-stack".to_string(),
8049            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8050                .to_string(),
8051            status: "CREATE_COMPLETE".to_string(),
8052            created_time: "2024-01-01".to_string(),
8053            updated_time: String::new(),
8054            deleted_time: String::new(),
8055            drift_status: String::new(),
8056            last_drift_check_time: String::new(),
8057            status_reason: String::new(),
8058            description: "Test stack".to_string(),
8059            detailed_status: String::new(),
8060            root_stack: String::new(),
8061            parent_stack: String::new(),
8062            termination_protection: false,
8063            iam_role: String::new(),
8064            tags: Vec::new(),
8065            stack_policy: String::new(),
8066            rollback_monitoring_time: String::new(),
8067            rollback_alarms: Vec::new(),
8068            notification_arns: Vec::new(),
8069        }];
8070        app.cfn_state.table.reset();
8071
8072        // Verify filtering works
8073        let filtered = filtered_cloudformation_stacks(&app);
8074        assert_eq!(filtered.len(), 1);
8075        assert_eq!(filtered[0].name, "test-stack");
8076
8077        assert_eq!(app.cfn_state.current_stack, None);
8078
8079        // Enter drills into stack detail view
8080        app.handle_action(Action::Select);
8081        assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
8082    }
8083
8084    #[test]
8085    fn test_cloudformation_copy_to_clipboard() {
8086        let mut app = test_app();
8087        app.current_service = Service::CloudFormationStacks;
8088        app.service_selected = true;
8089        app.mode = Mode::Normal;
8090        app.cfn_state.status_filter = CfnStatusFilter::Complete;
8091        app.cfn_state.table.items = vec![
8092            CfnStack {
8093                name: "stack1".to_string(),
8094                stack_id: "id1".to_string(),
8095                status: "CREATE_COMPLETE".to_string(),
8096                created_time: "2024-01-01".to_string(),
8097                updated_time: String::new(),
8098                deleted_time: String::new(),
8099                drift_status: String::new(),
8100                last_drift_check_time: String::new(),
8101                status_reason: String::new(),
8102                description: String::new(),
8103                detailed_status: String::new(),
8104                root_stack: String::new(),
8105                parent_stack: String::new(),
8106                termination_protection: false,
8107                iam_role: String::new(),
8108                tags: Vec::new(),
8109                stack_policy: String::new(),
8110                rollback_monitoring_time: String::new(),
8111                rollback_alarms: Vec::new(),
8112                notification_arns: Vec::new(),
8113            },
8114            CfnStack {
8115                name: "stack2".to_string(),
8116                stack_id: "id2".to_string(),
8117                status: "UPDATE_COMPLETE".to_string(),
8118                created_time: "2024-01-02".to_string(),
8119                updated_time: String::new(),
8120                deleted_time: String::new(),
8121                drift_status: String::new(),
8122                last_drift_check_time: String::new(),
8123                status_reason: String::new(),
8124                description: String::new(),
8125                detailed_status: String::new(),
8126                root_stack: String::new(),
8127                parent_stack: String::new(),
8128                termination_protection: false,
8129                iam_role: String::new(),
8130                tags: Vec::new(),
8131                stack_policy: String::new(),
8132                rollback_monitoring_time: String::new(),
8133                rollback_alarms: Vec::new(),
8134                notification_arns: Vec::new(),
8135            },
8136        ];
8137
8138        assert!(!app.snapshot_requested);
8139        app.handle_action(Action::CopyToClipboard);
8140
8141        // Should set snapshot_requested flag
8142        assert!(app.snapshot_requested);
8143    }
8144
8145    #[test]
8146    fn test_cloudformation_expansion_shows_all_visible_columns() {
8147        let mut app = test_app();
8148        app.current_service = Service::CloudFormationStacks;
8149        app.cfn_state.status_filter = CfnStatusFilter::Complete;
8150        app.cfn_state.table.items = vec![CfnStack {
8151            name: "test-stack".to_string(),
8152            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
8153                .to_string(),
8154            status: "CREATE_COMPLETE".to_string(),
8155            created_time: "2024-01-01".to_string(),
8156            updated_time: "2024-01-02".to_string(),
8157            deleted_time: String::new(),
8158            drift_status: "IN_SYNC".to_string(),
8159            last_drift_check_time: "2024-01-03".to_string(),
8160            status_reason: String::new(),
8161            description: "Test description".to_string(),
8162            detailed_status: String::new(),
8163            root_stack: String::new(),
8164            parent_stack: String::new(),
8165            termination_protection: false,
8166            iam_role: String::new(),
8167            tags: Vec::new(),
8168            stack_policy: String::new(),
8169            rollback_monitoring_time: String::new(),
8170            rollback_alarms: Vec::new(),
8171            notification_arns: Vec::new(),
8172        }];
8173
8174        // Set visible columns
8175        app.cfn_visible_column_ids = [
8176            CfnColumn::Name,
8177            CfnColumn::Status,
8178            CfnColumn::CreatedTime,
8179            CfnColumn::Description,
8180        ]
8181        .iter()
8182        .map(|c| c.id())
8183        .collect();
8184
8185        app.cfn_state.table.expanded_item = Some(0);
8186
8187        // Verify all visible columns would be shown in expansion
8188        // (This is a structural test - actual rendering is in UI layer)
8189        assert_eq!(app.cfn_visible_column_ids.len(), 4);
8190        assert!(app.cfn_state.table.has_expanded_item());
8191    }
8192
8193    #[test]
8194    fn test_cloudformation_empty_list_shows_page_1() {
8195        let mut app = test_app();
8196        app.current_service = Service::CloudFormationStacks;
8197        app.cfn_state.table.items = vec![];
8198
8199        let filtered = filtered_cloudformation_stacks(&app);
8200        assert_eq!(filtered.len(), 0);
8201
8202        // Pagination should still show [1] even with 0 items
8203        let page_size = app.cfn_state.table.page_size.value();
8204        let total_pages = filtered.len().div_ceil(page_size);
8205        assert_eq!(total_pages, 0);
8206
8207        // render_pagination_text(0, 0) should return "[1]"
8208        // This is tested in UI layer
8209    }
8210}
8211
8212impl App {
8213    pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
8214        let mut all = AwsRegion::all();
8215
8216        // Add latencies to regions
8217        for region in &mut all {
8218            region.latency_ms = self.region_latencies.get(region.code).copied();
8219        }
8220
8221        // Filter by search term
8222        let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
8223            all
8224        } else {
8225            let filter_lower = self.region_filter.to_lowercase();
8226            all.into_iter()
8227                .filter(|r| {
8228                    r.name.to_lowercase().contains(&filter_lower)
8229                        || r.code.to_lowercase().contains(&filter_lower)
8230                        || r.group.to_lowercase().contains(&filter_lower)
8231                })
8232                .collect()
8233        };
8234
8235        // Sort by latency (lowest first), treat None as 1000ms
8236        let mut sorted = filtered;
8237        sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
8238        sorted
8239    }
8240
8241    pub fn measure_region_latencies(&mut self) {
8242        use std::time::Instant;
8243        self.region_latencies.clear();
8244
8245        let regions = AwsRegion::all();
8246        let start_all = Instant::now();
8247        tracing::info!("Starting latency measurement for {} regions", regions.len());
8248
8249        let handles: Vec<_> = regions
8250            .iter()
8251            .map(|region| {
8252                let code = region.code.to_string();
8253                std::thread::spawn(move || {
8254                    // Use STS endpoint - fastest and most reliable
8255                    let endpoint = format!("https://sts.{}.amazonaws.com", code);
8256                    let start = Instant::now();
8257
8258                    match ureq::get(&endpoint)
8259                        .timeout(std::time::Duration::from_secs(2))
8260                        .call()
8261                    {
8262                        Ok(_) => {
8263                            let latency = start.elapsed().as_millis() as u64;
8264                            Some((code, latency))
8265                        }
8266                        Err(e) => {
8267                            tracing::debug!("Failed to measure {}: {}", code, e);
8268                            Some((code, 9999))
8269                        }
8270                    }
8271                })
8272            })
8273            .collect();
8274
8275        for handle in handles {
8276            if let Ok(Some((code, latency))) = handle.join() {
8277                self.region_latencies.insert(code, latency);
8278            }
8279        }
8280
8281        tracing::info!(
8282            "Measured {} regions in {:?}",
8283            self.region_latencies.len(),
8284            start_all.elapsed()
8285        );
8286    }
8287
8288    pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
8289        filter_profiles(&self.available_profiles, &self.profile_filter)
8290    }
8291
8292    pub fn get_filtered_sessions(&self) -> Vec<&Session> {
8293        if self.session_filter.is_empty() {
8294            return self.sessions.iter().collect();
8295        }
8296        let filter_lower = self.session_filter.to_lowercase();
8297        self.sessions
8298            .iter()
8299            .filter(|s| {
8300                s.profile.to_lowercase().contains(&filter_lower)
8301                    || s.region.to_lowercase().contains(&filter_lower)
8302                    || s.account_id.to_lowercase().contains(&filter_lower)
8303                    || s.role_arn.to_lowercase().contains(&filter_lower)
8304            })
8305            .collect()
8306    }
8307
8308    pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
8309        if self.tab_filter.is_empty() {
8310            return self.tabs.iter().enumerate().collect();
8311        }
8312        let filter_lower = self.tab_filter.to_lowercase();
8313        self.tabs
8314            .iter()
8315            .enumerate()
8316            .filter(|(_, tab)| {
8317                tab.title.to_lowercase().contains(&filter_lower)
8318                    || tab.breadcrumb.to_lowercase().contains(&filter_lower)
8319            })
8320            .collect()
8321    }
8322
8323    pub fn load_aws_profiles() -> Vec<AwsProfile> {
8324        AwsProfile::load_all()
8325    }
8326
8327    pub async fn fetch_profile_accounts(&mut self) {
8328        for profile in &mut self.available_profiles {
8329            if profile.account.is_none() {
8330                let region = profile
8331                    .region
8332                    .clone()
8333                    .unwrap_or_else(|| "us-east-1".to_string());
8334                if let Ok(account) =
8335                    rusticity_core::AwsConfig::get_account_for_profile(&profile.name, &region).await
8336                {
8337                    profile.account = Some(account);
8338                }
8339            }
8340        }
8341    }
8342
8343    fn save_current_session(&mut self) {
8344        // If no tabs, delete the session if it exists
8345        if self.tabs.is_empty() {
8346            if let Some(ref session) = self.current_session {
8347                let _ = session.delete();
8348                self.current_session = None;
8349            }
8350            return;
8351        }
8352
8353        let session = if let Some(ref mut current) = self.current_session {
8354            // Update existing session
8355            current.tabs = self
8356                .tabs
8357                .iter()
8358                .map(|t| SessionTab {
8359                    service: format!("{:?}", t.service),
8360                    title: t.title.clone(),
8361                    breadcrumb: t.breadcrumb.clone(),
8362                    filter: match t.service {
8363                        Service::CloudWatchLogGroups => {
8364                            Some(self.log_groups_state.log_groups.filter.clone())
8365                        }
8366                        _ => None,
8367                    },
8368                    selected_item: None,
8369                })
8370                .collect();
8371            current.clone()
8372        } else {
8373            // Create new session
8374            let mut session = Session::new(
8375                self.profile.clone(),
8376                self.region.clone(),
8377                self.config.account_id.clone(),
8378                self.config.role_arn.clone(),
8379            );
8380            session.tabs = self
8381                .tabs
8382                .iter()
8383                .map(|t| SessionTab {
8384                    service: format!("{:?}", t.service),
8385                    title: t.title.clone(),
8386                    breadcrumb: t.breadcrumb.clone(),
8387                    filter: match t.service {
8388                        Service::CloudWatchLogGroups => {
8389                            Some(self.log_groups_state.log_groups.filter.clone())
8390                        }
8391                        _ => None,
8392                    },
8393                    selected_item: None,
8394                })
8395                .collect();
8396            self.current_session = Some(session.clone());
8397            session
8398        };
8399
8400        let _ = session.save();
8401    }
8402}
8403
8404#[cfg(test)]
8405mod iam_policy_view_tests {
8406    use super::*;
8407    use test_helpers::*;
8408
8409    #[test]
8410    fn test_enter_opens_policy_view() {
8411        let mut app = test_app();
8412        app.current_service = Service::IamRoles;
8413        app.service_selected = true;
8414        app.mode = Mode::Normal;
8415        app.view_mode = ViewMode::Detail;
8416        app.iam_state.current_role = Some("TestRole".to_string());
8417        app.iam_state.policies.items = vec![IamPolicy {
8418            policy_name: "TestPolicy".to_string(),
8419            policy_type: "Inline".to_string(),
8420            attached_via: "Direct".to_string(),
8421            attached_entities: "1".to_string(),
8422            description: "Test".to_string(),
8423            creation_time: "2023-01-01".to_string(),
8424            edited_time: "2023-01-01".to_string(),
8425            policy_arn: None,
8426        }];
8427        app.iam_state.policies.reset();
8428
8429        app.handle_action(Action::Select);
8430
8431        assert_eq!(app.view_mode, ViewMode::PolicyView);
8432        assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
8433        assert_eq!(app.iam_state.policy_scroll, 0);
8434        assert!(app.iam_state.policies.loading);
8435    }
8436
8437    #[test]
8438    fn test_escape_closes_policy_view() {
8439        let mut app = test_app();
8440        app.current_service = Service::IamRoles;
8441        app.service_selected = true;
8442        app.mode = Mode::Normal;
8443        app.view_mode = ViewMode::PolicyView;
8444        app.iam_state.current_role = Some("TestRole".to_string());
8445        app.iam_state.current_policy = Some("TestPolicy".to_string());
8446        app.iam_state.policy_document = "{\n  \"test\": \"value\"\n}".to_string();
8447        app.iam_state.policy_scroll = 5;
8448
8449        app.handle_action(Action::PrevPane);
8450
8451        assert_eq!(app.view_mode, ViewMode::Detail);
8452        assert_eq!(app.iam_state.current_policy, None);
8453        assert_eq!(app.iam_state.policy_document, "");
8454        assert_eq!(app.iam_state.policy_scroll, 0);
8455    }
8456
8457    #[test]
8458    fn test_ctrl_d_scrolls_down_in_policy_view() {
8459        let mut app = test_app();
8460        app.current_service = Service::IamRoles;
8461        app.service_selected = true;
8462        app.mode = Mode::Normal;
8463        app.view_mode = ViewMode::PolicyView;
8464        app.iam_state.current_role = Some("TestRole".to_string());
8465        app.iam_state.current_policy = Some("TestPolicy".to_string());
8466        app.iam_state.policy_document = (0..100)
8467            .map(|i| format!("line {}", i))
8468            .collect::<Vec<_>>()
8469            .join("\n");
8470        app.iam_state.policy_scroll = 0;
8471
8472        app.handle_action(Action::ScrollDown);
8473
8474        assert_eq!(app.iam_state.policy_scroll, 10);
8475
8476        app.handle_action(Action::ScrollDown);
8477
8478        assert_eq!(app.iam_state.policy_scroll, 20);
8479    }
8480
8481    #[test]
8482    fn test_ctrl_u_scrolls_up_in_policy_view() {
8483        let mut app = test_app();
8484        app.current_service = Service::IamRoles;
8485        app.service_selected = true;
8486        app.mode = Mode::Normal;
8487        app.view_mode = ViewMode::PolicyView;
8488        app.iam_state.current_role = Some("TestRole".to_string());
8489        app.iam_state.current_policy = Some("TestPolicy".to_string());
8490        app.iam_state.policy_document = (0..100)
8491            .map(|i| format!("line {}", i))
8492            .collect::<Vec<_>>()
8493            .join("\n");
8494        app.iam_state.policy_scroll = 30;
8495
8496        app.handle_action(Action::ScrollUp);
8497
8498        assert_eq!(app.iam_state.policy_scroll, 20);
8499
8500        app.handle_action(Action::ScrollUp);
8501
8502        assert_eq!(app.iam_state.policy_scroll, 10);
8503    }
8504
8505    #[test]
8506    fn test_scroll_does_not_go_negative() {
8507        let mut app = test_app();
8508        app.current_service = Service::IamRoles;
8509        app.service_selected = true;
8510        app.mode = Mode::Normal;
8511        app.view_mode = ViewMode::PolicyView;
8512        app.iam_state.current_role = Some("TestRole".to_string());
8513        app.iam_state.current_policy = Some("TestPolicy".to_string());
8514        app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
8515        app.iam_state.policy_scroll = 0;
8516
8517        app.handle_action(Action::ScrollUp);
8518
8519        assert_eq!(app.iam_state.policy_scroll, 0);
8520    }
8521
8522    #[test]
8523    fn test_scroll_does_not_exceed_max() {
8524        let mut app = test_app();
8525        app.current_service = Service::IamRoles;
8526        app.service_selected = true;
8527        app.mode = Mode::Normal;
8528        app.view_mode = ViewMode::PolicyView;
8529        app.iam_state.current_role = Some("TestRole".to_string());
8530        app.iam_state.current_policy = Some("TestPolicy".to_string());
8531        app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
8532        app.iam_state.policy_scroll = 0;
8533
8534        app.handle_action(Action::ScrollDown);
8535
8536        assert_eq!(app.iam_state.policy_scroll, 2); // Max is 2 (3 lines - 1)
8537    }
8538
8539    #[test]
8540    fn test_policy_view_console_url() {
8541        let mut app = test_app();
8542        app.current_service = Service::IamRoles;
8543        app.service_selected = true;
8544        app.view_mode = ViewMode::PolicyView;
8545        app.iam_state.current_role = Some("TestRole".to_string());
8546        app.iam_state.current_policy = Some("TestPolicy".to_string());
8547
8548        let url = app.get_console_url();
8549
8550        assert!(url.contains("us-east-1.console.aws.amazon.com"));
8551        assert!(url.contains("/roles/details/TestRole"));
8552        assert!(url.contains("/editPolicy/TestPolicy"));
8553        assert!(url.contains("step=addPermissions"));
8554    }
8555
8556    #[test]
8557    fn test_esc_from_policy_view_goes_to_role_detail() {
8558        let mut app = test_app();
8559        app.current_service = Service::IamRoles;
8560        app.service_selected = true;
8561        app.mode = Mode::Normal;
8562        app.view_mode = ViewMode::PolicyView;
8563        app.iam_state.current_role = Some("TestRole".to_string());
8564        app.iam_state.current_policy = Some("TestPolicy".to_string());
8565        app.iam_state.policy_document = "test".to_string();
8566        app.iam_state.policy_scroll = 5;
8567
8568        app.handle_action(Action::GoBack);
8569
8570        assert_eq!(app.view_mode, ViewMode::Detail);
8571        assert_eq!(app.iam_state.current_policy, None);
8572        assert_eq!(app.iam_state.policy_document, "");
8573        assert_eq!(app.iam_state.policy_scroll, 0);
8574        assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
8575    }
8576
8577    #[test]
8578    fn test_esc_from_role_detail_goes_to_role_list() {
8579        let mut app = test_app();
8580        app.current_service = Service::IamRoles;
8581        app.service_selected = true;
8582        app.mode = Mode::Normal;
8583        app.view_mode = ViewMode::Detail;
8584        app.iam_state.current_role = Some("TestRole".to_string());
8585
8586        app.handle_action(Action::GoBack);
8587
8588        assert_eq!(app.iam_state.current_role, None);
8589    }
8590
8591    #[test]
8592    fn test_right_arrow_expands_policy_row() {
8593        let mut app = test_app();
8594        app.current_service = Service::IamRoles;
8595        app.service_selected = true;
8596        app.mode = Mode::Normal;
8597        app.view_mode = ViewMode::Detail;
8598        app.iam_state.current_role = Some("TestRole".to_string());
8599        app.iam_state.policies.items = vec![IamPolicy {
8600            policy_name: "TestPolicy".to_string(),
8601            policy_type: "Inline".to_string(),
8602            attached_via: "Direct".to_string(),
8603            attached_entities: "1".to_string(),
8604            description: "Test".to_string(),
8605            creation_time: "2023-01-01".to_string(),
8606            edited_time: "2023-01-01".to_string(),
8607            policy_arn: None,
8608        }];
8609        app.iam_state.policies.reset();
8610
8611        app.handle_action(Action::NextPane);
8612
8613        // Should expand, not drill down
8614        assert_eq!(app.view_mode, ViewMode::Detail);
8615        assert_eq!(app.iam_state.current_policy, None);
8616        assert_eq!(app.iam_state.policies.expanded_item, Some(0));
8617    }
8618
8619    #[test]
8620    fn test_managed_policy_has_creation_and_edit_time() {
8621        // Managed policies have a policy_arn and should show creation/edit times
8622        // when loaded. Inline policies don't have these (they're embedded in the role).
8623        // This test verifies managed policies don't show "-" for creation_time.
8624        use crate::iam::Policy as IamPolicy;
8625        let managed = IamPolicy {
8626            policy_name: "MyManagedPolicy".to_string(),
8627            policy_type: "Managed".to_string(),
8628            attached_via: "Direct".to_string(),
8629            attached_entities: "-".to_string(),
8630            description: "-".to_string(),
8631            creation_time: "2024-01-01 00:00:00 (UTC)".to_string(),
8632            edited_time: "2024-06-01 00:00:00 (UTC)".to_string(),
8633            policy_arn: Some("arn:aws:iam::123:policy/MyManagedPolicy".to_string()),
8634        };
8635        assert_ne!(
8636            managed.creation_time, "-",
8637            "Managed policy must have a real creation time, not '-'"
8638        );
8639        assert_ne!(
8640            managed.edited_time, "-",
8641            "Managed policy must have a real edit time, not '-'"
8642        );
8643
8644        // Inline policies legitimately have no creation/edit time
8645        let inline = IamPolicy {
8646            policy_name: "InlinePolicy".to_string(),
8647            policy_type: "Inline".to_string(),
8648            attached_via: "Direct".to_string(),
8649            attached_entities: "-".to_string(),
8650            description: "-".to_string(),
8651            creation_time: "-".to_string(),
8652            edited_time: "-".to_string(),
8653            policy_arn: None,
8654        };
8655        assert_eq!(
8656            inline.creation_time, "-",
8657            "Inline policies have no creation time"
8658        );
8659    }
8660
8661    #[test]
8662    fn test_yank_in_policy_view_copies_document_not_arn() {
8663        // In PolicyView, 'y' must copy the policy JSON, not the role ARN.
8664        let mut app = test_app();
8665        app.current_service = Service::IamRoles;
8666        app.service_selected = true;
8667        app.mode = Mode::Normal;
8668        app.view_mode = ViewMode::PolicyView;
8669        app.iam_state.current_role = Some("TestRole".to_string());
8670        app.iam_state.current_policy = Some("TestPolicy".to_string());
8671
8672        let policy_json = "{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": []\n}".to_string();
8673        app.iam_state.policy_document = policy_json.clone();
8674
8675        // Call yank_roles — it must read policy_document when view_mode == PolicyView.
8676        use crate::iam::actions::yank_roles;
8677        yank_roles(&app);
8678
8679        // State must be unchanged after yank (clipboard is a side-effect we can't inspect in tests)
8680        assert_eq!(app.iam_state.policy_document, policy_json);
8681        assert_eq!(app.view_mode, ViewMode::PolicyView);
8682    }
8683}
8684
8685#[cfg(test)]
8686mod tab_filter_tests {
8687    use super::*;
8688    use test_helpers::*;
8689
8690    #[test]
8691    fn test_space_t_opens_tab_picker() {
8692        let mut app = test_app();
8693        app.tabs = vec![
8694            Tab {
8695                service: Service::CloudWatchLogGroups,
8696                title: "Tab 1".to_string(),
8697                breadcrumb: "CloudWatch > Log groups".to_string(),
8698            },
8699            Tab {
8700                service: Service::S3Buckets,
8701                title: "Tab 2".to_string(),
8702                breadcrumb: "S3 › Buckets".to_string(),
8703            },
8704        ];
8705        app.current_tab = 0;
8706
8707        app.handle_action(Action::OpenTabPicker);
8708
8709        assert_eq!(app.mode, Mode::TabPicker);
8710        assert_eq!(app.tab_picker_selected, 0);
8711    }
8712
8713    #[test]
8714    fn test_tab_filter_works() {
8715        let mut app = test_app();
8716        app.tabs = vec![
8717            Tab {
8718                service: Service::CloudWatchLogGroups,
8719                title: "CloudWatch Logs".to_string(),
8720                breadcrumb: "CloudWatch > Log groups".to_string(),
8721            },
8722            Tab {
8723                service: Service::S3Buckets,
8724                title: "S3 Buckets".to_string(),
8725                breadcrumb: "S3 › Buckets".to_string(),
8726            },
8727            Tab {
8728                service: Service::CloudWatchAlarms,
8729                title: "CloudWatch Alarms".to_string(),
8730                breadcrumb: "CloudWatch › Alarms".to_string(),
8731            },
8732        ];
8733        app.mode = Mode::TabPicker;
8734
8735        // Filter for "s3"
8736        app.handle_action(Action::FilterInput('s'));
8737        app.handle_action(Action::FilterInput('3'));
8738
8739        let filtered = app.get_filtered_tabs();
8740        assert_eq!(filtered.len(), 1);
8741        assert_eq!(filtered[0].1.title, "S3 Buckets");
8742    }
8743
8744    #[test]
8745    fn test_tab_filter_by_breadcrumb() {
8746        let mut app = test_app();
8747        app.tabs = vec![
8748            Tab {
8749                service: Service::CloudWatchLogGroups,
8750                title: "Tab 1".to_string(),
8751                breadcrumb: "CloudWatch > Log groups".to_string(),
8752            },
8753            Tab {
8754                service: Service::S3Buckets,
8755                title: "Tab 2".to_string(),
8756                breadcrumb: "S3 › Buckets".to_string(),
8757            },
8758        ];
8759        app.mode = Mode::TabPicker;
8760
8761        // Filter for "cloudwatch"
8762        app.handle_action(Action::FilterInput('c'));
8763        app.handle_action(Action::FilterInput('l'));
8764        app.handle_action(Action::FilterInput('o'));
8765        app.handle_action(Action::FilterInput('u'));
8766        app.handle_action(Action::FilterInput('d'));
8767
8768        let filtered = app.get_filtered_tabs();
8769        assert_eq!(filtered.len(), 1);
8770        assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
8771    }
8772
8773    #[test]
8774    fn test_tab_filter_backspace() {
8775        let mut app = test_app();
8776        app.tabs = vec![
8777            Tab {
8778                service: Service::CloudWatchLogGroups,
8779                title: "CloudWatch Logs".to_string(),
8780                breadcrumb: "CloudWatch > Log groups".to_string(),
8781            },
8782            Tab {
8783                service: Service::S3Buckets,
8784                title: "S3 Buckets".to_string(),
8785                breadcrumb: "S3 › Buckets".to_string(),
8786            },
8787        ];
8788        app.mode = Mode::TabPicker;
8789
8790        app.handle_action(Action::FilterInput('s'));
8791        app.handle_action(Action::FilterInput('3'));
8792        assert_eq!(app.tab_filter, "s3");
8793
8794        app.handle_action(Action::FilterBackspace);
8795        assert_eq!(app.tab_filter, "s");
8796
8797        let filtered = app.get_filtered_tabs();
8798        assert_eq!(filtered.len(), 2); // Both match "s"
8799    }
8800
8801    #[test]
8802    fn test_tab_selection_with_filter() {
8803        let mut app = test_app();
8804        app.tabs = vec![
8805            Tab {
8806                service: Service::CloudWatchLogGroups,
8807                title: "CloudWatch Logs".to_string(),
8808                breadcrumb: "CloudWatch > Log groups".to_string(),
8809            },
8810            Tab {
8811                service: Service::S3Buckets,
8812                title: "S3 Buckets".to_string(),
8813                breadcrumb: "S3 › Buckets".to_string(),
8814            },
8815        ];
8816        app.mode = Mode::TabPicker;
8817        app.current_tab = 0;
8818
8819        // Filter for "s3"
8820        app.handle_action(Action::FilterInput('s'));
8821        app.handle_action(Action::FilterInput('3'));
8822
8823        // Select the filtered tab
8824        app.handle_action(Action::Select);
8825
8826        assert_eq!(app.current_tab, 1); // Should select the S3 tab (index 1)
8827        assert_eq!(app.mode, Mode::Normal);
8828        assert_eq!(app.tab_filter, ""); // Filter should be cleared
8829    }
8830}
8831
8832#[cfg(test)]
8833mod region_latency_tests {
8834    use super::*;
8835    use test_helpers::*;
8836
8837    #[test]
8838    fn test_regions_sorted_by_latency() {
8839        let mut app = test_app();
8840
8841        // Add some latencies
8842        app.region_latencies.insert("us-west-2".to_string(), 50);
8843        app.region_latencies.insert("us-east-1".to_string(), 10);
8844        app.region_latencies.insert("eu-west-1".to_string(), 100);
8845
8846        let filtered = app.get_filtered_regions();
8847
8848        // Should be sorted by latency (lowest first)
8849        let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
8850
8851        assert!(with_latency.len() >= 3);
8852        assert_eq!(with_latency[0].code, "us-east-1");
8853        assert_eq!(with_latency[0].latency_ms, Some(10));
8854        assert_eq!(with_latency[1].code, "us-west-2");
8855        assert_eq!(with_latency[1].latency_ms, Some(50));
8856        assert_eq!(with_latency[2].code, "eu-west-1");
8857        assert_eq!(with_latency[2].latency_ms, Some(100));
8858    }
8859
8860    #[test]
8861    fn test_regions_with_latency_before_without() {
8862        let mut app = test_app();
8863
8864        // Only add latency for one region
8865        app.region_latencies.insert("eu-west-1".to_string(), 100);
8866
8867        let filtered = app.get_filtered_regions();
8868
8869        // Region with latency should come first
8870        assert_eq!(filtered[0].code, "eu-west-1");
8871        assert_eq!(filtered[0].latency_ms, Some(100));
8872
8873        // Rest should be sorted by name
8874        for region in &filtered[1..] {
8875            assert!(region.latency_ms.is_none());
8876        }
8877    }
8878
8879    #[test]
8880    fn test_region_filter_with_latency() {
8881        let mut app = test_app();
8882
8883        app.region_latencies.insert("us-east-1".to_string(), 10);
8884        app.region_latencies.insert("us-west-2".to_string(), 50);
8885        app.region_filter = "us".to_string();
8886
8887        let filtered = app.get_filtered_regions();
8888
8889        // Should only have US regions, sorted by latency
8890        assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
8891        assert_eq!(filtered[0].code, "us-east-1");
8892        assert_eq!(filtered[1].code, "us-west-2");
8893    }
8894
8895    #[test]
8896    fn test_latency_persists_across_filters() {
8897        let mut app = test_app();
8898
8899        app.region_latencies.insert("us-east-1".to_string(), 10);
8900
8901        // Filter to something else
8902        app.region_filter = "eu".to_string();
8903        let filtered = app.get_filtered_regions();
8904        assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
8905
8906        // Clear filter
8907        app.region_filter.clear();
8908        let all = app.get_filtered_regions();
8909
8910        // Latency should still be there
8911        let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
8912        assert_eq!(us_east.latency_ms, Some(10));
8913    }
8914
8915    #[test]
8916    fn test_measure_region_latencies_clears_previous() {
8917        let mut app = test_app();
8918
8919        // Add some fake latencies
8920        app.region_latencies.insert("us-east-1".to_string(), 100);
8921        app.region_latencies.insert("eu-west-1".to_string(), 200);
8922
8923        // Measure again (will fail to connect but should clear)
8924        app.measure_region_latencies();
8925
8926        // Old latencies should be cleared
8927        assert!(
8928            app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
8929        );
8930    }
8931
8932    #[test]
8933    fn test_regions_with_latency_sorted_first() {
8934        let mut app = test_app();
8935
8936        // Add latencies: one fast, one slow (>1000ms would be treated as >1s)
8937        app.region_latencies.insert("us-east-1".to_string(), 50);
8938        app.region_latencies.insert("eu-west-1".to_string(), 500);
8939
8940        let filtered = app.get_filtered_regions();
8941
8942        // Should show all regions
8943        assert!(filtered.len() > 2);
8944
8945        // Fast regions first
8946        assert_eq!(filtered[0].code, "us-east-1");
8947        assert_eq!(filtered[0].latency_ms, Some(50));
8948        assert_eq!(filtered[1].code, "eu-west-1");
8949        assert_eq!(filtered[1].latency_ms, Some(500));
8950
8951        // Regions without latency treated as 1000ms, so they come after 500ms
8952        for region in &filtered[2..] {
8953            assert!(region.latency_ms.is_none());
8954        }
8955    }
8956
8957    #[test]
8958    fn test_regions_without_latency_sorted_as_1000ms() {
8959        let mut app = test_app();
8960
8961        // Add one region with 1500ms (slower than default 1000ms)
8962        app.region_latencies
8963            .insert("ap-southeast-2".to_string(), 1500);
8964        // Add one region with 50ms (faster)
8965        app.region_latencies.insert("us-east-1".to_string(), 50);
8966
8967        let filtered = app.get_filtered_regions();
8968
8969        // Fast region first
8970        assert_eq!(filtered[0].code, "us-east-1");
8971        assert_eq!(filtered[0].latency_ms, Some(50));
8972
8973        // Regions without latency (treated as 1000ms) come before 1500ms
8974        let slow_region_idx = filtered
8975            .iter()
8976            .position(|r| r.code == "ap-southeast-2")
8977            .unwrap();
8978        assert!(slow_region_idx > 1); // Should be after fast region and regions without latency
8979
8980        // All regions between index 1 and slow_region_idx should have no latency
8981        for region in filtered.iter().take(slow_region_idx).skip(1) {
8982            assert!(region.latency_ms.is_none());
8983        }
8984    }
8985
8986    #[test]
8987    fn test_region_picker_opens_with_latencies() {
8988        let mut app = test_app();
8989
8990        // Simulate opening region picker
8991        app.region_filter.clear();
8992        app.region_picker_selected = 0;
8993        app.measure_region_latencies();
8994
8995        // Should have attempted to measure (even if all fail in test env)
8996        // The map should be initialized
8997        assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
8998    }
8999
9000    #[test]
9001    fn test_region_picker_enter_in_filter_mode_exits_insert_not_selects() {
9002        // Regression: pressing Enter while filter is active (INSERT mode) must
9003        // exit INSERT mode and move focus to the table — NOT select the first region.
9004        // This matches ServicePicker behavior.
9005        let mut app = test_app();
9006        app.mode = Mode::RegionPicker;
9007        app.region_filter_active = true; // INSERT mode — typing in filter
9008        app.region_filter = "us".to_string();
9009        let original_region = app.region.clone();
9010
9011        // Enter must exit INSERT mode, not select
9012        app.handle_action(Action::Select);
9013
9014        assert!(
9015            !app.region_filter_active,
9016            "Filter must be deactivated (switch to NORMAL/table focus)"
9017        );
9018        assert_eq!(
9019            app.mode,
9020            Mode::RegionPicker,
9021            "Must stay in RegionPicker, not close it"
9022        );
9023        assert_eq!(
9024            app.region, original_region,
9025            "Region must NOT change when Enter is pressed in INSERT mode"
9026        );
9027    }
9028
9029    #[test]
9030    fn test_region_picker_enter_in_normal_mode_selects_region() {
9031        // Second Enter (when filter is NOT active) must select the region.
9032        let mut app = test_app();
9033        app.mode = Mode::RegionPicker;
9034        app.region_filter_active = false; // NORMAL mode — table focused
9035        app.region_filter.clear();
9036
9037        app.handle_action(Action::Select);
9038
9039        // Should have selected and closed picker (mode changed)
9040        assert_eq!(
9041            app.mode,
9042            Mode::Normal,
9043            "Must close picker after selecting region in NORMAL mode"
9044        );
9045    }
9046
9047    #[test]
9048    fn test_region_filter_can_type_any_character_in_insert_mode() {
9049        // Regression: 'k' and 'j' couldn't be typed in the region filter because the
9050        // keymap maps them to PrevItem/NextItem in RegionPicker mode, but they should
9051        // go to the filter when region_filter_active is true.
9052        use crate::keymap::{handle_key, Mode as KMode};
9053        use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
9054
9055        let mut app = test_app();
9056        app.mode = Mode::RegionPicker;
9057        app.region_filter_active = true;
9058        app.region_filter.clear();
9059
9060        // Simulate actual keypresses through keymap
9061        let t_key = KeyEvent::new(KeyCode::Char('t'), KeyModifiers::NONE);
9062        let o_key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
9063        let k_key = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE);
9064
9065        // These go through keymap → handle_action
9066        if let Some(action) = handle_key(t_key, KMode::RegionPicker) {
9067            app.handle_action(action);
9068        }
9069        if let Some(action) = handle_key(o_key, KMode::RegionPicker) {
9070            app.handle_action(action);
9071        }
9072        if let Some(action) = handle_key(k_key, KMode::RegionPicker) {
9073            app.handle_action(action);
9074        }
9075
9076        assert_eq!(
9077            app.region_filter, "tok",
9078            "'k' must be typed into filter (not navigate up) when filter is active"
9079        );
9080    }
9081
9082    #[test]
9083    fn test_ecr_tab_next() {
9084        assert_eq!(EcrTab::Private.next(), EcrTab::Public);
9085        assert_eq!(EcrTab::Public.next(), EcrTab::Private);
9086    }
9087
9088    #[test]
9089    fn test_ecr_tab_switching() {
9090        let mut app = test_app();
9091        app.current_service = Service::EcrRepositories;
9092        app.service_selected = true;
9093        app.ecr_state.tab = EcrTab::Private;
9094
9095        app.handle_action(Action::NextDetailTab);
9096        assert_eq!(app.ecr_state.tab, EcrTab::Public);
9097        assert_eq!(app.ecr_state.repositories.selected, 0);
9098
9099        app.handle_action(Action::NextDetailTab);
9100        assert_eq!(app.ecr_state.tab, EcrTab::Private);
9101    }
9102
9103    #[test]
9104    fn test_ecr_navigation() {
9105        let mut app = test_app();
9106        app.current_service = Service::EcrRepositories;
9107        app.service_selected = true;
9108        app.mode = Mode::Normal;
9109        app.ecr_state.repositories.items = vec![
9110            EcrRepository {
9111                name: "repo1".to_string(),
9112                uri: "uri1".to_string(),
9113                created_at: "2023-01-01".to_string(),
9114                tag_immutability: "MUTABLE".to_string(),
9115                encryption_type: "AES256".to_string(),
9116            },
9117            EcrRepository {
9118                name: "repo2".to_string(),
9119                uri: "uri2".to_string(),
9120                created_at: "2023-01-02".to_string(),
9121                tag_immutability: "IMMUTABLE".to_string(),
9122                encryption_type: "KMS".to_string(),
9123            },
9124        ];
9125
9126        app.handle_action(Action::NextItem);
9127        assert_eq!(app.ecr_state.repositories.selected, 1);
9128
9129        app.handle_action(Action::PrevItem);
9130        assert_eq!(app.ecr_state.repositories.selected, 0);
9131    }
9132
9133    #[test]
9134    fn test_ecr_filter() {
9135        let mut app = test_app();
9136        app.current_service = Service::EcrRepositories;
9137        app.service_selected = true;
9138        app.ecr_state.repositories.items = vec![
9139            EcrRepository {
9140                name: "my-app".to_string(),
9141                uri: "uri1".to_string(),
9142                created_at: "2023-01-01".to_string(),
9143                tag_immutability: "MUTABLE".to_string(),
9144                encryption_type: "AES256".to_string(),
9145            },
9146            EcrRepository {
9147                name: "other-service".to_string(),
9148                uri: "uri2".to_string(),
9149                created_at: "2023-01-02".to_string(),
9150                tag_immutability: "IMMUTABLE".to_string(),
9151                encryption_type: "KMS".to_string(),
9152            },
9153        ];
9154
9155        app.ecr_state.repositories.filter = "app".to_string();
9156        let filtered = filtered_ecr_repositories(&app);
9157        assert_eq!(filtered.len(), 1);
9158        assert_eq!(filtered[0].name, "my-app");
9159    }
9160
9161    #[test]
9162    fn test_ecr_filter_input() {
9163        let mut app = test_app();
9164        app.current_service = Service::EcrRepositories;
9165        app.service_selected = true;
9166        app.mode = Mode::FilterInput;
9167
9168        app.handle_action(Action::FilterInput('t'));
9169        app.handle_action(Action::FilterInput('e'));
9170        app.handle_action(Action::FilterInput('s'));
9171        app.handle_action(Action::FilterInput('t'));
9172        assert_eq!(app.ecr_state.repositories.filter, "test");
9173
9174        app.handle_action(Action::FilterBackspace);
9175        assert_eq!(app.ecr_state.repositories.filter, "tes");
9176    }
9177
9178    #[test]
9179    fn test_ecr_filter_resets_selection() {
9180        let mut app = test_app();
9181        app.current_service = Service::EcrRepositories;
9182        app.service_selected = true;
9183        app.mode = Mode::FilterInput;
9184        app.ecr_state.repositories.items = vec![
9185            EcrRepository {
9186                name: "repo1".to_string(),
9187                uri: "uri1".to_string(),
9188                created_at: "2023-01-01".to_string(),
9189                tag_immutability: "MUTABLE".to_string(),
9190                encryption_type: "AES256".to_string(),
9191            },
9192            EcrRepository {
9193                name: "repo2".to_string(),
9194                uri: "uri2".to_string(),
9195                created_at: "2023-01-02".to_string(),
9196                tag_immutability: "IMMUTABLE".to_string(),
9197                encryption_type: "KMS".to_string(),
9198            },
9199            EcrRepository {
9200                name: "repo3".to_string(),
9201                uri: "uri3".to_string(),
9202                created_at: "2023-01-03".to_string(),
9203                tag_immutability: "MUTABLE".to_string(),
9204                encryption_type: "AES256".to_string(),
9205            },
9206        ];
9207
9208        // Move selection to second item
9209        app.ecr_state.repositories.selected = 2;
9210        assert_eq!(app.ecr_state.repositories.selected, 2);
9211
9212        // Apply filter - selection should reset to 0
9213        app.handle_action(Action::FilterInput('t'));
9214        assert_eq!(app.ecr_state.repositories.filter, "t");
9215        assert_eq!(app.ecr_state.repositories.selected, 0);
9216
9217        // Move selection again
9218        app.ecr_state.repositories.selected = 1;
9219
9220        // Backspace filter - selection should reset to 0
9221        app.handle_action(Action::FilterBackspace);
9222        assert_eq!(app.ecr_state.repositories.filter, "");
9223        assert_eq!(app.ecr_state.repositories.selected, 0);
9224    }
9225
9226    #[test]
9227    fn test_ecr_images_filter_resets_selection() {
9228        let mut app = test_app();
9229        app.current_service = Service::EcrRepositories;
9230        app.service_selected = true;
9231        app.mode = Mode::FilterInput;
9232        app.ecr_state.current_repository = Some("test-repo".to_string());
9233        app.ecr_state.images.items = vec![
9234            EcrImage {
9235                tag: "v1.0.0".to_string(),
9236                artifact_type: "container".to_string(),
9237                digest: "sha256:abc123".to_string(),
9238                pushed_at: "2023-01-01".to_string(),
9239                size_bytes: 1000,
9240                uri: "uri1".to_string(),
9241                last_pull_time: "".to_string(),
9242            },
9243            EcrImage {
9244                tag: "v2.0.0".to_string(),
9245                artifact_type: "container".to_string(),
9246                digest: "sha256:def456".to_string(),
9247                pushed_at: "2023-01-02".to_string(),
9248                size_bytes: 2000,
9249                uri: "uri2".to_string(),
9250                last_pull_time: "".to_string(),
9251            },
9252        ];
9253
9254        // Move selection to second item
9255        app.ecr_state.images.selected = 1;
9256        assert_eq!(app.ecr_state.images.selected, 1);
9257
9258        // Apply filter - selection should reset to 0
9259        app.handle_action(Action::FilterInput('v'));
9260        assert_eq!(app.ecr_state.images.filter, "v");
9261        assert_eq!(app.ecr_state.images.selected, 0);
9262    }
9263
9264    #[test]
9265    fn test_iam_users_filter_input() {
9266        let mut app = test_app();
9267        app.current_service = Service::IamUsers;
9268        app.service_selected = true;
9269        app.mode = Mode::FilterInput;
9270
9271        app.handle_action(Action::FilterInput('a'));
9272        app.handle_action(Action::FilterInput('d'));
9273        app.handle_action(Action::FilterInput('m'));
9274        app.handle_action(Action::FilterInput('i'));
9275        app.handle_action(Action::FilterInput('n'));
9276        assert_eq!(app.iam_state.users.filter, "admin");
9277
9278        app.handle_action(Action::FilterBackspace);
9279        assert_eq!(app.iam_state.users.filter, "admi");
9280    }
9281
9282    #[test]
9283    fn test_iam_policies_filter_input() {
9284        let mut app = test_app();
9285        app.current_service = Service::IamUsers;
9286        app.service_selected = true;
9287        app.iam_state.current_user = Some("testuser".to_string());
9288        app.mode = Mode::FilterInput;
9289
9290        app.handle_action(Action::FilterInput('r'));
9291        app.handle_action(Action::FilterInput('e'));
9292        app.handle_action(Action::FilterInput('a'));
9293        app.handle_action(Action::FilterInput('d'));
9294        assert_eq!(app.iam_state.policies.filter, "read");
9295
9296        app.handle_action(Action::FilterBackspace);
9297        assert_eq!(app.iam_state.policies.filter, "rea");
9298    }
9299
9300    #[test]
9301    fn test_iam_start_filter() {
9302        let mut app = test_app();
9303        app.current_service = Service::IamUsers;
9304        app.service_selected = true;
9305        app.mode = Mode::Normal;
9306
9307        app.handle_action(Action::StartFilter);
9308        assert_eq!(app.mode, Mode::FilterInput);
9309    }
9310
9311    #[test]
9312    fn test_iam_roles_filter_input() {
9313        let mut app = test_app();
9314        app.current_service = Service::IamRoles;
9315        app.service_selected = true;
9316        app.mode = Mode::FilterInput;
9317
9318        app.handle_action(Action::FilterInput('a'));
9319        app.handle_action(Action::FilterInput('d'));
9320        app.handle_action(Action::FilterInput('m'));
9321        app.handle_action(Action::FilterInput('i'));
9322        app.handle_action(Action::FilterInput('n'));
9323        assert_eq!(app.iam_state.roles.filter, "admin");
9324
9325        app.handle_action(Action::FilterBackspace);
9326        assert_eq!(app.iam_state.roles.filter, "admi");
9327    }
9328
9329    #[test]
9330    fn test_iam_roles_start_filter() {
9331        let mut app = test_app();
9332        app.current_service = Service::IamRoles;
9333        app.service_selected = true;
9334        app.mode = Mode::Normal;
9335
9336        app.handle_action(Action::StartFilter);
9337        assert_eq!(app.mode, Mode::FilterInput);
9338    }
9339
9340    #[test]
9341    fn test_iam_roles_navigation() {
9342        let mut app = test_app();
9343        app.current_service = Service::IamRoles;
9344        app.service_selected = true;
9345        app.mode = Mode::Normal;
9346        app.iam_state.roles.items = (0..10)
9347            .map(|i| IamRole {
9348                role_name: format!("role{}", i),
9349                path: "/".to_string(),
9350                trusted_entities: String::new(),
9351                last_activity: String::new(),
9352                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
9353                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9354                description: String::new(),
9355                max_session_duration: Some(3600),
9356            })
9357            .collect();
9358
9359        assert_eq!(app.iam_state.roles.selected, 0);
9360
9361        app.handle_action(Action::NextItem);
9362        assert_eq!(app.iam_state.roles.selected, 1);
9363
9364        app.handle_action(Action::NextItem);
9365        assert_eq!(app.iam_state.roles.selected, 2);
9366
9367        app.handle_action(Action::PrevItem);
9368        assert_eq!(app.iam_state.roles.selected, 1);
9369    }
9370
9371    #[test]
9372    fn test_iam_roles_page_hotkey() {
9373        let mut app = test_app();
9374        app.current_service = Service::IamRoles;
9375        app.service_selected = true;
9376        app.mode = Mode::Normal;
9377        app.iam_state.roles.page_size = PageSize::Ten;
9378        app.iam_state.roles.items = (0..100)
9379            .map(|i| IamRole {
9380                role_name: format!("role{}", i),
9381                path: "/".to_string(),
9382                trusted_entities: String::new(),
9383                last_activity: String::new(),
9384                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
9385                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9386                description: String::new(),
9387                max_session_duration: Some(3600),
9388            })
9389            .collect();
9390
9391        app.handle_action(Action::FilterInput('2'));
9392        app.handle_action(Action::OpenColumnSelector);
9393        assert_eq!(app.iam_state.roles.selected, 10); // Page 2 = index 10 (with page size 10)
9394    }
9395
9396    #[test]
9397    fn test_iam_users_page_hotkey() {
9398        let mut app = test_app();
9399        app.current_service = Service::IamUsers;
9400        app.service_selected = true;
9401        app.mode = Mode::Normal;
9402        app.iam_state.users.page_size = PageSize::Ten;
9403        app.iam_state.users.items = (0..100)
9404            .map(|i| IamUser {
9405                user_name: format!("user{}", i),
9406                path: "/".to_string(),
9407                groups: String::new(),
9408                last_activity: String::new(),
9409                mfa: String::new(),
9410                password_age: String::new(),
9411                console_last_sign_in: String::new(),
9412                access_key_id: String::new(),
9413                active_key_age: String::new(),
9414                access_key_last_used: String::new(),
9415                arn: format!("arn:aws:iam::123456789012:user/user{}", i),
9416                creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9417                console_access: String::new(),
9418                signing_certs: String::new(),
9419            })
9420            .collect();
9421
9422        app.handle_action(Action::FilterInput('3'));
9423        app.handle_action(Action::OpenColumnSelector);
9424        assert_eq!(app.iam_state.users.selected, 20); // Page 3 = index 20 (with page size 10)
9425    }
9426
9427    #[test]
9428    fn test_ecr_scroll_navigation() {
9429        let mut app = test_app();
9430        app.current_service = Service::EcrRepositories;
9431        app.service_selected = true;
9432        app.ecr_state.repositories.items = (0..20)
9433            .map(|i| EcrRepository {
9434                name: format!("repo{}", i),
9435                uri: format!("uri{}", i),
9436                created_at: "2023-01-01".to_string(),
9437                tag_immutability: "MUTABLE".to_string(),
9438                encryption_type: "AES256".to_string(),
9439            })
9440            .collect();
9441
9442        app.handle_action(Action::ScrollDown);
9443        assert_eq!(app.ecr_state.repositories.selected, 10);
9444
9445        app.handle_action(Action::ScrollUp);
9446        assert_eq!(app.ecr_state.repositories.selected, 0);
9447    }
9448
9449    #[test]
9450    fn test_ecr_tab_switching_triggers_reload() {
9451        let mut app = test_app();
9452        app.current_service = Service::EcrRepositories;
9453        app.service_selected = true;
9454        app.ecr_state.tab = EcrTab::Private;
9455        app.ecr_state.repositories.loading = false;
9456        app.ecr_state.repositories.items = vec![EcrRepository {
9457            name: "private-repo".to_string(),
9458            uri: "uri".to_string(),
9459            created_at: "2023-01-01".to_string(),
9460            tag_immutability: "MUTABLE".to_string(),
9461            encryption_type: "AES256".to_string(),
9462        }];
9463
9464        app.handle_action(Action::NextDetailTab);
9465        assert_eq!(app.ecr_state.tab, EcrTab::Public);
9466        assert!(app.ecr_state.repositories.loading);
9467        assert_eq!(app.ecr_state.repositories.selected, 0);
9468    }
9469
9470    #[test]
9471    fn test_ecr_tab_cycles_between_private_and_public() {
9472        let mut app = test_app();
9473        app.current_service = Service::EcrRepositories;
9474        app.service_selected = true;
9475        app.ecr_state.tab = EcrTab::Private;
9476
9477        app.handle_action(Action::NextDetailTab);
9478        assert_eq!(app.ecr_state.tab, EcrTab::Public);
9479
9480        app.handle_action(Action::NextDetailTab);
9481        assert_eq!(app.ecr_state.tab, EcrTab::Private);
9482    }
9483
9484    #[test]
9485    fn test_page_size_values() {
9486        assert_eq!(PageSize::Ten.value(), 10);
9487        assert_eq!(PageSize::TwentyFive.value(), 25);
9488        assert_eq!(PageSize::Fifty.value(), 50);
9489        assert_eq!(PageSize::OneHundred.value(), 100);
9490    }
9491
9492    #[test]
9493    fn test_page_size_next() {
9494        assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
9495        assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
9496        assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
9497        assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
9498    }
9499
9500    #[test]
9501    fn test_ecr_enter_drills_into_repository() {
9502        let mut app = test_app();
9503        app.current_service = Service::EcrRepositories;
9504        app.service_selected = true;
9505        app.mode = Mode::Normal;
9506        app.ecr_state.repositories.items = vec![EcrRepository {
9507            name: "my-repo".to_string(),
9508            uri: "uri".to_string(),
9509            created_at: "2023-01-01".to_string(),
9510            tag_immutability: "MUTABLE".to_string(),
9511            encryption_type: "AES256".to_string(),
9512        }];
9513
9514        app.handle_action(Action::Select);
9515        assert_eq!(
9516            app.ecr_state.current_repository,
9517            Some("my-repo".to_string())
9518        );
9519        assert!(app.ecr_state.repositories.loading);
9520    }
9521
9522    #[test]
9523    fn test_ecr_repository_expansion() {
9524        let mut app = test_app();
9525        app.current_service = Service::EcrRepositories;
9526        app.service_selected = true;
9527        app.ecr_state.repositories.items = vec![EcrRepository {
9528            name: "my-repo".to_string(),
9529            uri: "uri".to_string(),
9530            created_at: "2023-01-01".to_string(),
9531            tag_immutability: "MUTABLE".to_string(),
9532            encryption_type: "AES256".to_string(),
9533        }];
9534        app.ecr_state.repositories.selected = 0;
9535
9536        assert_eq!(app.ecr_state.repositories.expanded_item, None);
9537
9538        app.handle_action(Action::NextPane);
9539        assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
9540
9541        app.handle_action(Action::PrevPane);
9542        assert_eq!(app.ecr_state.repositories.expanded_item, None);
9543    }
9544
9545    #[test]
9546    fn test_ecr_ctrl_d_scrolls_down() {
9547        let mut app = test_app();
9548        app.current_service = Service::EcrRepositories;
9549        app.service_selected = true;
9550        app.mode = Mode::Normal;
9551        app.ecr_state.repositories.items = (0..30)
9552            .map(|i| EcrRepository {
9553                name: format!("repo{}", i),
9554                uri: format!("uri{}", i),
9555                created_at: "2023-01-01".to_string(),
9556                tag_immutability: "MUTABLE".to_string(),
9557                encryption_type: "AES256".to_string(),
9558            })
9559            .collect();
9560        app.ecr_state.repositories.selected = 0;
9561
9562        app.handle_action(Action::PageDown);
9563        assert_eq!(app.ecr_state.repositories.selected, 10);
9564    }
9565
9566    #[test]
9567    fn test_ecr_ctrl_u_scrolls_up() {
9568        let mut app = test_app();
9569        app.current_service = Service::EcrRepositories;
9570        app.service_selected = true;
9571        app.mode = Mode::Normal;
9572        app.ecr_state.repositories.items = (0..30)
9573            .map(|i| EcrRepository {
9574                name: format!("repo{}", i),
9575                uri: format!("uri{}", i),
9576                created_at: "2023-01-01".to_string(),
9577                tag_immutability: "MUTABLE".to_string(),
9578                encryption_type: "AES256".to_string(),
9579            })
9580            .collect();
9581        app.ecr_state.repositories.selected = 15;
9582
9583        app.handle_action(Action::PageUp);
9584        assert_eq!(app.ecr_state.repositories.selected, 5);
9585    }
9586
9587    #[test]
9588    fn test_ecr_images_ctrl_d_scrolls_down() {
9589        let mut app = test_app();
9590        app.current_service = Service::EcrRepositories;
9591        app.service_selected = true;
9592        app.mode = Mode::Normal;
9593        app.ecr_state.current_repository = Some("repo".to_string());
9594        app.ecr_state.images.items = (0..30)
9595            .map(|i| EcrImage {
9596                tag: format!("tag{}", i),
9597                artifact_type: "container".to_string(),
9598                pushed_at: "2023-01-01T12:00:00Z".to_string(),
9599                size_bytes: 104857600,
9600                uri: format!("uri{}", i),
9601                digest: format!("sha256:{}", i),
9602                last_pull_time: String::new(),
9603            })
9604            .collect();
9605        app.ecr_state.images.selected = 0;
9606
9607        app.handle_action(Action::PageDown);
9608        assert_eq!(app.ecr_state.images.selected, 10);
9609    }
9610
9611    #[test]
9612    fn test_ecr_esc_goes_back_from_images_to_repos() {
9613        let mut app = test_app();
9614        app.current_service = Service::EcrRepositories;
9615        app.service_selected = true;
9616        app.mode = Mode::Normal;
9617        app.ecr_state.current_repository = Some("my-repo".to_string());
9618        app.ecr_state.images.items = vec![EcrImage {
9619            tag: "latest".to_string(),
9620            artifact_type: "container".to_string(),
9621            pushed_at: "2023-01-01T12:00:00Z".to_string(),
9622            size_bytes: 104857600,
9623            uri: "uri".to_string(),
9624            digest: "sha256:abc".to_string(),
9625            last_pull_time: String::new(),
9626        }];
9627
9628        app.handle_action(Action::GoBack);
9629        assert_eq!(app.ecr_state.current_repository, None);
9630        assert!(app.ecr_state.images.items.is_empty());
9631    }
9632
9633    #[test]
9634    fn test_ecr_esc_collapses_expanded_image_first() {
9635        let mut app = test_app();
9636        app.current_service = Service::EcrRepositories;
9637        app.service_selected = true;
9638        app.mode = Mode::Normal;
9639        app.ecr_state.current_repository = Some("my-repo".to_string());
9640        app.ecr_state.images.expanded_item = Some(0);
9641
9642        app.handle_action(Action::GoBack);
9643        assert_eq!(app.ecr_state.images.expanded_item, None);
9644        assert_eq!(
9645            app.ecr_state.current_repository,
9646            Some("my-repo".to_string())
9647        );
9648    }
9649
9650    #[test]
9651    fn test_pagination_with_lowercase_p() {
9652        let mut app = test_app();
9653        app.current_service = Service::EcrRepositories;
9654        app.service_selected = true;
9655        app.mode = Mode::Normal;
9656        app.ecr_state.repositories.items = (0..100)
9657            .map(|i| EcrRepository {
9658                name: format!("repo{}", i),
9659                uri: format!("uri{}", i),
9660                created_at: "2023-01-01".to_string(),
9661                tag_immutability: "MUTABLE".to_string(),
9662                encryption_type: "AES256".to_string(),
9663            })
9664            .collect();
9665
9666        // Type "2" then "p" to go to page 2
9667        app.handle_action(Action::FilterInput('2'));
9668        assert_eq!(app.page_input, "2");
9669
9670        app.handle_action(Action::OpenColumnSelector); // 'p' key
9671        assert_eq!(app.ecr_state.repositories.selected, 50); // Page 2 starts at index 50
9672        assert_eq!(app.page_input, ""); // Should be cleared
9673    }
9674
9675    #[test]
9676    fn test_lowercase_p_without_number_opens_preferences() {
9677        let mut app = test_app();
9678        app.current_service = Service::EcrRepositories;
9679        app.service_selected = true;
9680        app.mode = Mode::Normal;
9681
9682        app.handle_action(Action::OpenColumnSelector); // 'p' key without number
9683        assert_eq!(app.mode, Mode::ColumnSelector);
9684    }
9685
9686    #[test]
9687    fn test_ctrl_o_generates_correct_console_url() {
9688        let mut app = test_app();
9689        app.current_service = Service::EcrRepositories;
9690        app.service_selected = true;
9691        app.mode = Mode::Normal;
9692        app.config.account_id = "123456789012".to_string();
9693
9694        // Test repository list URL
9695        let url = app.get_console_url();
9696        assert!(url.contains("ecr/private-registry/repositories"));
9697        assert!(url.contains("region=us-east-1"));
9698
9699        // Test images URL
9700        app.ecr_state.current_repository = Some("my-repo".to_string());
9701        let url = app.get_console_url();
9702        assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
9703        assert!(url.contains("region=us-east-1"));
9704    }
9705
9706    #[test]
9707    fn test_page_input_display_and_reset() {
9708        let mut app = test_app();
9709        app.current_service = Service::EcrRepositories;
9710        app.service_selected = true;
9711        app.mode = Mode::Normal;
9712        app.ecr_state.repositories.items = (0..100)
9713            .map(|i| EcrRepository {
9714                name: format!("repo{}", i),
9715                uri: format!("uri{}", i),
9716                created_at: "2023-01-01".to_string(),
9717                tag_immutability: "MUTABLE".to_string(),
9718                encryption_type: "AES256".to_string(),
9719            })
9720            .collect();
9721
9722        // Type "2"
9723        app.handle_action(Action::FilterInput('2'));
9724        assert_eq!(app.page_input, "2");
9725
9726        // Press 'p' to go to page 2
9727        app.handle_action(Action::OpenColumnSelector);
9728        assert_eq!(app.page_input, ""); // Should be cleared
9729        assert_eq!(app.ecr_state.repositories.selected, 50); // Page 2 starts at index 50
9730    }
9731
9732    #[test]
9733    fn test_page_navigation_updates_scroll_offset_for_cfn() {
9734        let mut app = test_app();
9735        app.current_service = Service::CloudFormationStacks;
9736        app.service_selected = true;
9737        app.mode = Mode::Normal;
9738        app.cfn_state.table.items = (0..100)
9739            .map(|i| CfnStack {
9740                name: format!("stack-{}", i),
9741                stack_id: format!(
9742                    "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
9743                    i
9744                ),
9745                status: "CREATE_COMPLETE".to_string(),
9746                created_time: "2023-01-01T00:00:00Z".to_string(),
9747                updated_time: "2023-01-01T00:00:00Z".to_string(),
9748                deleted_time: String::new(),
9749                drift_status: "IN_SYNC".to_string(),
9750                last_drift_check_time: String::new(),
9751                status_reason: String::new(),
9752                description: String::new(),
9753                detailed_status: String::new(),
9754                root_stack: String::new(),
9755                parent_stack: String::new(),
9756                termination_protection: false,
9757                iam_role: String::new(),
9758                tags: vec![],
9759                stack_policy: String::new(),
9760                rollback_monitoring_time: String::new(),
9761                rollback_alarms: vec![],
9762                notification_arns: vec![],
9763            })
9764            .collect();
9765
9766        // Type "2" then "p" to go to page 2
9767        app.handle_action(Action::FilterInput('2'));
9768        assert_eq!(app.page_input, "2");
9769
9770        app.handle_action(Action::OpenColumnSelector); // 'p' key
9771        assert_eq!(app.page_input, ""); // Should be cleared
9772
9773        // Verify both selected and scroll_offset are updated
9774        let page_size = app.cfn_state.table.page_size.value();
9775        let expected_offset = page_size; // Page 2 starts at page_size
9776        assert_eq!(app.cfn_state.table.selected, expected_offset);
9777        assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
9778
9779        // Verify pagination display shows page 2
9780        let current_page = app.cfn_state.table.scroll_offset / page_size;
9781        assert_eq!(
9782            current_page, 1,
9783            "2p should go to page 2 (0-indexed as 1), not page 3"
9784        ); // 0-indexed, so page 2 is index 1
9785    }
9786
9787    #[test]
9788    fn test_3p_goes_to_page_3_not_page_5() {
9789        let mut app = test_app();
9790        app.current_service = Service::CloudFormationStacks;
9791        app.service_selected = true;
9792        app.mode = Mode::Normal;
9793        app.cfn_state.table.items = (0..200)
9794            .map(|i| CfnStack {
9795                name: format!("stack-{}", i),
9796                stack_id: format!(
9797                    "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
9798                    i
9799                ),
9800                status: "CREATE_COMPLETE".to_string(),
9801                created_time: "2023-01-01T00:00:00Z".to_string(),
9802                updated_time: "2023-01-01T00:00:00Z".to_string(),
9803                deleted_time: String::new(),
9804                drift_status: "IN_SYNC".to_string(),
9805                last_drift_check_time: String::new(),
9806                status_reason: String::new(),
9807                description: String::new(),
9808                detailed_status: String::new(),
9809                root_stack: String::new(),
9810                parent_stack: String::new(),
9811                termination_protection: false,
9812                iam_role: String::new(),
9813                tags: vec![],
9814                stack_policy: String::new(),
9815                rollback_monitoring_time: String::new(),
9816                rollback_alarms: vec![],
9817                notification_arns: vec![],
9818            })
9819            .collect();
9820
9821        // Type "3" then "p" to go to page 3
9822        app.handle_action(Action::FilterInput('3'));
9823        app.handle_action(Action::OpenColumnSelector);
9824
9825        let page_size = app.cfn_state.table.page_size.value();
9826        let current_page = app.cfn_state.table.scroll_offset / page_size;
9827        assert_eq!(
9828            current_page, 2,
9829            "3p should go to page 3 (0-indexed as 2), not page 5"
9830        );
9831        assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
9832    }
9833
9834    #[test]
9835    fn test_log_streams_page_navigation_uses_correct_page_size() {
9836        let mut app = test_app();
9837        app.current_service = Service::CloudWatchLogGroups;
9838        app.view_mode = ViewMode::Detail;
9839        app.service_selected = true;
9840        app.mode = Mode::Normal;
9841        app.log_groups_state.log_streams = (0..100)
9842            .map(|i| LogStream {
9843                name: format!("stream-{}", i),
9844                creation_time: None,
9845                last_event_time: None,
9846            })
9847            .collect();
9848
9849        // Type "2" then "p" to go to page 2
9850        app.handle_action(Action::FilterInput('2'));
9851        app.handle_action(Action::OpenColumnSelector);
9852
9853        // Should go to page 2 (page index 1)
9854        assert_eq!(app.log_groups_state.stream_current_page, 1);
9855        assert_eq!(app.log_groups_state.selected_stream, 0);
9856
9857        // Verify pagination display shows page 2 (not page 3)
9858        assert_eq!(
9859            app.log_groups_state.stream_current_page, 1,
9860            "2p should go to page 2 (0-indexed as 1), not page 3"
9861        );
9862    }
9863
9864    #[test]
9865    fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
9866        let mut app = test_app();
9867        app.current_service = Service::EcrRepositories;
9868        app.service_selected = true;
9869        app.mode = Mode::Normal;
9870        app.ecr_state.repositories.page_size = PageSize::TwentyFive; // Set to 25
9871        app.ecr_state.repositories.items = (0..100)
9872            .map(|i| EcrRepository {
9873                name: format!("repo{}", i),
9874                uri: format!("uri{}", i),
9875                created_at: "2023-01-01".to_string(),
9876                tag_immutability: "MUTABLE".to_string(),
9877                encryption_type: "AES256".to_string(),
9878            })
9879            .collect();
9880
9881        // Type "3" then "p" to go to page 3
9882        app.handle_action(Action::FilterInput('3'));
9883        app.handle_action(Action::OpenColumnSelector);
9884
9885        // With page_size=25, page 3 starts at index 50
9886        assert_eq!(app.ecr_state.repositories.selected, 50);
9887
9888        let page_size = app.ecr_state.repositories.page_size.value();
9889        let current_page = app.ecr_state.repositories.selected / page_size;
9890        assert_eq!(
9891            current_page, 2,
9892            "3p with page_size=25 should go to page 3 (0-indexed as 2)"
9893        );
9894    }
9895
9896    #[test]
9897    fn test_page_navigation_updates_scroll_offset_for_alarms() {
9898        let mut app = test_app();
9899        app.current_service = Service::CloudWatchAlarms;
9900        app.service_selected = true;
9901        app.mode = Mode::Normal;
9902        app.alarms_state.table.items = (0..100)
9903            .map(|i| Alarm {
9904                name: format!("alarm-{}", i),
9905                state: "OK".to_string(),
9906                state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
9907                description: String::new(),
9908                metric_name: "CPUUtilization".to_string(),
9909                namespace: "AWS/EC2".to_string(),
9910                statistic: "Average".to_string(),
9911                period: 300,
9912                comparison_operator: "GreaterThanThreshold".to_string(),
9913                threshold: 80.0,
9914                actions_enabled: true,
9915                state_reason: String::new(),
9916                resource: String::new(),
9917                dimensions: String::new(),
9918                expression: String::new(),
9919                alarm_type: "MetricAlarm".to_string(),
9920                cross_account: String::new(),
9921                ..Default::default()
9922            })
9923            .collect();
9924
9925        // Type "2" then "p" to go to page 2
9926        app.handle_action(Action::FilterInput('2'));
9927        app.handle_action(Action::OpenColumnSelector);
9928
9929        // Verify both selected and scroll_offset are updated
9930        let page_size = app.alarms_state.table.page_size.value();
9931        let expected_offset = page_size; // Page 2 starts at page_size
9932        assert_eq!(app.alarms_state.table.selected, expected_offset);
9933        assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
9934    }
9935
9936    #[test]
9937    fn test_ecr_pagination_with_65_repos() {
9938        let mut app = test_app();
9939        app.current_service = Service::EcrRepositories;
9940        app.service_selected = true;
9941        app.mode = Mode::Normal;
9942        app.ecr_state.repositories.items = (0..65)
9943            .map(|i| EcrRepository {
9944                name: format!("repo{:02}", i),
9945                uri: format!("uri{}", i),
9946                created_at: "2023-01-01".to_string(),
9947                tag_immutability: "MUTABLE".to_string(),
9948                encryption_type: "AES256".to_string(),
9949            })
9950            .collect();
9951
9952        // Page 1: items 0-49 (50 items)
9953        assert_eq!(app.ecr_state.repositories.selected, 0);
9954        let page_size = 50;
9955        let current_page = app.ecr_state.repositories.selected / page_size;
9956        assert_eq!(current_page, 0);
9957
9958        // Go to page 2
9959        app.handle_action(Action::FilterInput('2'));
9960        app.handle_action(Action::OpenColumnSelector);
9961        assert_eq!(app.ecr_state.repositories.selected, 50);
9962
9963        // Page 2: items 50-64 (15 items)
9964        let current_page = app.ecr_state.repositories.selected / page_size;
9965        assert_eq!(current_page, 1);
9966    }
9967
9968    #[test]
9969    fn test_ecr_repos_input_focus_tab_cycling() {
9970        let mut app = test_app();
9971        app.current_service = Service::EcrRepositories;
9972        app.service_selected = true;
9973        app.mode = Mode::FilterInput;
9974        app.ecr_state.input_focus = InputFocus::Filter;
9975
9976        // Tab should cycle to Pagination
9977        app.handle_action(Action::NextFilterFocus);
9978        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
9979
9980        // Tab again should cycle back to Input
9981        app.handle_action(Action::NextFilterFocus);
9982        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
9983
9984        // Shift+Tab should cycle backwards to Pagination
9985        app.handle_action(Action::PrevFilterFocus);
9986        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
9987
9988        // Shift+Tab again should cycle back to Input
9989        app.handle_action(Action::PrevFilterFocus);
9990        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
9991    }
9992
9993    #[test]
9994    fn test_ecr_images_column_toggle_not_off_by_one() {
9995        use crate::ecr::image::Column as ImageColumn;
9996        let mut app = test_app();
9997        app.current_service = Service::EcrRepositories;
9998        app.service_selected = true;
9999        app.mode = Mode::ColumnSelector;
10000        app.ecr_state.current_repository = Some("test-repo".to_string());
10001
10002        // Start with all columns visible
10003        app.ecr_image_visible_column_ids = ImageColumn::ids();
10004        let initial_count = app.ecr_image_visible_column_ids.len();
10005
10006        // Select first column (index 0) and toggle it
10007        app.column_selector_index = 0;
10008        app.handle_action(Action::ToggleColumn);
10009
10010        // First column should be removed
10011        assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
10012        assert!(!app
10013            .ecr_image_visible_column_ids
10014            .contains(&ImageColumn::Tag.id()));
10015
10016        // Toggle it back
10017        app.handle_action(Action::ToggleColumn);
10018        assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
10019        assert!(app
10020            .ecr_image_visible_column_ids
10021            .contains(&ImageColumn::Tag.id()));
10022    }
10023
10024    #[test]
10025    fn test_ecr_repos_column_toggle_works() {
10026        let mut app = test_app();
10027        app.current_service = Service::EcrRepositories;
10028        app.service_selected = true;
10029        app.mode = Mode::ColumnSelector;
10030        app.ecr_state.current_repository = None;
10031
10032        // Start with all columns visible
10033        app.ecr_repo_visible_column_ids = EcrColumn::ids();
10034        let initial_count = app.ecr_repo_visible_column_ids.len();
10035
10036        // Select first column (index 1, since 0 is header) and toggle it
10037        app.column_selector_index = 1;
10038        app.handle_action(Action::ToggleColumn);
10039
10040        // First column should be removed
10041        assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
10042        assert!(!app
10043            .ecr_repo_visible_column_ids
10044            .contains(&EcrColumn::Name.id()));
10045
10046        // Toggle it back
10047        app.handle_action(Action::ToggleColumn);
10048        assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
10049        assert!(app
10050            .ecr_repo_visible_column_ids
10051            .contains(&EcrColumn::Name.id()));
10052    }
10053
10054    #[test]
10055    fn test_ecr_repos_pagination_left_right_navigation() {
10056        use crate::ecr::repo::Repository as EcrRepository;
10057        let mut app = test_app();
10058        app.current_service = Service::EcrRepositories;
10059        app.service_selected = true;
10060        app.mode = Mode::FilterInput;
10061        app.ecr_state.input_focus = InputFocus::Pagination;
10062
10063        // Create 150 repos (3 pages with page size 50)
10064        app.ecr_state.repositories.items = (0..150)
10065            .map(|i| EcrRepository {
10066                name: format!("repo{:03}", i),
10067                uri: format!("uri{}", i),
10068                created_at: "2023-01-01".to_string(),
10069                tag_immutability: "MUTABLE".to_string(),
10070                encryption_type: "AES256".to_string(),
10071            })
10072            .collect();
10073
10074        // Start on page 1 (index 0)
10075        app.ecr_state.repositories.selected = 0;
10076        eprintln!(
10077            "Initial: selected={}, focus={:?}, mode={:?}",
10078            app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
10079        );
10080
10081        // Right arrow (PageDown) should go to page 2
10082        app.handle_action(Action::PageDown);
10083        eprintln!(
10084            "After PageDown: selected={}",
10085            app.ecr_state.repositories.selected
10086        );
10087        assert_eq!(app.ecr_state.repositories.selected, 50);
10088
10089        // Right arrow again should go to page 3
10090        app.handle_action(Action::PageDown);
10091        eprintln!(
10092            "After 2nd PageDown: selected={}",
10093            app.ecr_state.repositories.selected
10094        );
10095        assert_eq!(app.ecr_state.repositories.selected, 100);
10096
10097        // Right arrow at last page should stay at last page
10098        app.handle_action(Action::PageDown);
10099        eprintln!(
10100            "After 3rd PageDown: selected={}",
10101            app.ecr_state.repositories.selected
10102        );
10103        assert_eq!(app.ecr_state.repositories.selected, 100);
10104
10105        // Left arrow (PageUp) should go back to page 2
10106        app.handle_action(Action::PageUp);
10107        eprintln!(
10108            "After PageUp: selected={}",
10109            app.ecr_state.repositories.selected
10110        );
10111        assert_eq!(app.ecr_state.repositories.selected, 50);
10112
10113        // Left arrow again should go to page 1
10114        app.handle_action(Action::PageUp);
10115        eprintln!(
10116            "After 2nd PageUp: selected={}",
10117            app.ecr_state.repositories.selected
10118        );
10119        assert_eq!(app.ecr_state.repositories.selected, 0);
10120
10121        // Left arrow at first page should stay at first page
10122        app.handle_action(Action::PageUp);
10123        eprintln!(
10124            "After 3rd PageUp: selected={}",
10125            app.ecr_state.repositories.selected
10126        );
10127        assert_eq!(app.ecr_state.repositories.selected, 0);
10128    }
10129
10130    #[test]
10131    fn test_ecr_repos_filter_input_when_input_focused() {
10132        use crate::ecr::repo::Repository as EcrRepository;
10133        let mut app = test_app();
10134        app.current_service = Service::EcrRepositories;
10135        app.service_selected = true;
10136        app.mode = Mode::FilterInput;
10137        app.ecr_state.input_focus = InputFocus::Filter;
10138
10139        // Create some repos
10140        app.ecr_state.repositories.items = vec![
10141            EcrRepository {
10142                name: "test-repo".to_string(),
10143                uri: "uri1".to_string(),
10144                created_at: "2023-01-01".to_string(),
10145                tag_immutability: "MUTABLE".to_string(),
10146                encryption_type: "AES256".to_string(),
10147            },
10148            EcrRepository {
10149                name: "prod-repo".to_string(),
10150                uri: "uri2".to_string(),
10151                created_at: "2023-01-01".to_string(),
10152                tag_immutability: "MUTABLE".to_string(),
10153                encryption_type: "AES256".to_string(),
10154            },
10155        ];
10156
10157        // When input is focused, typing should add to filter
10158        assert_eq!(app.ecr_state.repositories.filter, "");
10159        app.handle_action(Action::FilterInput('t'));
10160        assert_eq!(app.ecr_state.repositories.filter, "t");
10161        app.handle_action(Action::FilterInput('e'));
10162        assert_eq!(app.ecr_state.repositories.filter, "te");
10163        app.handle_action(Action::FilterInput('s'));
10164        assert_eq!(app.ecr_state.repositories.filter, "tes");
10165        app.handle_action(Action::FilterInput('t'));
10166        assert_eq!(app.ecr_state.repositories.filter, "test");
10167    }
10168
10169    #[test]
10170    fn test_ecr_repos_digit_input_when_pagination_focused() {
10171        use crate::ecr::repo::Repository as EcrRepository;
10172        let mut app = test_app();
10173        app.current_service = Service::EcrRepositories;
10174        app.service_selected = true;
10175        app.mode = Mode::FilterInput;
10176        app.ecr_state.input_focus = InputFocus::Pagination;
10177
10178        // Create some repos
10179        app.ecr_state.repositories.items = vec![EcrRepository {
10180            name: "test-repo".to_string(),
10181            uri: "uri1".to_string(),
10182            created_at: "2023-01-01".to_string(),
10183            tag_immutability: "MUTABLE".to_string(),
10184            encryption_type: "AES256".to_string(),
10185        }];
10186
10187        // When pagination is focused, digits should go to page_input, not filter
10188        assert_eq!(app.ecr_state.repositories.filter, "");
10189        assert_eq!(app.page_input, "");
10190        app.handle_action(Action::FilterInput('2'));
10191        assert_eq!(app.ecr_state.repositories.filter, "");
10192        assert_eq!(app.page_input, "2");
10193
10194        // Non-digits should not be added to either
10195        app.handle_action(Action::FilterInput('a'));
10196        assert_eq!(app.ecr_state.repositories.filter, "");
10197        assert_eq!(app.page_input, "2");
10198    }
10199
10200    #[test]
10201    fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
10202        use crate::ecr::repo::Repository as EcrRepository;
10203        let mut app = test_app();
10204        app.current_service = Service::EcrRepositories;
10205        app.service_selected = true;
10206        app.mode = Mode::FilterInput;
10207        app.ecr_state.input_focus = InputFocus::Filter;
10208
10209        // Create 150 repos (3 pages)
10210        app.ecr_state.repositories.items = (0..150)
10211            .map(|i| EcrRepository {
10212                name: format!("repo{:03}", i),
10213                uri: format!("uri{}", i),
10214                created_at: "2023-01-01".to_string(),
10215                tag_immutability: "MUTABLE".to_string(),
10216                encryption_type: "AES256".to_string(),
10217            })
10218            .collect();
10219
10220        // Start on page 1
10221        app.ecr_state.repositories.selected = 0;
10222
10223        // When input is focused, left/right should scroll table (not change pages)
10224        app.handle_action(Action::PageDown);
10225        assert_eq!(
10226            app.ecr_state.repositories.selected, 10,
10227            "Should scroll down by 10"
10228        );
10229
10230        app.handle_action(Action::PageUp);
10231        assert_eq!(
10232            app.ecr_state.repositories.selected, 0,
10233            "Should scroll back up"
10234        );
10235    }
10236
10237    #[test]
10238    fn test_ecr_repos_pagination_control_actually_works() {
10239        use crate::ecr::repo::Repository as EcrRepository;
10240
10241        // Test that verifies the exact conditions needed for pagination to work
10242        let mut app = test_app();
10243        app.current_service = Service::EcrRepositories;
10244        app.service_selected = true;
10245        app.mode = Mode::FilterInput;
10246        app.ecr_state.current_repository = None;
10247        app.ecr_state.input_focus = InputFocus::Pagination;
10248
10249        // Create 100 repos (2 pages with page size 50)
10250        app.ecr_state.repositories.items = (0..100)
10251            .map(|i| EcrRepository {
10252                name: format!("repo{:03}", i),
10253                uri: format!("uri{}", i),
10254                created_at: "2023-01-01".to_string(),
10255                tag_immutability: "MUTABLE".to_string(),
10256                encryption_type: "AES256".to_string(),
10257            })
10258            .collect();
10259
10260        app.ecr_state.repositories.selected = 0;
10261
10262        // Verify all conditions are met
10263        assert_eq!(app.mode, Mode::FilterInput);
10264        assert_eq!(app.current_service, Service::EcrRepositories);
10265        assert_eq!(app.ecr_state.current_repository, None);
10266        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10267
10268        // Now test pagination
10269        app.handle_action(Action::PageDown);
10270        assert_eq!(
10271            app.ecr_state.repositories.selected, 50,
10272            "PageDown should move to page 2"
10273        );
10274
10275        app.handle_action(Action::PageUp);
10276        assert_eq!(
10277            app.ecr_state.repositories.selected, 0,
10278            "PageUp should move back to page 1"
10279        );
10280    }
10281
10282    #[test]
10283    fn test_ecr_repos_can_reach_last_page_when_not_multiple_of_page_size() {
10284        use crate::ecr::repo::Repository as EcrRepository;
10285        let mut app = test_app();
10286        app.current_service = Service::EcrRepositories;
10287        app.service_selected = true;
10288        app.mode = Mode::FilterInput;
10289        app.ecr_state.input_focus = InputFocus::Pagination;
10290
10291        // 754 repos, page size 50 — last page starts at 750 (page 16)
10292        app.ecr_state.repositories.items = (0..754)
10293            .map(|i| EcrRepository {
10294                name: format!("repo{:03}", i),
10295                uri: format!("uri{}", i),
10296                created_at: "2023-01-01".to_string(),
10297                tag_immutability: "MUTABLE".to_string(),
10298                encryption_type: "AES256".to_string(),
10299            })
10300            .collect();
10301
10302        // Navigate to page 15 (selected = 700)
10303        app.ecr_state.repositories.selected = 700;
10304
10305        // Right arrow must reach page 16 (selected = 750)
10306        app.handle_action(Action::PageDown);
10307        assert_eq!(
10308            app.ecr_state.repositories.selected, 750,
10309            "Should reach last page (750) but got {}",
10310            app.ecr_state.repositories.selected
10311        );
10312
10313        // Right arrow again must stay at 750 (can't go past last page)
10314        app.handle_action(Action::PageDown);
10315        assert_eq!(app.ecr_state.repositories.selected, 750);
10316    }
10317
10318    #[test]
10319    fn test_ecr_repos_start_filter_resets_focus_to_input() {
10320        let mut app = test_app();
10321        app.current_service = Service::EcrRepositories;
10322        app.service_selected = true;
10323        app.mode = Mode::Normal;
10324        app.ecr_state.current_repository = None;
10325
10326        // Set focus to Pagination
10327        app.ecr_state.input_focus = InputFocus::Pagination;
10328
10329        // Start filter mode
10330        app.handle_action(Action::StartFilter);
10331
10332        // Should reset to Input focus
10333        assert_eq!(app.mode, Mode::FilterInput);
10334        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10335    }
10336
10337    #[test]
10338    fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
10339        use crate::ecr::repo::Repository as EcrRepository;
10340
10341        let mut app = test_app();
10342        app.current_service = Service::EcrRepositories;
10343        app.service_selected = true;
10344        app.mode = Mode::Normal;
10345        app.ecr_state.current_repository = None;
10346
10347        // Create 100 repos (2 pages)
10348        app.ecr_state.repositories.items = (0..100)
10349            .map(|i| EcrRepository {
10350                name: format!("repo{:03}", i),
10351                uri: format!("uri{}", i),
10352                created_at: "2023-01-01".to_string(),
10353                tag_immutability: "MUTABLE".to_string(),
10354                encryption_type: "AES256".to_string(),
10355            })
10356            .collect();
10357
10358        app.ecr_state.repositories.selected = 0;
10359
10360        // User presses 'i' to enter filter mode
10361        app.handle_action(Action::StartFilter);
10362        assert_eq!(app.mode, Mode::FilterInput);
10363        assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10364
10365        // User presses Tab to switch to pagination
10366        app.handle_action(Action::NextFilterFocus);
10367        assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10368
10369        // User presses right arrow (PageDown)
10370        eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
10371            app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
10372        app.handle_action(Action::PageDown);
10373        eprintln!(
10374            "After PageDown: selected={}",
10375            app.ecr_state.repositories.selected
10376        );
10377
10378        // Should move to page 2
10379        assert_eq!(
10380            app.ecr_state.repositories.selected, 50,
10381            "Right arrow should move to page 2"
10382        );
10383
10384        // User presses left arrow (PageUp)
10385        app.handle_action(Action::PageUp);
10386        assert_eq!(
10387            app.ecr_state.repositories.selected, 0,
10388            "Left arrow should move back to page 1"
10389        );
10390    }
10391
10392    #[test]
10393    fn test_apig_filter_input() {
10394        let mut app = test_app();
10395        app.current_service = Service::ApiGatewayApis;
10396        app.service_selected = true;
10397        app.mode = Mode::FilterInput;
10398
10399        app.handle_action(Action::FilterInput('t'));
10400        app.handle_action(Action::FilterInput('e'));
10401        app.handle_action(Action::FilterInput('s'));
10402        app.handle_action(Action::FilterInput('t'));
10403        assert_eq!(app.apig_state.apis.filter, "test");
10404
10405        app.handle_action(Action::FilterBackspace);
10406        assert_eq!(app.apig_state.apis.filter, "tes");
10407    }
10408
10409    #[test]
10410    fn test_apig_start_filter_enters_filter_mode() {
10411        let mut app = test_app();
10412        app.current_service = Service::ApiGatewayApis;
10413        app.service_selected = true;
10414        app.mode = Mode::Normal;
10415
10416        app.handle_action(Action::StartFilter);
10417        assert_eq!(app.mode, Mode::FilterInput);
10418        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10419    }
10420
10421    #[test]
10422    fn test_apig_input_focus_cycles_with_tab() {
10423        let mut app = test_app();
10424        app.current_service = Service::ApiGatewayApis;
10425        app.service_selected = true;
10426        app.mode = Mode::FilterInput;
10427        app.apig_state.input_focus = InputFocus::Filter;
10428
10429        // Tab cycles from Filter to Pagination
10430        app.handle_action(Action::NextFilterFocus);
10431        assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
10432
10433        // Tab cycles back to Filter
10434        app.handle_action(Action::NextFilterFocus);
10435        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10436    }
10437
10438    #[test]
10439    fn test_apig_input_focus_cycles_with_shift_tab() {
10440        let mut app = test_app();
10441        app.current_service = Service::ApiGatewayApis;
10442        app.service_selected = true;
10443        app.mode = Mode::FilterInput;
10444        app.apig_state.input_focus = InputFocus::Filter;
10445
10446        // Shift+Tab cycles from Filter to Pagination (backward)
10447        app.handle_action(Action::PrevFilterFocus);
10448        assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
10449
10450        // Shift+Tab cycles back to Filter
10451        app.handle_action(Action::PrevFilterFocus);
10452        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10453    }
10454
10455    #[test]
10456    fn test_apig_exact_user_flow_i_tab_filter() {
10457        let mut app = test_app();
10458        app.current_service = Service::ApiGatewayApis;
10459        app.service_selected = true;
10460        app.mode = Mode::Normal;
10461
10462        // Create test APIs
10463        app.apig_state.apis.items = vec![
10464            crate::apig::api::RestApi {
10465                id: "api1".to_string(),
10466                name: "test-api".to_string(),
10467                description: "Test API".to_string(),
10468                created_date: "2023-01-01".to_string(),
10469                api_key_source: "HEADER".to_string(),
10470                endpoint_configuration: "REGIONAL".to_string(),
10471                protocol_type: "REST".to_string(),
10472                disable_execute_api_endpoint: false,
10473                status: "AVAILABLE".to_string(),
10474            },
10475            crate::apig::api::RestApi {
10476                id: "api2".to_string(),
10477                name: "prod-api".to_string(),
10478                description: "Production API".to_string(),
10479                created_date: "2023-01-02".to_string(),
10480                api_key_source: "HEADER".to_string(),
10481                endpoint_configuration: "REGIONAL".to_string(),
10482                protocol_type: "REST".to_string(),
10483                disable_execute_api_endpoint: false,
10484                status: "AVAILABLE".to_string(),
10485            },
10486        ];
10487
10488        // User presses 'i' to enter filter mode
10489        app.handle_action(Action::StartFilter);
10490        assert_eq!(app.mode, Mode::FilterInput);
10491        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10492
10493        // User types "test"
10494        app.handle_action(Action::FilterInput('t'));
10495        app.handle_action(Action::FilterInput('e'));
10496        app.handle_action(Action::FilterInput('s'));
10497        app.handle_action(Action::FilterInput('t'));
10498        assert_eq!(app.apig_state.apis.filter, "test");
10499
10500        // User presses Tab to switch to pagination
10501        app.handle_action(Action::NextFilterFocus);
10502        assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
10503
10504        // User presses Tab again to go back to filter
10505        app.handle_action(Action::NextFilterFocus);
10506        assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10507    }
10508
10509    #[test]
10510    fn test_apig_row_expansion() {
10511        let mut app = test_app();
10512        app.current_service = Service::ApiGatewayApis;
10513        app.service_selected = true;
10514        app.apig_state.apis.items = vec![crate::apig::api::RestApi {
10515            id: "api1".to_string(),
10516            name: "test-api".to_string(),
10517            description: "Test API".to_string(),
10518            created_date: "2023-01-01".to_string(),
10519            api_key_source: "HEADER".to_string(),
10520            endpoint_configuration: "REGIONAL".to_string(),
10521            protocol_type: "REST".to_string(),
10522            disable_execute_api_endpoint: false,
10523            status: "AVAILABLE".to_string(),
10524        }];
10525        app.apig_state.apis.selected = 0;
10526
10527        assert_eq!(app.apig_state.apis.expanded_item, None);
10528
10529        // Right arrow (NextPane) should NOT expand rows
10530        app.handle_action(Action::NextPane);
10531        assert_eq!(app.apig_state.apis.expanded_item, None);
10532
10533        // Manually expand using ExpandRow action
10534        app.handle_action(Action::ExpandRow);
10535        assert_eq!(app.apig_state.apis.expanded_item, Some(0));
10536
10537        // Right arrow should NOT collapse expanded row
10538        app.handle_action(Action::NextPane);
10539        assert_eq!(app.apig_state.apis.expanded_item, Some(0));
10540
10541        // Left arrow (PrevPane) should collapse
10542        app.handle_action(Action::PrevPane);
10543        assert_eq!(app.apig_state.apis.expanded_item, None);
10544    }
10545
10546    #[test]
10547    fn test_apig_column_preferences() {
10548        let mut app = test_app();
10549        app.current_service = Service::ApiGatewayApis;
10550        app.service_selected = true;
10551
10552        // All columns should be visible by default
10553        let initial_count = app.apig_api_visible_column_ids.len();
10554        assert_eq!(initial_count, app.apig_api_column_ids.len());
10555
10556        // Open preferences
10557        app.handle_action(Action::OpenColumnSelector);
10558        assert_eq!(app.mode, Mode::ColumnSelector);
10559
10560        // Toggle first column (index 1, since 0 is header)
10561        app.column_selector_index = 1;
10562        let first_col = app.apig_api_column_ids[0];
10563        assert!(app.apig_api_visible_column_ids.contains(&first_col));
10564
10565        app.handle_action(Action::ToggleColumn);
10566
10567        // First column should now be hidden
10568        assert!(!app.apig_api_visible_column_ids.contains(&first_col));
10569        assert_eq!(app.apig_api_visible_column_ids.len(), initial_count - 1);
10570
10571        // Toggle it back
10572        app.handle_action(Action::ToggleColumn);
10573        assert!(app.apig_api_visible_column_ids.contains(&first_col));
10574        assert_eq!(app.apig_api_visible_column_ids.len(), initial_count);
10575    }
10576
10577    #[test]
10578    fn test_apig_page_size_preferences() {
10579        use crate::common::PageSize;
10580
10581        let mut app = test_app();
10582        app.current_service = Service::ApiGatewayApis;
10583        app.service_selected = true;
10584
10585        // Default page size
10586        assert_eq!(app.apig_state.apis.page_size, PageSize::Fifty);
10587
10588        // Open preferences
10589        app.handle_action(Action::OpenColumnSelector);
10590
10591        // Page size options start after: header (1) + columns (8) + blank line (1) + "Page Size" header (1) = 11
10592        // So indices are: 11 (header), 12 (10), 13 (25), 14 (50), 15 (100)
10593        // But the code uses: column_count + 3, +4, +5, +6
10594        // With 8 columns: 11, 12, 13, 14
10595        app.column_selector_index = app.apig_api_column_ids.len() + 3;
10596        app.handle_action(Action::ToggleColumn);
10597        assert_eq!(app.apig_state.apis.page_size, PageSize::Ten);
10598
10599        // Select page size 100
10600        app.column_selector_index = app.apig_api_column_ids.len() + 6;
10601        app.handle_action(Action::ToggleColumn);
10602        assert_eq!(app.apig_state.apis.page_size, PageSize::OneHundred);
10603    }
10604
10605    #[test]
10606    fn test_apig_preferences_skip_blank_row() {
10607        let mut app = test_app();
10608        app.current_service = Service::ApiGatewayApis;
10609        app.service_selected = true;
10610        app.mode = Mode::ColumnSelector;
10611
10612        // Start at last column (index 8)
10613        app.column_selector_index = 8;
10614
10615        // Next should skip blank row (9) and go to page size header (10)
10616        app.handle_action(Action::NextItem);
10617        assert_eq!(app.column_selector_index, 10);
10618
10619        // Prev should skip blank row (9) and go back to last column (8)
10620        app.handle_action(Action::PrevItem);
10621        assert_eq!(app.column_selector_index, 8);
10622    }
10623
10624    #[test]
10625    fn test_apig_preferences_ctrl_d_skip_blank_row() {
10626        let mut app = test_app();
10627        app.current_service = Service::ApiGatewayApis;
10628        app.service_selected = true;
10629        app.mode = Mode::ColumnSelector;
10630
10631        // Start at index 0
10632        app.column_selector_index = 0;
10633
10634        // Ctrl+D (PageDown) by 10 would land on blank row (9), should skip to 10
10635        app.handle_action(Action::PageDown);
10636        assert_eq!(app.column_selector_index, 10);
10637
10638        // Ctrl+U (PageUp) by 10 would land on 0
10639        app.handle_action(Action::PageUp);
10640        assert_eq!(app.column_selector_index, 0);
10641    }
10642
10643    #[test]
10644    fn test_apig_preferences_ctrl_u_skip_blank_row() {
10645        let mut app = test_app();
10646        app.current_service = Service::ApiGatewayApis;
10647        app.service_selected = true;
10648        app.mode = Mode::ColumnSelector;
10649
10650        // Start at page size header (index 10)
10651        app.column_selector_index = 10;
10652
10653        // Ctrl+U (PageUp) by 10 would land on 0
10654        app.handle_action(Action::PageUp);
10655        assert_eq!(app.column_selector_index, 0);
10656
10657        // Go to index 19 (if it exists, otherwise use 14 which is max)
10658        app.column_selector_index = 14; // Max index for APIG
10659
10660        // Ctrl+U (PageUp) by 10 would land on 4
10661        app.handle_action(Action::PageUp);
10662        assert_eq!(app.column_selector_index, 4);
10663    }
10664
10665    #[test]
10666    fn test_apig_preferences_tab_cycles_sections() {
10667        let mut app = test_app();
10668        app.current_service = Service::ApiGatewayApis;
10669        app.service_selected = true;
10670        app.mode = Mode::ColumnSelector;
10671
10672        // Start at columns header (0)
10673        app.column_selector_index = 0;
10674
10675        // Tab should jump to page size section (column_count + 2 = 10)
10676        app.handle_action(Action::NextPreferences);
10677        assert_eq!(app.column_selector_index, 10);
10678
10679        // Tab again should wrap back to columns (0)
10680        app.handle_action(Action::NextPreferences);
10681        assert_eq!(app.column_selector_index, 0);
10682    }
10683
10684    #[test]
10685    fn test_apig_preferences_shift_tab_cycles_sections() {
10686        let mut app = test_app();
10687        app.current_service = Service::ApiGatewayApis;
10688        app.service_selected = true;
10689        app.mode = Mode::ColumnSelector;
10690
10691        // Start at columns header (0)
10692        app.column_selector_index = 0;
10693
10694        // Shift+Tab should jump to page size section (10)
10695        app.handle_action(Action::PrevPreferences);
10696        assert_eq!(app.column_selector_index, 10);
10697
10698        // Shift+Tab again should wrap back to columns (0)
10699        app.handle_action(Action::PrevPreferences);
10700        assert_eq!(app.column_selector_index, 0);
10701    }
10702
10703    #[test]
10704    fn test_apig_arrow_navigation() {
10705        let mut app = test_app();
10706        app.current_service = Service::ApiGatewayApis;
10707        app.service_selected = true;
10708        app.mode = Mode::Normal; // Ensure we're in Normal mode
10709        app.apig_state.apis.filter = String::new(); // Ensure filter is empty
10710        app.apig_state.apis.items = vec![
10711            crate::apig::api::RestApi {
10712                id: "api1".to_string(),
10713                name: "test-api-1".to_string(),
10714                description: "Test API 1".to_string(),
10715                created_date: "2023-01-01".to_string(),
10716                api_key_source: "HEADER".to_string(),
10717                endpoint_configuration: "REGIONAL".to_string(),
10718                protocol_type: "REST".to_string(),
10719                disable_execute_api_endpoint: false,
10720                status: "AVAILABLE".to_string(),
10721            },
10722            crate::apig::api::RestApi {
10723                id: "api2".to_string(),
10724                name: "test-api-2".to_string(),
10725                description: "Test API 2".to_string(),
10726                created_date: "2023-01-02".to_string(),
10727                api_key_source: "HEADER".to_string(),
10728                endpoint_configuration: "REGIONAL".to_string(),
10729                protocol_type: "HTTP".to_string(),
10730                disable_execute_api_endpoint: false,
10731                status: "AVAILABLE".to_string(),
10732            },
10733        ];
10734        app.apig_state.apis.selected = 0;
10735
10736        // Down arrow should move to next item
10737        app.handle_action(Action::NextItem);
10738        assert_eq!(app.apig_state.apis.selected, 1);
10739
10740        // Up arrow should move to previous item
10741        app.handle_action(Action::PrevItem);
10742        assert_eq!(app.apig_state.apis.selected, 0);
10743    }
10744
10745    #[test]
10746    fn test_apig_collapse_row() {
10747        let mut app = test_app();
10748        app.current_service = Service::ApiGatewayApis;
10749        app.service_selected = true;
10750        app.apig_state.apis.items = vec![crate::apig::api::RestApi {
10751            id: "api1".to_string(),
10752            name: "test-api".to_string(),
10753            description: "Test API".to_string(),
10754            created_date: "2023-01-01".to_string(),
10755            api_key_source: "HEADER".to_string(),
10756            endpoint_configuration: "REGIONAL".to_string(),
10757            protocol_type: "REST".to_string(),
10758            disable_execute_api_endpoint: false,
10759            status: "AVAILABLE".to_string(),
10760        }];
10761        app.apig_state.apis.selected = 0;
10762        app.apig_state.apis.expanded_item = Some(0);
10763
10764        // Left arrow should collapse
10765        app.handle_action(Action::CollapseRow);
10766        assert_eq!(app.apig_state.apis.expanded_item, None);
10767    }
10768
10769    #[test]
10770    fn test_apig_filter_resets_selection() {
10771        let mut app = test_app();
10772        app.current_service = Service::ApiGatewayApis;
10773        app.service_selected = true;
10774        app.mode = Mode::FilterInput;
10775        app.apig_state.input_focus = InputFocus::Filter;
10776        app.apig_state.apis.items = vec![
10777            crate::apig::api::RestApi {
10778                id: "api1".to_string(),
10779                name: "alpha-api".to_string(),
10780                description: "Alpha API".to_string(),
10781                created_date: "2023-01-01".to_string(),
10782                api_key_source: "HEADER".to_string(),
10783                endpoint_configuration: "REGIONAL".to_string(),
10784                protocol_type: "REST".to_string(),
10785                disable_execute_api_endpoint: false,
10786                status: "AVAILABLE".to_string(),
10787            },
10788            crate::apig::api::RestApi {
10789                id: "api2".to_string(),
10790                name: "beta-api".to_string(),
10791                description: "Beta API".to_string(),
10792                created_date: "2023-01-02".to_string(),
10793                api_key_source: "HEADER".to_string(),
10794                endpoint_configuration: "REGIONAL".to_string(),
10795                protocol_type: "HTTP".to_string(),
10796                disable_execute_api_endpoint: false,
10797                status: "AVAILABLE".to_string(),
10798            },
10799        ];
10800
10801        // Select second item and expand it
10802        app.apig_state.apis.selected = 1;
10803        app.apig_state.apis.expanded_item = Some(1);
10804
10805        // Type a filter character
10806        app.handle_action(Action::FilterInput('a'));
10807
10808        // Selection and expansion should be reset
10809        assert_eq!(app.apig_state.apis.selected, 0);
10810        assert_eq!(app.apig_state.apis.expanded_item, None);
10811        assert_eq!(app.apig_state.apis.filter, "a");
10812    }
10813
10814    #[test]
10815    fn test_service_picker_starts_in_normal_mode() {
10816        let app = test_app();
10817        assert_eq!(app.mode, Mode::ServicePicker);
10818        assert!(!app.service_picker.filter_active);
10819    }
10820
10821    #[test]
10822    fn test_service_picker_i_key_activates_filter() {
10823        let mut app = test_app();
10824
10825        // Start in ServicePicker mode (Normal mode - filter not active)
10826        assert_eq!(app.mode, Mode::ServicePicker);
10827        assert!(!app.service_picker.filter_active);
10828        assert!(app.service_picker.filter.is_empty());
10829
10830        // Press 'i' to enter filter mode
10831        app.handle_action(Action::EnterFilterMode);
10832
10833        // Should still be in ServicePicker mode but filter should be active
10834        assert_eq!(app.mode, Mode::ServicePicker);
10835        assert!(app.service_picker.filter_active);
10836        assert!(app.service_picker.filter.is_empty());
10837
10838        // Now typing should work
10839        app.handle_action(Action::FilterInput('s'));
10840        assert_eq!(app.service_picker.filter, "s");
10841    }
10842
10843    #[test]
10844    fn test_service_picker_esc_exits_filter_mode() {
10845        let mut app = test_app();
10846        assert_eq!(app.mode, Mode::ServicePicker);
10847        assert!(!app.service_picker.filter_active);
10848
10849        // Enter filter mode
10850        app.handle_action(Action::EnterFilterMode);
10851        assert!(app.service_picker.filter_active);
10852        assert_eq!(app.mode, Mode::ServicePicker);
10853
10854        // Type something
10855        app.handle_action(Action::FilterInput('s'));
10856        assert_eq!(app.service_picker.filter, "s");
10857
10858        // Esc should exit filter mode (not close menu)
10859        app.handle_action(Action::ExitFilterMode);
10860        assert!(!app.service_picker.filter_active);
10861        assert_eq!(app.mode, Mode::ServicePicker); // Still in picker
10862
10863        // Second Esc should close menu
10864        app.handle_action(Action::ExitFilterMode);
10865        assert_eq!(app.mode, Mode::Normal);
10866    }
10867
10868    #[test]
10869    fn test_service_picker_navigation_only_works_in_normal_mode() {
10870        let mut app = test_app();
10871        app.service_picker.selected = 0;
10872
10873        // In normal mode, navigation should work
10874        assert!(!app.service_picker.filter_active);
10875        app.handle_action(Action::NextItem);
10876        assert_eq!(app.service_picker.selected, 1);
10877
10878        // Enter filter mode
10879        app.handle_action(Action::EnterFilterMode);
10880        assert!(app.service_picker.filter_active);
10881
10882        // In filter mode, navigation should NOT work
10883        let prev_selected = app.service_picker.selected;
10884        app.handle_action(Action::NextItem);
10885        assert_eq!(app.service_picker.selected, prev_selected); // Unchanged
10886
10887        // Exit filter mode
10888        app.handle_action(Action::ExitFilterMode);
10889        assert!(!app.service_picker.filter_active);
10890
10891        // Navigation should work again
10892        app.handle_action(Action::NextItem);
10893        assert_eq!(app.service_picker.selected, prev_selected + 1);
10894    }
10895
10896    #[test]
10897    fn test_service_picker_typing_filters_services() {
10898        let mut app = test_app();
10899
10900        // Start in ServicePicker mode
10901        assert_eq!(app.mode, Mode::ServicePicker);
10902        assert!(!app.service_picker.filter_active);
10903
10904        // Enter filter mode
10905        app.handle_action(Action::EnterFilterMode);
10906        assert!(app.service_picker.filter_active);
10907
10908        // Type "s3" to filter
10909        app.handle_action(Action::FilterInput('s'));
10910        app.handle_action(Action::FilterInput('3'));
10911
10912        assert_eq!(app.service_picker.filter, "s3");
10913        assert_eq!(app.mode, Mode::ServicePicker);
10914    }
10915
10916    #[test]
10917    fn test_service_picker_enter_exits_insert_mode() {
10918        let mut app = test_app();
10919        assert_eq!(app.mode, Mode::ServicePicker);
10920        assert!(!app.service_selected);
10921
10922        // Enter filter mode (INSERT mode)
10923        app.handle_action(Action::EnterFilterMode);
10924        assert!(app.service_picker.filter_active);
10925
10926        // Type to filter
10927        app.handle_action(Action::FilterInput('s'));
10928        app.handle_action(Action::FilterInput('3'));
10929
10930        // Press Enter - should exit INSERT mode, not select service
10931        app.handle_action(Action::Select);
10932        assert!(!app.service_selected, "Should not select service");
10933        assert!(!app.service_picker.filter_active, "Should exit INSERT mode");
10934        assert_eq!(app.mode, Mode::ServicePicker);
10935
10936        // Now press Enter again in NORMAL mode - should select service
10937        app.handle_action(Action::Select);
10938        assert!(app.service_selected, "Should select service in NORMAL mode");
10939        assert_eq!(app.mode, Mode::Normal);
10940    }
10941
10942    #[test]
10943    fn test_service_picker_resets_on_open() {
10944        let mut app = test_app();
10945
10946        // Select a service to get into Normal mode
10947        app.service_selected = true;
10948        app.mode = Mode::Normal;
10949
10950        // Simulate having previous filter and selection
10951        app.service_picker.filter = "previous".to_string();
10952        app.service_picker.filter_active = true;
10953        app.service_picker.selected = 5;
10954
10955        // Open space menu (service picker)
10956        app.handle_action(Action::OpenSpaceMenu);
10957
10958        // Filter, filter_active, and selection should be reset
10959        assert_eq!(app.mode, Mode::SpaceMenu);
10960        assert!(app.service_picker.filter.is_empty());
10961        assert!(!app.service_picker.filter_active);
10962        assert_eq!(app.service_picker.selected, 0);
10963    }
10964
10965    #[test]
10966    fn test_kms_in_service_picker_services_list() {
10967        let app = test_app();
10968        assert!(
10969            app.service_picker.services.contains(&"KMS › Managed Keys"),
10970            "KMS › Managed Keys must be in the service picker list"
10971        );
10972    }
10973
10974    #[test]
10975    fn test_kms_service_picker_selects_kms_service() {
10976        let mut app = test_app();
10977        app.mode = Mode::ServicePicker;
10978        app.service_picker.filter_active = true;
10979
10980        // Type "kms" into the filter
10981        for c in ['k', 'm', 's'] {
10982            app.handle_action(Action::FilterInput(c));
10983        }
10984
10985        assert_eq!(app.service_picker.filter, "kms");
10986
10987        // The filtered list should contain "KMS › Managed Keys"
10988        let filtered: Vec<_> = app
10989            .service_picker
10990            .services
10991            .iter()
10992            .filter(|s| s.to_lowercase().contains("kms"))
10993            .collect();
10994        assert_eq!(filtered.len(), 1);
10995        assert_eq!(*filtered[0], "KMS › Managed Keys");
10996    }
10997
10998    #[test]
10999    fn test_selecting_kms_in_picker_switches_service() {
11000        let mut app = test_app();
11001        app.mode = Mode::ServicePicker;
11002        app.service_selected = false;
11003
11004        // Find the index of "KMS › Managed Keys" and select it
11005        // filtered_services() sorts alphabetically, so use that for the correct index
11006        let kms_idx = app
11007            .filtered_services()
11008            .iter()
11009            .position(|&s| s == "KMS › Managed Keys")
11010            .expect("KMS › Managed Keys must be in services list");
11011        app.service_picker.selected = kms_idx;
11012
11013        app.handle_action(Action::Select);
11014
11015        assert_eq!(app.current_service, Service::KmsKeys);
11016        assert!(app.service_selected);
11017    }
11018
11019    #[test]
11020    fn test_kms_picker_creates_session_tab_with_correct_title() {
11021        // Session tab title must be "KMS › Managed Keys" so it shows in the tabs row
11022        let mut app = test_app();
11023        app.mode = Mode::ServicePicker;
11024        app.service_selected = false;
11025
11026        let kms_idx = app
11027            .filtered_services()
11028            .iter()
11029            .position(|&s| s == "KMS › Managed Keys")
11030            .unwrap();
11031        app.service_picker.selected = kms_idx;
11032        app.handle_action(Action::Select);
11033
11034        assert!(!app.tabs.is_empty(), "a session tab must be created");
11035        let tab = &app.tabs[app.current_tab];
11036        assert_eq!(
11037            tab.title, "KMS › Managed Keys",
11038            "session tab title must match picker label"
11039        );
11040        assert_eq!(tab.service, Service::KmsKeys);
11041    }
11042
11043    #[test]
11044    fn test_kms_tab_switch_does_not_trigger_reload() {
11045        // Switching between AWS managed / Customer managed must NOT set loading=true.
11046        // The data is filtered client-side; re-fetching would show "0" briefly.
11047        use crate::ui::kms::Tab as KmsTab;
11048        let mut app = test_app();
11049        app.current_service = Service::KmsKeys;
11050        app.service_selected = true;
11051        app.mode = Mode::Normal;
11052        app.kms_state.tab = KmsTab::AwsManaged;
11053        app.kms_state.keys.loading = false;
11054
11055        app.handle_action(Action::NextDetailTab);
11056
11057        assert_eq!(app.kms_state.tab, KmsTab::CustomerManaged);
11058        assert!(
11059            !app.kms_state.keys.loading,
11060            "tab switch must NOT trigger a reload — keys are filtered client-side"
11061        );
11062    }
11063
11064    #[test]
11065    fn test_kms_tab_switching_cycles() {
11066        use crate::ui::kms::Tab as KmsTab;
11067        let mut app = test_app();
11068        app.current_service = Service::KmsKeys;
11069        app.service_selected = true;
11070        app.mode = Mode::Normal;
11071
11072        assert_eq!(app.kms_state.tab, KmsTab::AwsManaged);
11073
11074        app.handle_action(Action::NextDetailTab);
11075        assert_eq!(app.kms_state.tab, KmsTab::CustomerManaged);
11076
11077        app.handle_action(Action::NextDetailTab);
11078        assert_eq!(app.kms_state.tab, KmsTab::AwsManaged, "must wrap around");
11079
11080        app.handle_action(Action::PrevDetailTab);
11081        assert_eq!(app.kms_state.tab, KmsTab::CustomerManaged);
11082    }
11083
11084    #[test]
11085    fn test_kms_load_condition_triggers_on_loading_flag() {
11086        // After region change, main.rs sets kms_state.keys.loading = true.
11087        // Verify the load condition (loading == true) is independent of prev_service.
11088        // This simulates: same service, same region → loading=true forces re-fetch.
11089        let mut app = test_app();
11090        app.current_service = Service::KmsKeys;
11091        app.service_selected = true;
11092        app.kms_state.keys.loading = false;
11093
11094        // Simulate what region change does: set loading=true
11095        app.kms_state.keys.loading = true;
11096
11097        // The main.rs condition is: kms_state.keys.loading == true
11098        // Just verify the flag is set correctly
11099        assert!(
11100            app.kms_state.keys.loading,
11101            "loading flag must be true after region change to trigger reload"
11102        );
11103    }
11104
11105    #[test]
11106    fn test_kms_session_tab_survives_service_switch() {
11107        // When KMS is selected from picker, a session tab is created.
11108        // This tab must remain in app.tabs.
11109        let mut app = test_app();
11110        app.mode = Mode::ServicePicker;
11111        app.service_selected = false;
11112
11113        let kms_idx = app
11114            .filtered_services()
11115            .iter()
11116            .position(|&s| s == "KMS › Managed Keys")
11117            .unwrap();
11118        app.service_picker.selected = kms_idx;
11119        app.handle_action(Action::Select);
11120
11121        assert_eq!(app.tabs.len(), 1);
11122        assert_eq!(app.tabs[0].title, "KMS › Managed Keys");
11123        assert_eq!(app.tabs[0].service, Service::KmsKeys);
11124        assert_eq!(app.current_tab, 0);
11125    }
11126
11127    #[test]
11128    fn test_no_pii_in_test_data() {
11129        // Ensure test data uses placeholder account IDs, not real ones
11130        let test_repo = EcrRepository {
11131            name: "test-repo".to_string(),
11132            uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
11133            created_at: "2024-01-01".to_string(),
11134            tag_immutability: "MUTABLE".to_string(),
11135            encryption_type: "AES256".to_string(),
11136        };
11137
11138        // Verify placeholder account ID is used
11139        assert!(test_repo.uri.starts_with("123456789012"));
11140        assert!(!test_repo.uri.contains("123456789013")); // Not a real account
11141    }
11142
11143    #[test]
11144    fn test_lambda_versions_tab_triggers_loading() {
11145        let mut app = test_app();
11146        app.current_service = Service::LambdaFunctions;
11147        app.service_selected = true;
11148
11149        // Simulate selecting a function
11150        app.lambda_state.current_function = Some("test-function".to_string());
11151        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11152
11153        // Initially no versions
11154        assert!(app.lambda_state.version_table.items.is_empty());
11155
11156        // Switch to Versions tab
11157        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11158
11159        // The main loop should detect this change and load versions
11160        // We verify the state is set up correctly for loading
11161        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11162        assert!(app.lambda_state.current_function.is_some());
11163    }
11164
11165    #[test]
11166    fn test_lambda_versions_navigation() {
11167        let mut app = test_app();
11168        app.current_service = Service::LambdaFunctions;
11169        app.service_selected = true;
11170        app.lambda_state.current_function = Some("test-function".to_string());
11171        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11172
11173        // Add test versions
11174        app.lambda_state.version_table.items = vec![
11175            LambdaVersion {
11176                version: "3".to_string(),
11177                aliases: "prod".to_string(),
11178                description: "".to_string(),
11179                last_modified: "".to_string(),
11180                architecture: "X86_64".to_string(),
11181            },
11182            LambdaVersion {
11183                version: "2".to_string(),
11184                aliases: "".to_string(),
11185                description: "".to_string(),
11186                last_modified: "".to_string(),
11187                architecture: "X86_64".to_string(),
11188            },
11189            LambdaVersion {
11190                version: "1".to_string(),
11191                aliases: "".to_string(),
11192                description: "".to_string(),
11193                last_modified: "".to_string(),
11194                architecture: "X86_64".to_string(),
11195            },
11196        ];
11197
11198        // Verify versions are loaded
11199        assert_eq!(app.lambda_state.version_table.items.len(), 3);
11200        assert_eq!(app.lambda_state.version_table.items[0].version, "3");
11201        assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
11202
11203        // Verify selection can be changed
11204        app.lambda_state.version_table.selected = 1;
11205        assert_eq!(app.lambda_state.version_table.selected, 1);
11206    }
11207
11208    #[test]
11209    fn test_lambda_versions_with_aliases() {
11210        let version = LambdaVersion {
11211            version: "35".to_string(),
11212            aliases: "prod, staging".to_string(),
11213            description: "Production version".to_string(),
11214            last_modified: "2024-01-01".to_string(),
11215            architecture: "X86_64".to_string(),
11216        };
11217
11218        assert_eq!(version.aliases, "prod, staging");
11219        assert!(!version.aliases.is_empty());
11220    }
11221
11222    #[test]
11223    fn test_lambda_versions_expansion() {
11224        let mut app = test_app();
11225        app.current_service = Service::LambdaFunctions;
11226        app.service_selected = true;
11227        app.lambda_state.current_function = Some("test-function".to_string());
11228        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11229
11230        // Add test versions
11231        app.lambda_state.version_table.items = vec![
11232            LambdaVersion {
11233                version: "2".to_string(),
11234                aliases: "prod".to_string(),
11235                description: "Production".to_string(),
11236                last_modified: "2024-01-01".to_string(),
11237                architecture: "X86_64".to_string(),
11238            },
11239            LambdaVersion {
11240                version: "1".to_string(),
11241                aliases: "".to_string(),
11242                description: "".to_string(),
11243                last_modified: "2024-01-01".to_string(),
11244                architecture: "Arm64".to_string(),
11245            },
11246        ];
11247
11248        app.lambda_state.version_table.selected = 0;
11249
11250        // Verify expansion can be set
11251        app.lambda_state.version_table.expanded_item = Some(0);
11252        assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
11253
11254        // Select different version
11255        app.lambda_state.version_table.selected = 1;
11256        app.lambda_state.version_table.expanded_item = Some(1);
11257        assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
11258    }
11259
11260    #[test]
11261    fn test_lambda_versions_page_navigation() {
11262        let mut app = test_app();
11263        app.current_service = Service::LambdaFunctions;
11264        app.service_selected = true;
11265        app.lambda_state.current_function = Some("test-function".to_string());
11266        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11267
11268        // Add 30 test versions
11269        app.lambda_state.version_table.items = (1..=30)
11270            .map(|i| LambdaVersion {
11271                version: i.to_string(),
11272                aliases: "".to_string(),
11273                description: "".to_string(),
11274                last_modified: "".to_string(),
11275                architecture: "X86_64".to_string(),
11276            })
11277            .collect();
11278
11279        app.lambda_state.version_table.page_size = PageSize::Ten;
11280        app.lambda_state.version_table.selected = 0;
11281
11282        // Go to page 2
11283        app.page_input = "2".to_string();
11284        app.handle_action(Action::OpenColumnSelector);
11285
11286        // Should be at index 10 (start of page 2)
11287        assert_eq!(app.lambda_state.version_table.selected, 10);
11288    }
11289
11290    #[test]
11291    fn test_lambda_versions_pagination_arrow_keys() {
11292        let mut app = test_app();
11293        app.current_service = Service::LambdaFunctions;
11294        app.service_selected = true;
11295        app.lambda_state.current_function = Some("test-function".to_string());
11296        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11297        app.mode = Mode::FilterInput;
11298        app.lambda_state.version_input_focus = InputFocus::Pagination;
11299
11300        // Add 30 test versions
11301        app.lambda_state.version_table.items = (1..=30)
11302            .map(|i| LambdaVersion {
11303                version: i.to_string(),
11304                aliases: "".to_string(),
11305                description: "".to_string(),
11306                last_modified: "".to_string(),
11307                architecture: "X86_64".to_string(),
11308            })
11309            .collect();
11310
11311        app.lambda_state.version_table.page_size = PageSize::Ten;
11312        app.lambda_state.version_table.selected = 0;
11313
11314        // Right arrow (PageDown) should go to next page
11315        app.handle_action(Action::PageDown);
11316        assert_eq!(app.lambda_state.version_table.selected, 10);
11317
11318        // Left arrow (PageUp) should go back
11319        app.handle_action(Action::PageUp);
11320        assert_eq!(app.lambda_state.version_table.selected, 0);
11321    }
11322
11323    #[test]
11324    fn test_lambda_versions_page_input_in_filter_mode() {
11325        let mut app = test_app();
11326        app.current_service = Service::LambdaFunctions;
11327        app.service_selected = true;
11328        app.lambda_state.current_function = Some("test-function".to_string());
11329        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11330        app.mode = Mode::FilterInput;
11331        app.lambda_state.version_input_focus = InputFocus::Pagination;
11332
11333        // Add 30 test versions
11334        app.lambda_state.version_table.items = (1..=30)
11335            .map(|i| LambdaVersion {
11336                version: i.to_string(),
11337                aliases: "".to_string(),
11338                description: "".to_string(),
11339                last_modified: "".to_string(),
11340                architecture: "X86_64".to_string(),
11341            })
11342            .collect();
11343
11344        app.lambda_state.version_table.page_size = PageSize::Ten;
11345        app.lambda_state.version_table.selected = 0;
11346
11347        // Type "2" when focused on Pagination
11348        app.handle_action(Action::FilterInput('2'));
11349        assert_eq!(app.page_input, "2");
11350        assert_eq!(app.lambda_state.version_table.filter, ""); // Should not go to filter
11351
11352        // Press 'p' to go to page 2
11353        app.handle_action(Action::OpenColumnSelector);
11354        assert_eq!(app.lambda_state.version_table.selected, 10);
11355        assert_eq!(app.page_input, ""); // Should be cleared
11356    }
11357
11358    #[test]
11359    fn test_lambda_versions_filter_input() {
11360        let mut app = test_app();
11361        app.current_service = Service::LambdaFunctions;
11362        app.service_selected = true;
11363        app.lambda_state.current_function = Some("test-function".to_string());
11364        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11365        app.mode = Mode::FilterInput;
11366        app.lambda_state.version_input_focus = InputFocus::Filter;
11367
11368        // Add test versions
11369        app.lambda_state.version_table.items = vec![
11370            LambdaVersion {
11371                version: "1".to_string(),
11372                aliases: "prod".to_string(),
11373                description: "Production".to_string(),
11374                last_modified: "".to_string(),
11375                architecture: "X86_64".to_string(),
11376            },
11377            LambdaVersion {
11378                version: "2".to_string(),
11379                aliases: "staging".to_string(),
11380                description: "Staging".to_string(),
11381                last_modified: "".to_string(),
11382                architecture: "X86_64".to_string(),
11383            },
11384        ];
11385
11386        // Type filter text
11387        app.handle_action(Action::FilterInput('p'));
11388        app.handle_action(Action::FilterInput('r'));
11389        app.handle_action(Action::FilterInput('o'));
11390        app.handle_action(Action::FilterInput('d'));
11391        assert_eq!(app.lambda_state.version_table.filter, "prod");
11392
11393        // Backspace should work
11394        app.handle_action(Action::FilterBackspace);
11395        assert_eq!(app.lambda_state.version_table.filter, "pro");
11396    }
11397
11398    #[test]
11399    fn test_lambda_aliases_table_expansion() {
11400        use crate::lambda::Alias;
11401
11402        let mut app = test_app();
11403        app.current_service = Service::LambdaFunctions;
11404        app.service_selected = true;
11405        app.lambda_state.current_function = Some("test-function".to_string());
11406        app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
11407        app.mode = Mode::Normal;
11408
11409        app.lambda_state.alias_table.items = vec![
11410            Alias {
11411                name: "prod".to_string(),
11412                versions: "1".to_string(),
11413                description: "Production alias".to_string(),
11414            },
11415            Alias {
11416                name: "staging".to_string(),
11417                versions: "2".to_string(),
11418                description: "Staging alias".to_string(),
11419            },
11420        ];
11421
11422        app.lambda_state.alias_table.selected = 0;
11423
11424        // Select first alias - should open alias detail view (no tab change)
11425        app.handle_action(Action::Select);
11426        assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
11427        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11428
11429        // Go back
11430        app.handle_action(Action::GoBack);
11431        assert_eq!(app.lambda_state.current_alias, None);
11432        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11433
11434        // Select second alias
11435        app.lambda_state.alias_table.selected = 1;
11436        app.handle_action(Action::Select);
11437        assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
11438    }
11439
11440    #[test]
11441    fn test_lambda_versions_arrow_key_expansion() {
11442        let mut app = test_app();
11443        app.current_service = Service::LambdaFunctions;
11444        app.service_selected = true;
11445        app.lambda_state.current_function = Some("test-function".to_string());
11446        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11447        app.mode = Mode::Normal;
11448
11449        app.lambda_state.version_table.items = vec![LambdaVersion {
11450            version: "1".to_string(),
11451            aliases: "prod".to_string(),
11452            description: "Production".to_string(),
11453            last_modified: "2024-01-01".to_string(),
11454            architecture: "X86_64".to_string(),
11455        }];
11456
11457        app.lambda_state.version_table.selected = 0;
11458
11459        // Right arrow expands
11460        app.handle_action(Action::NextPane);
11461        assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
11462
11463        // Left arrow collapses
11464        app.handle_action(Action::PrevPane);
11465        assert_eq!(app.lambda_state.version_table.expanded_item, None);
11466    }
11467
11468    #[test]
11469    fn test_lambda_version_detail_view() {
11470        use crate::lambda::Function;
11471
11472        let mut app = test_app();
11473        app.current_service = Service::LambdaFunctions;
11474        app.service_selected = true;
11475        app.lambda_state.current_function = Some("test-function".to_string());
11476        app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11477        app.mode = Mode::Normal;
11478
11479        app.lambda_state.table.items = vec![Function {
11480            name: "test-function".to_string(),
11481            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11482            application: None,
11483            description: "Test".to_string(),
11484            package_type: "Zip".to_string(),
11485            runtime: "python3.12".to_string(),
11486            architecture: "X86_64".to_string(),
11487            code_size: 1024,
11488            code_sha256: "hash".to_string(),
11489            memory_mb: 128,
11490            timeout_seconds: 30,
11491            last_modified: "2024-01-01".to_string(),
11492            layers: vec![],
11493        }];
11494
11495        app.lambda_state.version_table.items = vec![LambdaVersion {
11496            version: "1".to_string(),
11497            aliases: "prod".to_string(),
11498            description: "Production".to_string(),
11499            last_modified: "2024-01-01".to_string(),
11500            architecture: "X86_64".to_string(),
11501        }];
11502
11503        app.lambda_state.version_table.selected = 0;
11504
11505        // Select version to open detail view
11506        app.handle_action(Action::Select);
11507        assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11508        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
11509
11510        // GoBack should go back to versions list
11511        app.handle_action(Action::GoBack);
11512        assert_eq!(app.lambda_state.current_version, None);
11513        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11514    }
11515
11516    #[test]
11517    fn test_lambda_version_detail_tabs() {
11518        use crate::lambda::Function;
11519
11520        let mut app = test_app();
11521        app.current_service = Service::LambdaFunctions;
11522        app.service_selected = true;
11523        app.lambda_state.current_function = Some("test-function".to_string());
11524        app.lambda_state.current_version = Some("1".to_string());
11525        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11526        app.mode = Mode::Normal;
11527
11528        app.lambda_state.table.items = vec![Function {
11529            name: "test-function".to_string(),
11530            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11531            application: None,
11532            description: "Test".to_string(),
11533            package_type: "Zip".to_string(),
11534            runtime: "python3.12".to_string(),
11535            architecture: "X86_64".to_string(),
11536            code_size: 1024,
11537            code_sha256: "hash".to_string(),
11538            memory_mb: 128,
11539            timeout_seconds: 30,
11540            last_modified: "2024-01-01".to_string(),
11541            layers: vec![],
11542        }];
11543
11544        // Tab should cycle between Code, Monitor, and Configuration
11545        app.handle_action(Action::NextDetailTab);
11546        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
11547
11548        app.handle_action(Action::NextDetailTab);
11549        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
11550
11551        app.handle_action(Action::NextDetailTab);
11552        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
11553
11554        // BackTab should cycle backward
11555        app.handle_action(Action::PrevDetailTab);
11556        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
11557
11558        app.handle_action(Action::PrevDetailTab);
11559        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
11560    }
11561
11562    #[test]
11563    fn test_lambda_aliases_arrow_key_expansion() {
11564        use crate::lambda::Alias;
11565
11566        let mut app = test_app();
11567        app.current_service = Service::LambdaFunctions;
11568        app.service_selected = true;
11569        app.lambda_state.current_function = Some("test-function".to_string());
11570        app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
11571        app.mode = Mode::Normal;
11572
11573        app.lambda_state.alias_table.items = vec![Alias {
11574            name: "prod".to_string(),
11575            versions: "1".to_string(),
11576            description: "Production alias".to_string(),
11577        }];
11578
11579        app.lambda_state.alias_table.selected = 0;
11580
11581        // Right arrow expands
11582        app.handle_action(Action::NextPane);
11583        assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
11584
11585        // Left arrow collapses
11586        app.handle_action(Action::PrevPane);
11587        assert_eq!(app.lambda_state.alias_table.expanded_item, None);
11588    }
11589
11590    #[test]
11591    fn test_lambda_functions_arrow_key_expansion() {
11592        use crate::lambda::Function;
11593
11594        let mut app = test_app();
11595        app.current_service = Service::LambdaFunctions;
11596        app.service_selected = true;
11597        app.mode = Mode::Normal;
11598
11599        app.lambda_state.table.items = vec![Function {
11600            name: "test-function".to_string(),
11601            arn: "arn".to_string(),
11602            application: None,
11603            description: "Test".to_string(),
11604            package_type: "Zip".to_string(),
11605            runtime: "python3.12".to_string(),
11606            architecture: "X86_64".to_string(),
11607            code_size: 1024,
11608            code_sha256: "hash".to_string(),
11609            memory_mb: 128,
11610            timeout_seconds: 30,
11611            last_modified: "2024-01-01".to_string(),
11612            layers: vec![],
11613        }];
11614
11615        app.lambda_state.table.selected = 0;
11616
11617        // Right arrow expands
11618        app.handle_action(Action::NextPane);
11619        assert_eq!(app.lambda_state.table.expanded_item, Some(0));
11620
11621        // Left arrow collapses
11622        app.handle_action(Action::PrevPane);
11623        assert_eq!(app.lambda_state.table.expanded_item, None);
11624    }
11625
11626    #[test]
11627    fn test_lambda_version_detail_with_application() {
11628        use crate::lambda::Function;
11629
11630        let mut app = test_app();
11631        app.current_service = Service::LambdaFunctions;
11632        app.service_selected = true;
11633        app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
11634        app.lambda_state.current_version = Some("1".to_string());
11635        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11636        app.mode = Mode::Normal;
11637
11638        app.lambda_state.table.items = vec![Function {
11639            name: "storefront-studio-beta-api".to_string(),
11640            arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
11641                .to_string(),
11642            application: Some("storefront-studio-beta".to_string()),
11643            description: "API function".to_string(),
11644            package_type: "Zip".to_string(),
11645            runtime: "python3.12".to_string(),
11646            architecture: "X86_64".to_string(),
11647            code_size: 1024,
11648            code_sha256: "hash".to_string(),
11649            memory_mb: 128,
11650            timeout_seconds: 30,
11651            last_modified: "2024-01-01".to_string(),
11652            layers: vec![],
11653        }];
11654
11655        // Verify function has application extracted
11656        assert_eq!(
11657            app.lambda_state.table.items[0].application,
11658            Some("storefront-studio-beta".to_string())
11659        );
11660        assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11661    }
11662
11663    #[test]
11664    fn test_lambda_layer_navigation() {
11665        use crate::lambda::{Function, Layer};
11666
11667        let mut app = test_app();
11668        app.current_service = Service::LambdaFunctions;
11669        app.service_selected = true;
11670        app.lambda_state.current_function = Some("test-function".to_string());
11671        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11672        app.mode = Mode::Normal;
11673
11674        app.lambda_state.table.items = vec![Function {
11675            name: "test-function".to_string(),
11676            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11677            application: None,
11678            description: "Test".to_string(),
11679            package_type: "Zip".to_string(),
11680            runtime: "python3.12".to_string(),
11681            architecture: "X86_64".to_string(),
11682            code_size: 1024,
11683            code_sha256: "hash".to_string(),
11684            memory_mb: 128,
11685            timeout_seconds: 30,
11686            last_modified: "2024-01-01".to_string(),
11687            layers: vec![
11688                Layer {
11689                    merge_order: "1".to_string(),
11690                    name: "layer1".to_string(),
11691                    layer_version: "1".to_string(),
11692                    compatible_runtimes: "python3.9".to_string(),
11693                    compatible_architectures: "x86_64".to_string(),
11694                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11695                },
11696                Layer {
11697                    merge_order: "2".to_string(),
11698                    name: "layer2".to_string(),
11699                    layer_version: "2".to_string(),
11700                    compatible_runtimes: "python3.9".to_string(),
11701                    compatible_architectures: "x86_64".to_string(),
11702                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11703                },
11704                Layer {
11705                    merge_order: "3".to_string(),
11706                    name: "layer3".to_string(),
11707                    layer_version: "3".to_string(),
11708                    compatible_runtimes: "python3.9".to_string(),
11709                    compatible_architectures: "x86_64".to_string(),
11710                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
11711                },
11712            ],
11713        }];
11714
11715        assert_eq!(app.lambda_state.layer_selected, 0);
11716
11717        app.handle_action(Action::NextItem);
11718        assert_eq!(app.lambda_state.layer_selected, 1);
11719
11720        app.handle_action(Action::NextItem);
11721        assert_eq!(app.lambda_state.layer_selected, 2);
11722
11723        app.handle_action(Action::NextItem);
11724        assert_eq!(app.lambda_state.layer_selected, 2);
11725
11726        app.handle_action(Action::PrevItem);
11727        assert_eq!(app.lambda_state.layer_selected, 1);
11728
11729        app.handle_action(Action::PrevItem);
11730        assert_eq!(app.lambda_state.layer_selected, 0);
11731
11732        app.handle_action(Action::PrevItem);
11733        assert_eq!(app.lambda_state.layer_selected, 0);
11734    }
11735
11736    #[test]
11737    fn test_lambda_layer_expansion() {
11738        use crate::lambda::{Function, Layer};
11739
11740        let mut app = test_app();
11741        app.current_service = Service::LambdaFunctions;
11742        app.service_selected = true;
11743        app.lambda_state.current_function = Some("test-function".to_string());
11744        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11745        app.mode = Mode::Normal;
11746
11747        app.lambda_state.table.items = vec![Function {
11748            name: "test-function".to_string(),
11749            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11750            application: None,
11751            description: "Test".to_string(),
11752            package_type: "Zip".to_string(),
11753            runtime: "python3.12".to_string(),
11754            architecture: "X86_64".to_string(),
11755            code_size: 1024,
11756            code_sha256: "hash".to_string(),
11757            memory_mb: 128,
11758            timeout_seconds: 30,
11759            last_modified: "2024-01-01".to_string(),
11760            layers: vec![Layer {
11761                merge_order: "1".to_string(),
11762                name: "test-layer".to_string(),
11763                layer_version: "1".to_string(),
11764                compatible_runtimes: "python3.9".to_string(),
11765                compatible_architectures: "x86_64".to_string(),
11766                version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
11767            }],
11768        }];
11769
11770        assert_eq!(app.lambda_state.layer_expanded, None);
11771
11772        app.handle_action(Action::NextPane);
11773        assert_eq!(app.lambda_state.layer_expanded, Some(0));
11774
11775        app.handle_action(Action::PrevPane);
11776        assert_eq!(app.lambda_state.layer_expanded, None);
11777
11778        app.handle_action(Action::NextPane);
11779        assert_eq!(app.lambda_state.layer_expanded, Some(0));
11780
11781        app.handle_action(Action::NextPane);
11782        assert_eq!(app.lambda_state.layer_expanded, None);
11783    }
11784
11785    #[test]
11786    fn test_lambda_layer_selection_and_expansion_workflow() {
11787        use crate::lambda::{Function, Layer};
11788
11789        let mut app = test_app();
11790        app.current_service = Service::LambdaFunctions;
11791        app.service_selected = true;
11792        app.lambda_state.current_function = Some("test-function".to_string());
11793        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11794        app.mode = Mode::Normal;
11795
11796        app.lambda_state.table.items = vec![Function {
11797            name: "test-function".to_string(),
11798            arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11799            application: None,
11800            description: "Test".to_string(),
11801            package_type: "Zip".to_string(),
11802            runtime: "python3.12".to_string(),
11803            architecture: "X86_64".to_string(),
11804            code_size: 1024,
11805            code_sha256: "hash".to_string(),
11806            memory_mb: 128,
11807            timeout_seconds: 30,
11808            last_modified: "2024-01-01".to_string(),
11809            layers: vec![
11810                Layer {
11811                    merge_order: "1".to_string(),
11812                    name: "layer1".to_string(),
11813                    layer_version: "1".to_string(),
11814                    compatible_runtimes: "python3.9".to_string(),
11815                    compatible_architectures: "x86_64".to_string(),
11816                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11817                },
11818                Layer {
11819                    merge_order: "2".to_string(),
11820                    name: "layer2".to_string(),
11821                    layer_version: "2".to_string(),
11822                    compatible_runtimes: "python3.9".to_string(),
11823                    compatible_architectures: "x86_64".to_string(),
11824                    version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11825                },
11826            ],
11827        }];
11828
11829        // Start at layer 0
11830        assert_eq!(app.lambda_state.layer_selected, 0);
11831        assert_eq!(app.lambda_state.layer_expanded, None);
11832
11833        // Expand layer 0
11834        app.handle_action(Action::NextPane);
11835        assert_eq!(app.lambda_state.layer_selected, 0);
11836        assert_eq!(app.lambda_state.layer_expanded, Some(0));
11837
11838        // Navigate to layer 1 while layer 0 is expanded
11839        app.handle_action(Action::NextItem);
11840        assert_eq!(app.lambda_state.layer_selected, 1);
11841        assert_eq!(app.lambda_state.layer_expanded, Some(0)); // Still expanded
11842
11843        // Expand layer 1 (should collapse layer 0 and expand layer 1)
11844        app.handle_action(Action::NextPane);
11845        assert_eq!(app.lambda_state.layer_selected, 1);
11846        assert_eq!(app.lambda_state.layer_expanded, Some(1));
11847
11848        // Collapse layer 1
11849        app.handle_action(Action::PrevPane);
11850        assert_eq!(app.lambda_state.layer_selected, 1);
11851        assert_eq!(app.lambda_state.layer_expanded, None);
11852
11853        // Navigate back to layer 0
11854        app.handle_action(Action::PrevItem);
11855        assert_eq!(app.lambda_state.layer_selected, 0);
11856        assert_eq!(app.lambda_state.layer_expanded, None);
11857    }
11858
11859    #[test]
11860    fn test_backtab_cycles_detail_tabs_backward() {
11861        let mut app = test_app();
11862        app.mode = Mode::Normal;
11863
11864        // Test Lambda detail tabs
11865        app.current_service = Service::LambdaFunctions;
11866        app.service_selected = true;
11867        app.lambda_state.current_function = Some("test-function".to_string());
11868        app.lambda_state.detail_tab = LambdaDetailTab::Code;
11869
11870        app.handle_action(Action::PrevDetailTab);
11871        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11872
11873        app.handle_action(Action::PrevDetailTab);
11874        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11875
11876        // Test IAM Roles detail tabs
11877        app.current_service = Service::IamRoles;
11878        app.iam_state.current_role = Some("test-role".to_string());
11879        app.iam_state.role_tab = RoleTab::Permissions;
11880
11881        app.handle_action(Action::PrevDetailTab);
11882        assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
11883
11884        // Test IAM Users detail tabs
11885        app.current_service = Service::IamUsers;
11886        app.iam_state.current_user = Some("test-user".to_string());
11887        app.iam_state.user_tab = UserTab::Permissions;
11888
11889        app.handle_action(Action::PrevDetailTab);
11890        assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
11891
11892        // Test IAM Groups detail tabs
11893        app.current_service = Service::IamUserGroups;
11894        app.iam_state.current_group = Some("test-group".to_string());
11895        app.iam_state.group_tab = GroupTab::Permissions;
11896
11897        app.handle_action(Action::PrevDetailTab);
11898        assert_eq!(app.iam_state.group_tab, GroupTab::Users);
11899
11900        // Test S3 object tabs
11901        app.current_service = Service::S3Buckets;
11902        app.s3_state.current_bucket = Some("test-bucket".to_string());
11903        app.s3_state.object_tab = S3ObjectTab::Properties;
11904
11905        app.handle_action(Action::PrevDetailTab);
11906        assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
11907
11908        // Test ECR repository tabs (Private/Public)
11909        app.current_service = Service::EcrRepositories;
11910        app.ecr_state.current_repository = None;
11911        app.ecr_state.tab = EcrTab::Private;
11912
11913        app.handle_action(Action::PrevDetailTab);
11914        assert_eq!(app.ecr_state.tab, EcrTab::Public);
11915
11916        // Test CloudFormation detail tabs
11917        app.current_service = Service::CloudFormationStacks;
11918        app.cfn_state.current_stack = Some("test-stack".to_string());
11919        app.cfn_state.detail_tab = CfnDetailTab::Resources;
11920    }
11921
11922    #[test]
11923    fn test_cloudformation_status_filter_active() {
11924        let filter = CfnStatusFilter::Active;
11925        assert!(filter.matches("CREATE_IN_PROGRESS"));
11926        assert!(filter.matches("UPDATE_IN_PROGRESS"));
11927        assert!(!filter.matches("CREATE_COMPLETE"));
11928        assert!(!filter.matches("DELETE_COMPLETE"));
11929        assert!(!filter.matches("CREATE_FAILED"));
11930    }
11931
11932    #[test]
11933    fn test_cloudformation_status_filter_complete() {
11934        let filter = CfnStatusFilter::Complete;
11935        assert!(filter.matches("CREATE_COMPLETE"));
11936        assert!(filter.matches("UPDATE_COMPLETE"));
11937        assert!(!filter.matches("DELETE_COMPLETE"));
11938        assert!(!filter.matches("CREATE_IN_PROGRESS"));
11939    }
11940
11941    #[test]
11942    fn test_cloudformation_status_filter_failed() {
11943        let filter = CfnStatusFilter::Failed;
11944        assert!(filter.matches("CREATE_FAILED"));
11945        assert!(filter.matches("UPDATE_FAILED"));
11946        assert!(!filter.matches("CREATE_COMPLETE"));
11947    }
11948
11949    #[test]
11950    fn test_cloudformation_status_filter_deleted() {
11951        let filter = CfnStatusFilter::Deleted;
11952        assert!(filter.matches("DELETE_COMPLETE"));
11953        assert!(filter.matches("DELETE_IN_PROGRESS"));
11954        assert!(!filter.matches("CREATE_COMPLETE"));
11955    }
11956
11957    #[test]
11958    fn test_cloudformation_status_filter_in_progress() {
11959        let filter = CfnStatusFilter::InProgress;
11960        assert!(filter.matches("CREATE_IN_PROGRESS"));
11961        assert!(filter.matches("UPDATE_IN_PROGRESS"));
11962        assert!(filter.matches("DELETE_IN_PROGRESS"));
11963        assert!(!filter.matches("CREATE_COMPLETE"));
11964    }
11965
11966    #[test]
11967    fn test_cloudformation_status_filter_cycle() {
11968        let filter = CfnStatusFilter::All;
11969        assert_eq!(filter.next(), CfnStatusFilter::Active);
11970        assert_eq!(filter.next().next(), CfnStatusFilter::Complete);
11971        assert_eq!(filter.next().next().next(), CfnStatusFilter::Failed);
11972        assert_eq!(filter.next().next().next().next(), CfnStatusFilter::Deleted);
11973        assert_eq!(
11974            filter.next().next().next().next().next(),
11975            CfnStatusFilter::InProgress
11976        );
11977        assert_eq!(
11978            filter.next().next().next().next().next().next(),
11979            CfnStatusFilter::All
11980        );
11981    }
11982
11983    #[test]
11984    fn test_cloudformation_default_columns() {
11985        let app = test_app();
11986        assert_eq!(app.cfn_visible_column_ids.len(), 4);
11987        assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
11988        assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
11989        assert!(app
11990            .cfn_visible_column_ids
11991            .contains(&CfnColumn::CreatedTime.id()));
11992        assert!(app
11993            .cfn_visible_column_ids
11994            .contains(&CfnColumn::Description.id()));
11995    }
11996
11997    #[test]
11998    fn test_cloudformation_all_columns() {
11999        let app = test_app();
12000        assert_eq!(app.cfn_column_ids.len(), 10);
12001    }
12002
12003    #[test]
12004    fn test_cloudformation_filter_by_name() {
12005        let mut app = test_app();
12006        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12007        app.cfn_state.table.items = vec![
12008            CfnStack {
12009                name: "my-stack".to_string(),
12010                stack_id: "id1".to_string(),
12011                status: "CREATE_COMPLETE".to_string(),
12012                created_time: "2024-01-01".to_string(),
12013                updated_time: String::new(),
12014                deleted_time: String::new(),
12015                drift_status: String::new(),
12016                last_drift_check_time: String::new(),
12017                status_reason: String::new(),
12018                description: String::new(),
12019                detailed_status: String::new(),
12020                root_stack: String::new(),
12021                parent_stack: String::new(),
12022                termination_protection: false,
12023                iam_role: String::new(),
12024                tags: Vec::new(),
12025                stack_policy: String::new(),
12026                rollback_monitoring_time: String::new(),
12027                rollback_alarms: Vec::new(),
12028                notification_arns: Vec::new(),
12029            },
12030            CfnStack {
12031                name: "other-stack".to_string(),
12032                stack_id: "id2".to_string(),
12033                status: "CREATE_COMPLETE".to_string(),
12034                created_time: "2024-01-02".to_string(),
12035                updated_time: String::new(),
12036                deleted_time: String::new(),
12037                drift_status: String::new(),
12038                last_drift_check_time: String::new(),
12039                status_reason: String::new(),
12040                description: String::new(),
12041                detailed_status: String::new(),
12042                root_stack: String::new(),
12043                parent_stack: String::new(),
12044                termination_protection: false,
12045                iam_role: String::new(),
12046                tags: Vec::new(),
12047                stack_policy: String::new(),
12048                rollback_monitoring_time: String::new(),
12049                rollback_alarms: Vec::new(),
12050                notification_arns: Vec::new(),
12051            },
12052        ];
12053
12054        app.cfn_state.table.filter = "my".to_string();
12055        let filtered = filtered_cloudformation_stacks(&app);
12056        assert_eq!(filtered.len(), 1);
12057        assert_eq!(filtered[0].name, "my-stack");
12058    }
12059
12060    #[test]
12061    fn test_cloudformation_filter_by_description() {
12062        let mut app = test_app();
12063        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12064        app.cfn_state.table.items = vec![CfnStack {
12065            name: "stack1".to_string(),
12066            stack_id: "id1".to_string(),
12067            status: "CREATE_COMPLETE".to_string(),
12068            created_time: "2024-01-01".to_string(),
12069            updated_time: String::new(),
12070            deleted_time: String::new(),
12071            drift_status: String::new(),
12072            last_drift_check_time: String::new(),
12073            status_reason: String::new(),
12074            description: "production stack".to_string(),
12075            detailed_status: String::new(),
12076            root_stack: String::new(),
12077            parent_stack: String::new(),
12078            termination_protection: false,
12079            iam_role: String::new(),
12080            tags: Vec::new(),
12081            stack_policy: String::new(),
12082            rollback_monitoring_time: String::new(),
12083            rollback_alarms: Vec::new(),
12084            notification_arns: Vec::new(),
12085        }];
12086
12087        app.cfn_state.table.filter = "production".to_string();
12088        let filtered = filtered_cloudformation_stacks(&app);
12089        assert_eq!(filtered.len(), 1);
12090    }
12091
12092    #[test]
12093    fn test_cloudformation_status_filter_applied() {
12094        let mut app = test_app();
12095        app.cfn_state.table.items = vec![
12096            CfnStack {
12097                name: "complete-stack".to_string(),
12098                stack_id: "id1".to_string(),
12099                status: "CREATE_COMPLETE".to_string(),
12100                created_time: "2024-01-01".to_string(),
12101                updated_time: String::new(),
12102                deleted_time: String::new(),
12103                drift_status: String::new(),
12104                last_drift_check_time: String::new(),
12105                status_reason: String::new(),
12106                description: String::new(),
12107                detailed_status: String::new(),
12108                root_stack: String::new(),
12109                parent_stack: String::new(),
12110                termination_protection: false,
12111                iam_role: String::new(),
12112                tags: Vec::new(),
12113                stack_policy: String::new(),
12114                rollback_monitoring_time: String::new(),
12115                rollback_alarms: Vec::new(),
12116                notification_arns: Vec::new(),
12117            },
12118            CfnStack {
12119                name: "failed-stack".to_string(),
12120                stack_id: "id2".to_string(),
12121                status: "CREATE_FAILED".to_string(),
12122                created_time: "2024-01-02".to_string(),
12123                updated_time: String::new(),
12124                deleted_time: String::new(),
12125                drift_status: String::new(),
12126                last_drift_check_time: String::new(),
12127                status_reason: String::new(),
12128                description: String::new(),
12129                detailed_status: String::new(),
12130                root_stack: String::new(),
12131                parent_stack: String::new(),
12132                termination_protection: false,
12133                iam_role: String::new(),
12134                tags: Vec::new(),
12135                stack_policy: String::new(),
12136                rollback_monitoring_time: String::new(),
12137                rollback_alarms: Vec::new(),
12138                notification_arns: Vec::new(),
12139            },
12140        ];
12141
12142        app.cfn_state.status_filter = CfnStatusFilter::Complete;
12143        let filtered = filtered_cloudformation_stacks(&app);
12144        assert_eq!(filtered.len(), 1);
12145        assert_eq!(filtered[0].name, "complete-stack");
12146
12147        app.cfn_state.status_filter = CfnStatusFilter::Failed;
12148        let filtered = filtered_cloudformation_stacks(&app);
12149        assert_eq!(filtered.len(), 1);
12150        assert_eq!(filtered[0].name, "failed-stack");
12151    }
12152
12153    #[test]
12154    fn test_cloudformation_default_page_size() {
12155        let app = test_app();
12156        assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
12157    }
12158
12159    #[test]
12160    fn test_cloudformation_default_status_filter() {
12161        let app = test_app();
12162        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
12163    }
12164
12165    #[test]
12166    fn test_cloudformation_view_nested_default_false() {
12167        let app = test_app();
12168        assert!(!app.cfn_state.view_nested);
12169    }
12170
12171    #[test]
12172    fn test_cloudformation_pagination_hotkeys() {
12173        let mut app = test_app();
12174        app.current_service = Service::CloudFormationStacks;
12175        app.service_selected = true;
12176        app.cfn_state.status_filter = CfnStatusFilter::All;
12177
12178        // Add 150 stacks
12179        for i in 0..150 {
12180            app.cfn_state.table.items.push(CfnStack {
12181                name: format!("stack-{}", i),
12182                stack_id: format!("id-{}", i),
12183                status: "CREATE_COMPLETE".to_string(),
12184                created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12185                updated_time: String::new(),
12186                deleted_time: String::new(),
12187                drift_status: String::new(),
12188                last_drift_check_time: String::new(),
12189                status_reason: String::new(),
12190                description: String::new(),
12191                detailed_status: String::new(),
12192                root_stack: String::new(),
12193                parent_stack: String::new(),
12194                termination_protection: false,
12195                iam_role: String::new(),
12196                tags: vec![],
12197                stack_policy: String::new(),
12198                rollback_monitoring_time: String::new(),
12199                rollback_alarms: vec![],
12200                notification_arns: vec![],
12201            });
12202        }
12203
12204        // Go to page 2
12205        app.go_to_page(2);
12206        assert_eq!(app.cfn_state.table.selected, 50);
12207
12208        // Go to page 3
12209        app.go_to_page(3);
12210        assert_eq!(app.cfn_state.table.selected, 100);
12211
12212        // Go to page 1
12213        app.go_to_page(1);
12214        assert_eq!(app.cfn_state.table.selected, 0);
12215    }
12216
12217    #[test]
12218    fn test_cloudformation_tab_cycling_in_filter_mode() {
12219        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12220        let mut app = test_app();
12221        app.current_service = Service::CloudFormationStacks;
12222        app.service_selected = true;
12223        app.mode = Mode::FilterInput;
12224        app.cfn_state.input_focus = InputFocus::Filter;
12225
12226        // Tab to StatusFilter
12227        app.handle_action(Action::NextFilterFocus);
12228        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12229
12230        // Tab to ViewNested
12231        app.handle_action(Action::NextFilterFocus);
12232        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12233
12234        // Tab to Pagination
12235        app.handle_action(Action::NextFilterFocus);
12236        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12237
12238        // Tab back to Filter
12239        app.handle_action(Action::NextFilterFocus);
12240        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12241    }
12242
12243    #[test]
12244    fn test_cloudformation_timestamp_format_includes_utc() {
12245        let stack = CfnStack {
12246            name: "test-stack".to_string(),
12247            stack_id: "id-123".to_string(),
12248            status: "CREATE_COMPLETE".to_string(),
12249            created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
12250            updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
12251            deleted_time: String::new(),
12252            drift_status: String::new(),
12253            last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
12254            status_reason: String::new(),
12255            description: String::new(),
12256            detailed_status: String::new(),
12257            root_stack: String::new(),
12258            parent_stack: String::new(),
12259            termination_protection: false,
12260            iam_role: String::new(),
12261            tags: vec![],
12262            stack_policy: String::new(),
12263            rollback_monitoring_time: String::new(),
12264            rollback_alarms: vec![],
12265            notification_arns: vec![],
12266        };
12267
12268        assert!(stack.created_time.contains("(UTC)"));
12269        assert!(stack.updated_time.contains("(UTC)"));
12270        assert!(stack.last_drift_check_time.contains("(UTC)"));
12271        assert_eq!(stack.created_time.len(), 25);
12272    }
12273
12274    #[test]
12275    fn test_cloudformation_enter_drills_into_stack_view() {
12276        let mut app = test_app();
12277        app.current_service = Service::CloudFormationStacks;
12278        app.service_selected = true;
12279        app.mode = Mode::Normal;
12280        app.cfn_state.status_filter = CfnStatusFilter::All;
12281        app.tabs = vec![Tab {
12282            service: Service::CloudFormationStacks,
12283            title: "CloudFormation › Stacks".to_string(),
12284            breadcrumb: "CloudFormation › Stacks".to_string(),
12285        }];
12286        app.current_tab = 0;
12287
12288        app.cfn_state.table.items.push(CfnStack {
12289            name: "test-stack".to_string(),
12290            stack_id: "id-123".to_string(),
12291            status: "CREATE_COMPLETE".to_string(),
12292            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12293            updated_time: String::new(),
12294            deleted_time: String::new(),
12295            drift_status: String::new(),
12296            last_drift_check_time: String::new(),
12297            status_reason: String::new(),
12298            description: String::new(),
12299            detailed_status: String::new(),
12300            root_stack: String::new(),
12301            parent_stack: String::new(),
12302            termination_protection: false,
12303            iam_role: String::new(),
12304            tags: vec![],
12305            stack_policy: String::new(),
12306            rollback_monitoring_time: String::new(),
12307            rollback_alarms: vec![],
12308            notification_arns: vec![],
12309        });
12310
12311        app.cfn_state.table.reset();
12312        assert_eq!(app.cfn_state.current_stack, None);
12313
12314        // Press Enter - should drill into stack detail view
12315        app.handle_action(Action::Select);
12316        assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
12317    }
12318
12319    #[test]
12320    fn test_cloudformation_arrow_keys_expand_collapse() {
12321        let mut app = test_app();
12322        app.current_service = Service::CloudFormationStacks;
12323        app.service_selected = true;
12324        app.mode = Mode::Normal;
12325        app.cfn_state.status_filter = CfnStatusFilter::All;
12326
12327        app.cfn_state.table.items.push(CfnStack {
12328            name: "test-stack".to_string(),
12329            stack_id: "id-123".to_string(),
12330            status: "CREATE_COMPLETE".to_string(),
12331            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12332            updated_time: String::new(),
12333            deleted_time: String::new(),
12334            drift_status: String::new(),
12335            last_drift_check_time: String::new(),
12336            status_reason: String::new(),
12337            description: String::new(),
12338            detailed_status: String::new(),
12339            root_stack: String::new(),
12340            parent_stack: String::new(),
12341            termination_protection: false,
12342            iam_role: String::new(),
12343            tags: vec![],
12344            stack_policy: String::new(),
12345            rollback_monitoring_time: String::new(),
12346            rollback_alarms: vec![],
12347            notification_arns: vec![],
12348        });
12349
12350        app.cfn_state.table.reset();
12351        assert_eq!(app.cfn_state.table.expanded_item, None);
12352
12353        // Right arrow - should expand
12354        app.handle_action(Action::NextPane);
12355        assert_eq!(app.cfn_state.table.expanded_item, Some(0));
12356
12357        // Left arrow - should collapse
12358        app.handle_action(Action::PrevPane);
12359        assert_eq!(app.cfn_state.table.expanded_item, None);
12360
12361        // Verify current_stack is still None (not drilled in)
12362        assert_eq!(app.cfn_state.current_stack, None);
12363    }
12364
12365    #[test]
12366    fn test_cloudformation_tab_cycling() {
12367        use crate::ui::cfn::DetailTab;
12368        let mut app = test_app();
12369        app.current_service = Service::CloudFormationStacks;
12370        app.service_selected = true;
12371        app.mode = Mode::Normal;
12372        app.cfn_state.status_filter = CfnStatusFilter::All;
12373        app.cfn_state.current_stack = Some("test-stack".to_string());
12374
12375        assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
12376    }
12377
12378    #[test]
12379    fn test_cloudformation_console_url() {
12380        use crate::ui::cfn::DetailTab;
12381        let mut app = test_app();
12382        app.current_service = Service::CloudFormationStacks;
12383        app.service_selected = true;
12384        app.cfn_state.status_filter = CfnStatusFilter::All;
12385
12386        app.cfn_state.table.items.push(CfnStack {
12387            name: "test-stack".to_string(),
12388            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12389                .to_string(),
12390            status: "CREATE_COMPLETE".to_string(),
12391            created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12392            updated_time: String::new(),
12393            deleted_time: String::new(),
12394            drift_status: String::new(),
12395            last_drift_check_time: String::new(),
12396            status_reason: String::new(),
12397            description: String::new(),
12398            detailed_status: String::new(),
12399            root_stack: String::new(),
12400            parent_stack: String::new(),
12401            termination_protection: false,
12402            iam_role: String::new(),
12403            tags: vec![],
12404            stack_policy: String::new(),
12405            rollback_monitoring_time: String::new(),
12406            rollback_alarms: vec![],
12407            notification_arns: vec![],
12408        });
12409
12410        app.cfn_state.current_stack = Some("test-stack".to_string());
12411
12412        // Stack info URL
12413        app.cfn_state.detail_tab = DetailTab::StackInfo;
12414        let url = app.get_console_url();
12415        assert!(url.contains("stackinfo"));
12416        assert!(url.contains("arn%3Aaws%3Acloudformation"));
12417
12418        // Events URL
12419        app.cfn_state.detail_tab = DetailTab::Events;
12420        let url = app.get_console_url();
12421        assert!(url.contains("events"));
12422        assert!(url.contains("arn%3Aaws%3Acloudformation"));
12423    }
12424
12425    #[test]
12426    fn test_iam_role_select() {
12427        let mut app = test_app();
12428        app.current_service = Service::IamRoles;
12429        app.service_selected = true;
12430        app.mode = Mode::Normal;
12431
12432        app.iam_state.roles.items = vec![
12433            IamRole {
12434                role_name: "role1".to_string(),
12435                path: "/".to_string(),
12436                trusted_entities: "AWS Service: ec2".to_string(),
12437                last_activity: "-".to_string(),
12438                arn: "arn:aws:iam::123456789012:role/role1".to_string(),
12439                creation_time: "2025-01-01".to_string(),
12440                description: "Test role 1".to_string(),
12441                max_session_duration: Some(3600),
12442            },
12443            IamRole {
12444                role_name: "role2".to_string(),
12445                path: "/".to_string(),
12446                trusted_entities: "AWS Service: lambda".to_string(),
12447                last_activity: "-".to_string(),
12448                arn: "arn:aws:iam::123456789012:role/role2".to_string(),
12449                creation_time: "2025-01-02".to_string(),
12450                description: "Test role 2".to_string(),
12451                max_session_duration: Some(7200),
12452            },
12453        ];
12454
12455        // Select first role
12456        app.iam_state.roles.selected = 0;
12457        app.handle_action(Action::Select);
12458
12459        assert_eq!(
12460            app.iam_state.current_role,
12461            Some("role1".to_string()),
12462            "Should open role detail view"
12463        );
12464        assert_eq!(
12465            app.iam_state.role_tab,
12466            RoleTab::Permissions,
12467            "Should default to Permissions tab"
12468        );
12469    }
12470
12471    #[test]
12472    fn test_iam_role_back_navigation() {
12473        let mut app = test_app();
12474        app.current_service = Service::IamRoles;
12475        app.service_selected = true;
12476        app.iam_state.current_role = Some("test-role".to_string());
12477
12478        app.handle_action(Action::GoBack);
12479
12480        assert_eq!(
12481            app.iam_state.current_role, None,
12482            "Should return to roles list"
12483        );
12484    }
12485
12486    #[test]
12487    fn test_iam_role_tab_navigation() {
12488        let mut app = test_app();
12489        app.current_service = Service::IamRoles;
12490        app.service_selected = true;
12491        app.iam_state.current_role = Some("test-role".to_string());
12492        app.iam_state.role_tab = RoleTab::Permissions;
12493
12494        app.handle_action(Action::NextDetailTab);
12495
12496        assert_eq!(
12497            app.iam_state.role_tab,
12498            RoleTab::TrustRelationships,
12499            "Should move to next tab"
12500        );
12501    }
12502
12503    #[test]
12504    fn test_iam_role_tab_cycle_order() {
12505        let mut app = test_app();
12506        app.current_service = Service::IamRoles;
12507        app.service_selected = true;
12508        app.iam_state.current_role = Some("test-role".to_string());
12509        app.iam_state.role_tab = RoleTab::Permissions;
12510
12511        app.handle_action(Action::NextDetailTab);
12512        assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
12513
12514        app.handle_action(Action::NextDetailTab);
12515        assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
12516
12517        app.handle_action(Action::NextDetailTab);
12518        assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
12519
12520        app.handle_action(Action::NextDetailTab);
12521        assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
12522
12523        app.handle_action(Action::NextDetailTab);
12524        assert_eq!(
12525            app.iam_state.role_tab,
12526            RoleTab::Permissions,
12527            "Should cycle back to first tab"
12528        );
12529    }
12530
12531    #[test]
12532    fn test_iam_role_pagination() {
12533        let mut app = test_app();
12534        app.current_service = Service::IamRoles;
12535        app.service_selected = true;
12536        app.iam_state.roles.page_size = PageSize::Ten;
12537
12538        app.iam_state.roles.items = (0..25)
12539            .map(|i| IamRole {
12540                role_name: format!("role{}", i),
12541                path: "/".to_string(),
12542                trusted_entities: "AWS Service: ec2".to_string(),
12543                last_activity: "-".to_string(),
12544                arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12545                creation_time: "2025-01-01".to_string(),
12546                description: format!("Test role {}", i),
12547                max_session_duration: Some(3600),
12548            })
12549            .collect();
12550
12551        // Jump to page 2
12552        app.go_to_page(2);
12553
12554        assert_eq!(
12555            app.iam_state.roles.selected, 10,
12556            "Should select first item of page 2"
12557        );
12558        assert_eq!(
12559            app.iam_state.roles.scroll_offset, 10,
12560            "Should update scroll offset"
12561        );
12562    }
12563
12564    #[test]
12565    fn test_tags_table_populated_on_role_detail() {
12566        let mut app = test_app();
12567        app.current_service = Service::IamRoles;
12568        app.service_selected = true;
12569        app.mode = Mode::Normal;
12570        app.iam_state.roles.items = vec![IamRole {
12571            role_name: "TestRole".to_string(),
12572            path: "/".to_string(),
12573            trusted_entities: String::new(),
12574            last_activity: String::new(),
12575            arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
12576            creation_time: "2025-01-01".to_string(),
12577            description: String::new(),
12578            max_session_duration: Some(3600),
12579        }];
12580
12581        // Manually populate tags to test table rendering
12582        app.iam_state.tags.items = vec![
12583            IamRoleTag {
12584                key: "Environment".to_string(),
12585                value: "Production".to_string(),
12586            },
12587            IamRoleTag {
12588                key: "Team".to_string(),
12589                value: "Platform".to_string(),
12590            },
12591        ];
12592
12593        assert_eq!(app.iam_state.tags.items.len(), 2);
12594        assert_eq!(app.iam_state.tags.items[0].key, "Environment");
12595        assert_eq!(app.iam_state.tags.items[0].value, "Production");
12596        assert_eq!(app.iam_state.tags.selected, 0);
12597    }
12598
12599    #[test]
12600    fn test_tags_table_navigation() {
12601        let mut app = test_app();
12602        app.current_service = Service::IamRoles;
12603        app.service_selected = true;
12604        app.mode = Mode::Normal;
12605        app.iam_state.current_role = Some("TestRole".to_string());
12606        app.iam_state.role_tab = RoleTab::Tags;
12607        app.iam_state.tags.items = vec![
12608            IamRoleTag {
12609                key: "Tag1".to_string(),
12610                value: "Value1".to_string(),
12611            },
12612            IamRoleTag {
12613                key: "Tag2".to_string(),
12614                value: "Value2".to_string(),
12615            },
12616        ];
12617
12618        app.handle_action(Action::NextItem);
12619        assert_eq!(app.iam_state.tags.selected, 1);
12620
12621        app.handle_action(Action::PrevItem);
12622        assert_eq!(app.iam_state.tags.selected, 0);
12623    }
12624
12625    #[test]
12626    fn test_last_accessed_table_navigation() {
12627        let mut app = test_app();
12628        app.current_service = Service::IamRoles;
12629        app.service_selected = true;
12630        app.mode = Mode::Normal;
12631        app.iam_state.current_role = Some("TestRole".to_string());
12632        app.iam_state.role_tab = RoleTab::LastAccessed;
12633        app.iam_state.last_accessed_services.items = vec![
12634            LastAccessedService {
12635                service: "S3".to_string(),
12636                policies_granting: "Policy1".to_string(),
12637                last_accessed: "2025-01-01".to_string(),
12638            },
12639            LastAccessedService {
12640                service: "EC2".to_string(),
12641                policies_granting: "Policy2".to_string(),
12642                last_accessed: "2025-01-02".to_string(),
12643            },
12644        ];
12645
12646        app.handle_action(Action::NextItem);
12647        assert_eq!(app.iam_state.last_accessed_services.selected, 1);
12648
12649        app.handle_action(Action::PrevItem);
12650        assert_eq!(app.iam_state.last_accessed_services.selected, 0);
12651    }
12652
12653    #[test]
12654    fn test_cfn_input_focus_next() {
12655        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12656        let mut app = test_app();
12657        app.current_service = Service::CloudFormationStacks;
12658        app.mode = Mode::FilterInput;
12659        app.cfn_state.input_focus = InputFocus::Filter;
12660
12661        app.handle_action(Action::NextFilterFocus);
12662        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12663
12664        app.handle_action(Action::NextFilterFocus);
12665        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12666
12667        app.handle_action(Action::NextFilterFocus);
12668        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12669
12670        app.handle_action(Action::NextFilterFocus);
12671        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12672    }
12673
12674    #[test]
12675    fn test_cfn_input_focus_prev() {
12676        use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12677        let mut app = test_app();
12678        app.current_service = Service::CloudFormationStacks;
12679        app.mode = Mode::FilterInput;
12680        app.cfn_state.input_focus = InputFocus::Filter;
12681
12682        app.handle_action(Action::PrevFilterFocus);
12683        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12684
12685        app.handle_action(Action::PrevFilterFocus);
12686        assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12687
12688        app.handle_action(Action::PrevFilterFocus);
12689        assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12690
12691        app.handle_action(Action::PrevFilterFocus);
12692        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12693    }
12694
12695    #[test]
12696    fn test_cw_logs_input_focus_prev() {
12697        let mut app = test_app();
12698        app.current_service = Service::CloudWatchLogGroups;
12699        app.mode = Mode::FilterInput;
12700        app.view_mode = ViewMode::Detail;
12701        app.log_groups_state.detail_tab = CwLogsDetailTab::LogStreams;
12702        app.log_groups_state.input_focus = InputFocus::Filter;
12703
12704        app.handle_action(Action::PrevFilterFocus);
12705        assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
12706
12707        app.handle_action(Action::PrevFilterFocus);
12708        assert_eq!(
12709            app.log_groups_state.input_focus,
12710            InputFocus::Checkbox("ShowExpired")
12711        );
12712
12713        app.handle_action(Action::PrevFilterFocus);
12714        assert_eq!(
12715            app.log_groups_state.input_focus,
12716            InputFocus::Checkbox("ExactMatch")
12717        );
12718
12719        app.handle_action(Action::PrevFilterFocus);
12720        assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
12721    }
12722
12723    #[test]
12724    fn test_cw_events_input_focus_prev() {
12725        use crate::ui::cw::logs::EventFilterFocus;
12726        let mut app = test_app();
12727        app.mode = Mode::EventFilterInput;
12728        app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
12729
12730        app.handle_action(Action::PrevFilterFocus);
12731        assert_eq!(
12732            app.log_groups_state.event_input_focus,
12733            EventFilterFocus::DateRange
12734        );
12735
12736        app.handle_action(Action::PrevFilterFocus);
12737        assert_eq!(
12738            app.log_groups_state.event_input_focus,
12739            EventFilterFocus::Filter
12740        );
12741    }
12742
12743    #[test]
12744    fn test_cfn_input_focus_cycle_complete() {
12745        let mut app = test_app();
12746        app.current_service = Service::CloudFormationStacks;
12747        app.mode = Mode::FilterInput;
12748        app.cfn_state.input_focus = InputFocus::Filter;
12749
12750        // Cycle forward through all controls
12751        for _ in 0..4 {
12752            app.handle_action(Action::NextFilterFocus);
12753        }
12754        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12755
12756        // Cycle backward through all controls
12757        for _ in 0..4 {
12758            app.handle_action(Action::PrevFilterFocus);
12759        }
12760        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12761    }
12762
12763    #[test]
12764    fn test_cfn_resources_pagination_left_right_arrows() {
12765        // Regression: Resources tab has a Pagination focus in filter controls,
12766        // but PageDown/PageUp in FilterInput mode didn't handle Resources —
12767        // they fell through to the stacks table instead.
12768        let mut app = test_app();
12769        app.current_service = Service::CloudFormationStacks;
12770        app.service_selected = true;
12771        app.mode = Mode::FilterInput;
12772        app.cfn_state.current_stack = Some("test-stack".to_string());
12773        app.cfn_state.detail_tab = CfnDetailTab::Resources;
12774        app.cfn_state.resources_input_focus = InputFocus::Pagination;
12775
12776        // 60 resources with page size 10 = 6 pages
12777        app.cfn_state.resources.items = (0..60)
12778            .map(|i| rusticity_core::cfn::StackResource {
12779                logical_id: format!("Resource{}", i),
12780                physical_id: format!("phys{}", i),
12781                resource_type: "AWS::S3::Bucket".to_string(),
12782                status: String::new(),
12783                module_info: String::new(),
12784            })
12785            .collect();
12786        app.cfn_state.resources.page_size = crate::common::PageSize::Ten;
12787        app.cfn_state.resources.selected = 0;
12788
12789        // Right arrow (PageDown) must go to page 2
12790        app.handle_action(Action::PageDown);
12791        assert_eq!(
12792            app.cfn_state.resources.selected, 10,
12793            "Right arrow with pagination focus must move to page 2 of Resources"
12794        );
12795
12796        // Left arrow (PageUp) must go back to page 1
12797        app.handle_action(Action::PageUp);
12798        assert_eq!(
12799            app.cfn_state.resources.selected, 0,
12800            "Left arrow with pagination focus must move back to page 1 of Resources"
12801        );
12802    }
12803
12804    #[test]
12805    fn test_cfn_filter_status_arrow_keys() {
12806        use crate::ui::cfn::STATUS_FILTER;
12807        let mut app = test_app();
12808        app.current_service = Service::CloudFormationStacks;
12809        app.mode = Mode::FilterInput;
12810        app.cfn_state.input_focus = STATUS_FILTER;
12811        app.cfn_state.status_filter = CfnStatusFilter::All;
12812
12813        app.handle_action(Action::NextItem);
12814        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
12815
12816        app.handle_action(Action::PrevItem);
12817        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
12818    }
12819
12820    #[test]
12821    fn test_cfn_filter_shift_tab_cycles_backward() {
12822        use crate::ui::cfn::STATUS_FILTER;
12823        let mut app = test_app();
12824        app.current_service = Service::CloudFormationStacks;
12825        app.mode = Mode::FilterInput;
12826        app.cfn_state.input_focus = STATUS_FILTER;
12827
12828        // Shift+Tab should go backward
12829        app.handle_action(Action::PrevFilterFocus);
12830        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12831
12832        // From Input, Shift+Tab should wrap to Pagination
12833        app.handle_action(Action::PrevFilterFocus);
12834        assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12835    }
12836
12837    #[test]
12838    fn test_cfn_pagination_arrow_keys() {
12839        let mut app = test_app();
12840        app.current_service = Service::CloudFormationStacks;
12841        app.mode = Mode::FilterInput;
12842        app.cfn_state.input_focus = InputFocus::Pagination;
12843        app.cfn_state.table.scroll_offset = 0;
12844        app.cfn_state.table.page_size = PageSize::Ten;
12845
12846        // Add some test stacks
12847        app.cfn_state.table.items = (0..30)
12848            .map(|i| CfnStack {
12849                name: format!("stack-{}", i),
12850                stack_id: format!("id-{}", i),
12851                status: "CREATE_COMPLETE".to_string(),
12852                created_time: "2024-01-01".to_string(),
12853                updated_time: String::new(),
12854                deleted_time: String::new(),
12855                drift_status: String::new(),
12856                last_drift_check_time: String::new(),
12857                status_reason: String::new(),
12858                description: String::new(),
12859                detailed_status: String::new(),
12860                root_stack: String::new(),
12861                parent_stack: String::new(),
12862                termination_protection: false,
12863                iam_role: String::new(),
12864                tags: Vec::new(),
12865                stack_policy: String::new(),
12866                rollback_monitoring_time: String::new(),
12867                rollback_alarms: Vec::new(),
12868                notification_arns: Vec::new(),
12869            })
12870            .collect();
12871
12872        // Right arrow should page forward
12873        app.handle_action(Action::PageDown);
12874        assert_eq!(app.cfn_state.table.scroll_offset, 10);
12875        // Verify page number calculation
12876        let page_size = app.cfn_state.table.page_size.value();
12877        let current_page = app.cfn_state.table.scroll_offset / page_size;
12878        assert_eq!(current_page, 1);
12879
12880        // Left arrow should page backward
12881        app.handle_action(Action::PageUp);
12882        assert_eq!(app.cfn_state.table.scroll_offset, 0);
12883        let current_page = app.cfn_state.table.scroll_offset / page_size;
12884        assert_eq!(current_page, 0);
12885    }
12886
12887    #[test]
12888    fn test_cfn_page_navigation_updates_selection() {
12889        let mut app = test_app();
12890        app.current_service = Service::CloudFormationStacks;
12891        app.mode = Mode::Normal;
12892
12893        // Add 30 test stacks
12894        app.cfn_state.table.items = (0..30)
12895            .map(|i| CfnStack {
12896                name: format!("stack-{}", i),
12897                stack_id: format!("id-{}", i),
12898                status: "CREATE_COMPLETE".to_string(),
12899                created_time: "2024-01-01".to_string(),
12900                updated_time: String::new(),
12901                deleted_time: String::new(),
12902                drift_status: String::new(),
12903                last_drift_check_time: String::new(),
12904                status_reason: String::new(),
12905                description: String::new(),
12906                detailed_status: String::new(),
12907                root_stack: String::new(),
12908                parent_stack: String::new(),
12909                termination_protection: false,
12910                iam_role: String::new(),
12911                tags: Vec::new(),
12912                stack_policy: String::new(),
12913                rollback_monitoring_time: String::new(),
12914                rollback_alarms: Vec::new(),
12915                notification_arns: Vec::new(),
12916            })
12917            .collect();
12918
12919        app.cfn_state.table.reset();
12920        app.cfn_state.table.scroll_offset = 0;
12921
12922        // Page down should update selection
12923        app.handle_action(Action::PageDown);
12924        assert_eq!(app.cfn_state.table.selected, 10);
12925
12926        // Page down again
12927        app.handle_action(Action::PageDown);
12928        assert_eq!(app.cfn_state.table.selected, 20);
12929
12930        // Page up should update selection
12931        app.handle_action(Action::PageUp);
12932        assert_eq!(app.cfn_state.table.selected, 10);
12933    }
12934
12935    #[test]
12936    fn test_cfn_filter_input_only_when_focused() {
12937        use crate::ui::cfn::STATUS_FILTER;
12938        let mut app = test_app();
12939        app.current_service = Service::CloudFormationStacks;
12940        app.mode = Mode::FilterInput;
12941        app.cfn_state.input_focus = STATUS_FILTER;
12942        app.cfn_state.table.filter = String::new();
12943
12944        // Typing should not add to filter when focus is not on Input
12945        app.handle_action(Action::FilterInput('t'));
12946        app.handle_action(Action::FilterInput('e'));
12947        app.handle_action(Action::FilterInput('s'));
12948        app.handle_action(Action::FilterInput('t'));
12949        assert_eq!(app.cfn_state.table.filter, "");
12950
12951        // Switch to Input focus
12952        app.cfn_state.input_focus = InputFocus::Filter;
12953        app.handle_action(Action::FilterInput('t'));
12954        app.handle_action(Action::FilterInput('e'));
12955        app.handle_action(Action::FilterInput('s'));
12956        app.handle_action(Action::FilterInput('t'));
12957        assert_eq!(app.cfn_state.table.filter, "test");
12958    }
12959
12960    #[test]
12961    fn test_cfn_input_focus_resets_on_start() {
12962        let mut app = test_app();
12963        app.current_service = Service::CloudFormationStacks;
12964        app.service_selected = true;
12965        app.mode = Mode::Normal;
12966        app.cfn_state.input_focus = InputFocus::Pagination;
12967
12968        // Start filter should reset focus to Input
12969        app.handle_action(Action::StartFilter);
12970        assert_eq!(app.mode, Mode::FilterInput);
12971        assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12972    }
12973
12974    #[test]
12975    fn test_iam_roles_input_focus_cycles_forward() {
12976        let mut app = test_app();
12977        app.current_service = Service::IamRoles;
12978        app.mode = Mode::FilterInput;
12979        app.iam_state.role_input_focus = InputFocus::Filter;
12980
12981        app.handle_action(Action::NextFilterFocus);
12982        assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12983
12984        app.handle_action(Action::NextFilterFocus);
12985        assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12986    }
12987
12988    #[test]
12989    fn test_iam_roles_input_focus_cycles_backward() {
12990        let mut app = test_app();
12991        app.current_service = Service::IamRoles;
12992        app.mode = Mode::FilterInput;
12993        app.iam_state.role_input_focus = InputFocus::Filter;
12994
12995        app.handle_action(Action::PrevFilterFocus);
12996        assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12997
12998        app.handle_action(Action::PrevFilterFocus);
12999        assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
13000    }
13001
13002    #[test]
13003    fn test_iam_roles_filter_input_only_when_focused() {
13004        let mut app = test_app();
13005        app.current_service = Service::IamRoles;
13006        app.mode = Mode::FilterInput;
13007        app.iam_state.role_input_focus = InputFocus::Pagination;
13008        app.iam_state.roles.filter = String::new();
13009
13010        // Typing should not add to filter when focus is on Pagination
13011        app.handle_action(Action::FilterInput('t'));
13012        app.handle_action(Action::FilterInput('e'));
13013        app.handle_action(Action::FilterInput('s'));
13014        app.handle_action(Action::FilterInput('t'));
13015        assert_eq!(app.iam_state.roles.filter, "");
13016
13017        // Switch to Input focus
13018        app.iam_state.role_input_focus = InputFocus::Filter;
13019        app.handle_action(Action::FilterInput('t'));
13020        app.handle_action(Action::FilterInput('e'));
13021        app.handle_action(Action::FilterInput('s'));
13022        app.handle_action(Action::FilterInput('t'));
13023        assert_eq!(app.iam_state.roles.filter, "test");
13024    }
13025
13026    #[test]
13027    fn test_iam_roles_page_down_updates_scroll_offset() {
13028        let mut app = test_app();
13029        app.current_service = Service::IamRoles;
13030        app.mode = Mode::Normal;
13031        app.iam_state.roles.items = (0..50)
13032            .map(|i| IamRole {
13033                role_name: format!("role-{}", i),
13034                path: "/".to_string(),
13035                trusted_entities: "AWS Service".to_string(),
13036                last_activity: "N/A".to_string(),
13037                arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
13038                creation_time: "2024-01-01".to_string(),
13039                description: String::new(),
13040                max_session_duration: Some(3600),
13041            })
13042            .collect();
13043
13044        app.iam_state.roles.selected = 0;
13045        app.iam_state.roles.scroll_offset = 0;
13046
13047        // Page down should update both selected and scroll_offset
13048        app.handle_action(Action::PageDown);
13049        assert_eq!(app.iam_state.roles.selected, 10);
13050        // scroll_offset should be updated to keep selection visible
13051        assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
13052
13053        // Page down again
13054        app.handle_action(Action::PageDown);
13055        assert_eq!(app.iam_state.roles.selected, 20);
13056        assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
13057    }
13058
13059    #[test]
13060    fn test_application_selection_and_deployments_tab() {
13061        use crate::lambda::Application as LambdaApplication;
13062        use LambdaApplicationDetailTab;
13063
13064        let mut app = test_app();
13065        app.current_service = Service::LambdaApplications;
13066        app.service_selected = true;
13067        app.mode = Mode::Normal;
13068
13069        app.lambda_application_state.table.items = vec![LambdaApplication {
13070            name: "test-app".to_string(),
13071            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13072            description: "Test application".to_string(),
13073            status: "CREATE_COMPLETE".to_string(),
13074            last_modified: "2024-01-01".to_string(),
13075        }];
13076
13077        // Select application
13078        app.handle_action(Action::Select);
13079        assert_eq!(
13080            app.lambda_application_state.current_application,
13081            Some("test-app".to_string())
13082        );
13083        assert_eq!(
13084            app.lambda_application_state.detail_tab,
13085            LambdaApplicationDetailTab::Overview
13086        );
13087
13088        // Switch to Deployments tab
13089        app.handle_action(Action::NextDetailTab);
13090        assert_eq!(
13091            app.lambda_application_state.detail_tab,
13092            LambdaApplicationDetailTab::Deployments
13093        );
13094
13095        // Go back
13096        app.handle_action(Action::GoBack);
13097        assert_eq!(app.lambda_application_state.current_application, None);
13098    }
13099
13100    #[test]
13101    fn test_application_resources_filter_and_pagination() {
13102        use crate::lambda::Application as LambdaApplication;
13103        use LambdaApplicationDetailTab;
13104
13105        let mut app = test_app();
13106        app.current_service = Service::LambdaApplications;
13107        app.service_selected = true;
13108        app.mode = Mode::Normal;
13109
13110        app.lambda_application_state.table.items = vec![LambdaApplication {
13111            name: "test-app".to_string(),
13112            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13113            description: "Test application".to_string(),
13114            status: "CREATE_COMPLETE".to_string(),
13115            last_modified: "2024-01-01".to_string(),
13116        }];
13117
13118        // Select application
13119        app.handle_action(Action::Select);
13120        assert_eq!(
13121            app.lambda_application_state.detail_tab,
13122            LambdaApplicationDetailTab::Overview
13123        );
13124
13125        // Verify resources were loaded
13126        assert!(!app.lambda_application_state.resources.items.is_empty());
13127
13128        // Test filter focus cycling
13129        app.mode = Mode::FilterInput;
13130        assert_eq!(
13131            app.lambda_application_state.resource_input_focus,
13132            InputFocus::Filter
13133        );
13134
13135        app.handle_action(Action::NextFilterFocus);
13136        assert_eq!(
13137            app.lambda_application_state.resource_input_focus,
13138            InputFocus::Pagination
13139        );
13140
13141        app.handle_action(Action::PrevFilterFocus);
13142        assert_eq!(
13143            app.lambda_application_state.resource_input_focus,
13144            InputFocus::Filter
13145        );
13146    }
13147
13148    #[test]
13149    fn test_application_deployments_filter_and_pagination() {
13150        use crate::lambda::Application as LambdaApplication;
13151        use LambdaApplicationDetailTab;
13152
13153        let mut app = test_app();
13154        app.current_service = Service::LambdaApplications;
13155        app.service_selected = true;
13156        app.mode = Mode::Normal;
13157
13158        app.lambda_application_state.table.items = vec![LambdaApplication {
13159            name: "test-app".to_string(),
13160            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13161            description: "Test application".to_string(),
13162            status: "CREATE_COMPLETE".to_string(),
13163            last_modified: "2024-01-01".to_string(),
13164        }];
13165
13166        // Select application and switch to Deployments tab
13167        app.handle_action(Action::Select);
13168        app.handle_action(Action::NextDetailTab);
13169        assert_eq!(
13170            app.lambda_application_state.detail_tab,
13171            LambdaApplicationDetailTab::Deployments
13172        );
13173
13174        // Verify deployments were loaded
13175        assert!(!app.lambda_application_state.deployments.items.is_empty());
13176
13177        // Test filter focus cycling
13178        app.mode = Mode::FilterInput;
13179        assert_eq!(
13180            app.lambda_application_state.deployment_input_focus,
13181            InputFocus::Filter
13182        );
13183
13184        app.handle_action(Action::NextFilterFocus);
13185        assert_eq!(
13186            app.lambda_application_state.deployment_input_focus,
13187            InputFocus::Pagination
13188        );
13189
13190        app.handle_action(Action::PrevFilterFocus);
13191        assert_eq!(
13192            app.lambda_application_state.deployment_input_focus,
13193            InputFocus::Filter
13194        );
13195    }
13196
13197    #[test]
13198    fn test_application_resource_expansion() {
13199        use crate::lambda::Application as LambdaApplication;
13200        use LambdaApplicationDetailTab;
13201
13202        let mut app = test_app();
13203        app.current_service = Service::LambdaApplications;
13204        app.service_selected = true;
13205        app.mode = Mode::Normal;
13206
13207        app.lambda_application_state.table.items = vec![LambdaApplication {
13208            name: "test-app".to_string(),
13209            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13210            description: "Test application".to_string(),
13211            status: "CREATE_COMPLETE".to_string(),
13212            last_modified: "2024-01-01".to_string(),
13213        }];
13214
13215        // Select application (Overview tab by default)
13216        app.handle_action(Action::Select);
13217        assert_eq!(
13218            app.lambda_application_state.detail_tab,
13219            LambdaApplicationDetailTab::Overview
13220        );
13221
13222        // Expand resource
13223        app.handle_action(Action::NextPane);
13224        assert_eq!(
13225            app.lambda_application_state.resources.expanded_item,
13226            Some(0)
13227        );
13228
13229        // Collapse resource
13230        app.handle_action(Action::PrevPane);
13231        assert_eq!(app.lambda_application_state.resources.expanded_item, None);
13232    }
13233
13234    #[test]
13235    fn test_application_deployment_expansion() {
13236        use crate::lambda::Application as LambdaApplication;
13237        use LambdaApplicationDetailTab;
13238
13239        let mut app = test_app();
13240        app.current_service = Service::LambdaApplications;
13241        app.service_selected = true;
13242        app.mode = Mode::Normal;
13243
13244        app.lambda_application_state.table.items = vec![LambdaApplication {
13245            name: "test-app".to_string(),
13246            arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13247            description: "Test application".to_string(),
13248            status: "CREATE_COMPLETE".to_string(),
13249            last_modified: "2024-01-01".to_string(),
13250        }];
13251
13252        // Select application and switch to Deployments tab
13253        app.handle_action(Action::Select);
13254        app.handle_action(Action::NextDetailTab);
13255        assert_eq!(
13256            app.lambda_application_state.detail_tab,
13257            LambdaApplicationDetailTab::Deployments
13258        );
13259
13260        // Expand deployment
13261        app.handle_action(Action::NextPane);
13262        assert_eq!(
13263            app.lambda_application_state.deployments.expanded_item,
13264            Some(0)
13265        );
13266
13267        // Collapse deployment
13268        app.handle_action(Action::PrevPane);
13269        assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
13270    }
13271
13272    #[test]
13273    fn test_s3_nested_prefix_expansion() {
13274        use crate::s3::Bucket;
13275        use S3Object;
13276
13277        let mut app = test_app();
13278        app.current_service = Service::S3Buckets;
13279        app.service_selected = true;
13280        app.mode = Mode::Normal;
13281
13282        // Setup bucket with nested prefixes (2 levels)
13283        app.s3_state.buckets.items = vec![Bucket {
13284            name: "test-bucket".to_string(),
13285            region: "us-east-1".to_string(),
13286            creation_date: "2024-01-01".to_string(),
13287        }];
13288
13289        // Level 1: bucket preview
13290        app.s3_state.bucket_preview.insert(
13291            "test-bucket".to_string(),
13292            vec![S3Object {
13293                key: "level1/".to_string(),
13294                size: 0,
13295                last_modified: "".to_string(),
13296                is_prefix: true,
13297                storage_class: "".to_string(),
13298            }],
13299        );
13300
13301        // Level 2: nested prefix
13302        app.s3_state.prefix_preview.insert(
13303            "level1/".to_string(),
13304            vec![S3Object {
13305                key: "level1/level2/".to_string(),
13306                size: 0,
13307                last_modified: "".to_string(),
13308                is_prefix: true,
13309                storage_class: "".to_string(),
13310            }],
13311        );
13312
13313        // Expand bucket (row 0)
13314        app.s3_state.selected_row = 0;
13315        app.handle_action(Action::NextPane);
13316        assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
13317
13318        // Expand level1/ (row 1)
13319        app.s3_state.selected_row = 1;
13320        app.handle_action(Action::NextPane);
13321        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
13322
13323        // Expand level2/ (row 2) - verifies nested expansion works
13324        app.s3_state.selected_row = 2;
13325        app.handle_action(Action::NextPane);
13326        assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
13327
13328        // Verify all are still expanded
13329        assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
13330        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
13331    }
13332
13333    #[test]
13334    fn test_s3_nested_prefix_collapse() {
13335        use crate::s3::Bucket;
13336        use S3Object;
13337
13338        let mut app = test_app();
13339        app.current_service = Service::S3Buckets;
13340        app.service_selected = true;
13341        app.mode = Mode::Normal;
13342
13343        app.s3_state.buckets.items = vec![Bucket {
13344            name: "test-bucket".to_string(),
13345            region: "us-east-1".to_string(),
13346            creation_date: "2024-01-01".to_string(),
13347        }];
13348
13349        app.s3_state.bucket_preview.insert(
13350            "test-bucket".to_string(),
13351            vec![S3Object {
13352                key: "level1/".to_string(),
13353                size: 0,
13354                last_modified: "".to_string(),
13355                is_prefix: true,
13356                storage_class: "".to_string(),
13357            }],
13358        );
13359
13360        app.s3_state.prefix_preview.insert(
13361            "level1/".to_string(),
13362            vec![S3Object {
13363                key: "level1/level2/".to_string(),
13364                size: 0,
13365                last_modified: "".to_string(),
13366                is_prefix: true,
13367                storage_class: "".to_string(),
13368            }],
13369        );
13370
13371        // Pre-expand all levels
13372        app.s3_state
13373            .expanded_prefixes
13374            .insert("test-bucket".to_string());
13375        app.s3_state.expanded_prefixes.insert("level1/".to_string());
13376        app.s3_state
13377            .expanded_prefixes
13378            .insert("level1/level2/".to_string());
13379
13380        // Collapse level2/ (row 2)
13381        app.s3_state.selected_row = 2;
13382        app.handle_action(Action::PrevPane);
13383        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
13384        assert!(app.s3_state.expanded_prefixes.contains("level1/")); // Parent still expanded
13385
13386        // Collapse level1/ (row 1)
13387        app.s3_state.selected_row = 1;
13388        app.handle_action(Action::PrevPane);
13389        assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
13390        assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); // Bucket still expanded
13391
13392        // Collapse bucket (row 0)
13393        app.s3_state.selected_row = 0;
13394        app.handle_action(Action::PrevPane);
13395        assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
13396    }
13397}
13398
13399#[cfg(test)]
13400mod sqs_tests {
13401    use super::*;
13402    use test_helpers::*;
13403
13404    #[test]
13405    fn test_sqs_filter_input() {
13406        let mut app = test_app();
13407        app.current_service = Service::SqsQueues;
13408        app.service_selected = true;
13409        app.mode = Mode::FilterInput;
13410
13411        app.handle_action(Action::FilterInput('t'));
13412        app.handle_action(Action::FilterInput('e'));
13413        app.handle_action(Action::FilterInput('s'));
13414        app.handle_action(Action::FilterInput('t'));
13415        assert_eq!(app.sqs_state.queues.filter, "test");
13416
13417        app.handle_action(Action::FilterBackspace);
13418        assert_eq!(app.sqs_state.queues.filter, "tes");
13419    }
13420
13421    #[test]
13422    fn test_sqs_start_filter() {
13423        let mut app = test_app();
13424        app.current_service = Service::SqsQueues;
13425        app.service_selected = true;
13426        app.mode = Mode::Normal;
13427
13428        app.handle_action(Action::StartFilter);
13429        assert_eq!(app.mode, Mode::FilterInput);
13430        assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
13431    }
13432
13433    #[test]
13434    fn test_sqs_filter_focus_cycling() {
13435        let mut app = test_app();
13436        app.current_service = Service::SqsQueues;
13437        app.service_selected = true;
13438        app.mode = Mode::FilterInput;
13439        app.sqs_state.input_focus = InputFocus::Filter;
13440
13441        app.handle_action(Action::NextFilterFocus);
13442        assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
13443
13444        app.handle_action(Action::NextFilterFocus);
13445        assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
13446
13447        app.handle_action(Action::PrevFilterFocus);
13448        assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
13449    }
13450
13451    #[test]
13452    fn test_sqs_navigation() {
13453        let mut app = test_app();
13454        app.current_service = Service::SqsQueues;
13455        app.service_selected = true;
13456        app.mode = Mode::Normal;
13457        app.sqs_state.queues.items = (0..10)
13458            .map(|i| SqsQueue {
13459                name: format!("queue{}", i),
13460                url: String::new(),
13461                queue_type: "Standard".to_string(),
13462                created_timestamp: String::new(),
13463                messages_available: "0".to_string(),
13464                messages_in_flight: "0".to_string(),
13465                encryption: "Disabled".to_string(),
13466                content_based_deduplication: "Disabled".to_string(),
13467                last_modified_timestamp: String::new(),
13468                visibility_timeout: String::new(),
13469                message_retention_period: String::new(),
13470                maximum_message_size: String::new(),
13471                delivery_delay: String::new(),
13472                receive_message_wait_time: String::new(),
13473                high_throughput_fifo: "N/A".to_string(),
13474                deduplication_scope: "N/A".to_string(),
13475                fifo_throughput_limit: "N/A".to_string(),
13476                dead_letter_queue: "-".to_string(),
13477                messages_delayed: "0".to_string(),
13478                redrive_allow_policy: "-".to_string(),
13479                redrive_policy: "".to_string(),
13480                redrive_task_id: "-".to_string(),
13481                redrive_task_start_time: "-".to_string(),
13482                redrive_task_status: "-".to_string(),
13483                redrive_task_percent: "-".to_string(),
13484                redrive_task_destination: "-".to_string(),
13485            })
13486            .collect();
13487
13488        app.handle_action(Action::NextItem);
13489        assert_eq!(app.sqs_state.queues.selected, 1);
13490
13491        app.handle_action(Action::PrevItem);
13492        assert_eq!(app.sqs_state.queues.selected, 0);
13493    }
13494
13495    #[test]
13496    fn test_sqs_page_navigation() {
13497        let mut app = test_app();
13498        app.current_service = Service::SqsQueues;
13499        app.service_selected = true;
13500        app.mode = Mode::Normal;
13501        app.sqs_state.queues.items = (0..100)
13502            .map(|i| SqsQueue {
13503                name: format!("queue{}", i),
13504                url: String::new(),
13505                queue_type: "Standard".to_string(),
13506                created_timestamp: String::new(),
13507                messages_available: "0".to_string(),
13508                messages_in_flight: "0".to_string(),
13509                encryption: "Disabled".to_string(),
13510                content_based_deduplication: "Disabled".to_string(),
13511                last_modified_timestamp: String::new(),
13512                visibility_timeout: String::new(),
13513                message_retention_period: String::new(),
13514                maximum_message_size: String::new(),
13515                delivery_delay: String::new(),
13516                receive_message_wait_time: String::new(),
13517                high_throughput_fifo: "N/A".to_string(),
13518                deduplication_scope: "N/A".to_string(),
13519                fifo_throughput_limit: "N/A".to_string(),
13520                dead_letter_queue: "-".to_string(),
13521                messages_delayed: "0".to_string(),
13522                redrive_allow_policy: "-".to_string(),
13523                redrive_policy: "".to_string(),
13524                redrive_task_id: "-".to_string(),
13525                redrive_task_start_time: "-".to_string(),
13526                redrive_task_status: "-".to_string(),
13527                redrive_task_percent: "-".to_string(),
13528                redrive_task_destination: "-".to_string(),
13529            })
13530            .collect();
13531
13532        app.handle_action(Action::PageDown);
13533        assert_eq!(app.sqs_state.queues.selected, 10);
13534
13535        app.handle_action(Action::PageUp);
13536        assert_eq!(app.sqs_state.queues.selected, 0);
13537    }
13538
13539    #[test]
13540    fn test_sqs_queue_expansion() {
13541        let mut app = test_app();
13542        app.current_service = Service::SqsQueues;
13543        app.service_selected = true;
13544        app.sqs_state.queues.items = vec![SqsQueue {
13545            name: "my-queue".to_string(),
13546            url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
13547            queue_type: "Standard".to_string(),
13548            created_timestamp: "2023-01-01".to_string(),
13549            messages_available: "5".to_string(),
13550            messages_in_flight: "2".to_string(),
13551            encryption: "Enabled".to_string(),
13552            content_based_deduplication: "Disabled".to_string(),
13553            last_modified_timestamp: "2023-01-02".to_string(),
13554            visibility_timeout: "30".to_string(),
13555            message_retention_period: "345600".to_string(),
13556            maximum_message_size: "262144".to_string(),
13557            delivery_delay: "0".to_string(),
13558            receive_message_wait_time: "0".to_string(),
13559            high_throughput_fifo: "N/A".to_string(),
13560            deduplication_scope: "N/A".to_string(),
13561            fifo_throughput_limit: "N/A".to_string(),
13562            dead_letter_queue: "-".to_string(),
13563            messages_delayed: "0".to_string(),
13564            redrive_allow_policy: "-".to_string(),
13565            redrive_policy: "".to_string(),
13566            redrive_task_id: "-".to_string(),
13567            redrive_task_start_time: "-".to_string(),
13568            redrive_task_status: "-".to_string(),
13569            redrive_task_percent: "-".to_string(),
13570            redrive_task_destination: "-".to_string(),
13571        }];
13572        app.sqs_state.queues.selected = 0;
13573
13574        assert_eq!(app.sqs_state.queues.expanded_item, None);
13575
13576        // Right arrow expands
13577        app.handle_action(Action::NextPane);
13578        assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
13579
13580        // Right arrow again keeps it expanded
13581        app.handle_action(Action::NextPane);
13582        assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
13583
13584        // Left arrow collapses
13585        app.handle_action(Action::PrevPane);
13586        assert_eq!(app.sqs_state.queues.expanded_item, None);
13587
13588        // Left arrow again keeps it collapsed
13589        app.handle_action(Action::PrevPane);
13590        assert_eq!(app.sqs_state.queues.expanded_item, None);
13591    }
13592
13593    #[test]
13594    fn test_sqs_column_toggle() {
13595        use crate::sqs::queue::Column as SqsColumn;
13596        let mut app = test_app();
13597        app.current_service = Service::SqsQueues;
13598        app.service_selected = true;
13599        app.mode = Mode::ColumnSelector;
13600
13601        // Start with all columns visible
13602        app.sqs_visible_column_ids = SqsColumn::ids();
13603        let initial_count = app.sqs_visible_column_ids.len();
13604
13605        // Select first column (index 0) and toggle it
13606        app.column_selector_index = 0;
13607        app.handle_action(Action::ToggleColumn);
13608
13609        // First column should be removed
13610        assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
13611        assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
13612
13613        // Toggle it back
13614        app.handle_action(Action::ToggleColumn);
13615        assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
13616        assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
13617    }
13618
13619    #[test]
13620    fn test_sqs_column_selector_navigation() {
13621        let mut app = test_app();
13622        app.current_service = Service::SqsQueues;
13623        app.service_selected = true;
13624        app.mode = Mode::ColumnSelector;
13625        app.column_selector_index = 0;
13626
13627        // Should be able to navigate through all columns
13628        let max_index = app.sqs_column_ids.len() - 1;
13629
13630        // Navigate to last column
13631        for _ in 0..max_index {
13632            app.handle_action(Action::NextItem);
13633        }
13634        assert_eq!(app.column_selector_index, max_index);
13635
13636        // Navigate back to first
13637        for _ in 0..max_index {
13638            app.handle_action(Action::PrevItem);
13639        }
13640        assert_eq!(app.column_selector_index, 0);
13641    }
13642
13643    #[test]
13644    fn test_sqs_queue_selection() {
13645        let mut app = test_app();
13646        app.current_service = Service::SqsQueues;
13647        app.service_selected = true;
13648        app.mode = Mode::Normal;
13649        app.sqs_state.queues.items = vec![SqsQueue {
13650            name: "my-queue".to_string(),
13651            url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
13652            queue_type: "Standard".to_string(),
13653            created_timestamp: "2023-01-01".to_string(),
13654            messages_available: "5".to_string(),
13655            messages_in_flight: "2".to_string(),
13656            encryption: "Enabled".to_string(),
13657            content_based_deduplication: "Disabled".to_string(),
13658            last_modified_timestamp: "2023-01-02".to_string(),
13659            visibility_timeout: "30".to_string(),
13660            message_retention_period: "345600".to_string(),
13661            maximum_message_size: "262144".to_string(),
13662            delivery_delay: "0".to_string(),
13663            receive_message_wait_time: "0".to_string(),
13664            high_throughput_fifo: "N/A".to_string(),
13665            deduplication_scope: "N/A".to_string(),
13666            fifo_throughput_limit: "N/A".to_string(),
13667            dead_letter_queue: "-".to_string(),
13668            messages_delayed: "0".to_string(),
13669            redrive_allow_policy: "-".to_string(),
13670            redrive_policy: "".to_string(),
13671            redrive_task_id: "-".to_string(),
13672            redrive_task_start_time: "-".to_string(),
13673            redrive_task_status: "-".to_string(),
13674            redrive_task_percent: "-".to_string(),
13675            redrive_task_destination: "-".to_string(),
13676        }];
13677        app.sqs_state.queues.selected = 0;
13678
13679        assert_eq!(app.sqs_state.current_queue, None);
13680
13681        // Select queue
13682        app.handle_action(Action::Select);
13683        assert_eq!(
13684            app.sqs_state.current_queue,
13685            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
13686        );
13687
13688        // Go back
13689        app.handle_action(Action::GoBack);
13690        assert_eq!(app.sqs_state.current_queue, None);
13691    }
13692
13693    #[test]
13694    fn test_sqs_lambda_triggers_expand_collapse() {
13695        let mut app = test_app();
13696        app.current_service = Service::SqsQueues;
13697        app.service_selected = true;
13698        app.sqs_state.current_queue =
13699            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13700        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13701        app.sqs_state.triggers.items = vec![LambdaTrigger {
13702            uuid: "test-uuid".to_string(),
13703            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
13704            status: "Enabled".to_string(),
13705            last_modified: "2024-01-01T00:00:00Z".to_string(),
13706        }];
13707        app.sqs_state.triggers.selected = 0;
13708
13709        assert_eq!(app.sqs_state.triggers.expanded_item, None);
13710
13711        // Right arrow expands
13712        app.handle_action(Action::NextPane);
13713        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13714
13715        // Left arrow collapses
13716        app.handle_action(Action::PrevPane);
13717        assert_eq!(app.sqs_state.triggers.expanded_item, None);
13718    }
13719
13720    #[test]
13721    fn test_sqs_lambda_triggers_expand_toggle() {
13722        let mut app = test_app();
13723        app.current_service = Service::SqsQueues;
13724        app.service_selected = true;
13725        app.sqs_state.current_queue =
13726            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13727        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13728        app.sqs_state.triggers.items = vec![LambdaTrigger {
13729            uuid: "test-uuid".to_string(),
13730            arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
13731            status: "Enabled".to_string(),
13732            last_modified: "2024-01-01T00:00:00Z".to_string(),
13733        }];
13734        app.sqs_state.triggers.selected = 0;
13735
13736        // Expand
13737        app.handle_action(Action::NextPane);
13738        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13739
13740        // Toggle collapses
13741        app.handle_action(Action::NextPane);
13742        assert_eq!(app.sqs_state.triggers.expanded_item, None);
13743
13744        // Toggle expands again
13745        app.handle_action(Action::NextPane);
13746        assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13747    }
13748
13749    #[test]
13750    fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
13751        use crate::ui::sqs::filtered_lambda_triggers;
13752
13753        let mut app = test_app();
13754        app.current_service = Service::SqsQueues;
13755        app.service_selected = true;
13756        app.sqs_state.current_queue =
13757            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13758        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13759        app.sqs_state.triggers.items = vec![
13760            LambdaTrigger {
13761                uuid: "uuid-3".to_string(),
13762                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
13763                status: "Enabled".to_string(),
13764                last_modified: "2024-03-01T00:00:00Z".to_string(),
13765            },
13766            LambdaTrigger {
13767                uuid: "uuid-1".to_string(),
13768                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
13769                status: "Enabled".to_string(),
13770                last_modified: "2024-01-01T00:00:00Z".to_string(),
13771            },
13772            LambdaTrigger {
13773                uuid: "uuid-2".to_string(),
13774                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
13775                status: "Enabled".to_string(),
13776                last_modified: "2024-02-01T00:00:00Z".to_string(),
13777            },
13778        ];
13779
13780        let sorted = filtered_lambda_triggers(&app);
13781
13782        // Should be sorted by last_modified ASC
13783        assert_eq!(sorted.len(), 3);
13784        assert_eq!(sorted[0].uuid, "uuid-1");
13785        assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
13786        assert_eq!(sorted[1].uuid, "uuid-2");
13787        assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
13788        assert_eq!(sorted[2].uuid, "uuid-3");
13789        assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
13790    }
13791
13792    #[test]
13793    fn test_sqs_lambda_triggers_filter_input() {
13794        let mut app = test_app();
13795        app.current_service = Service::SqsQueues;
13796        app.service_selected = true;
13797        app.mode = Mode::FilterInput;
13798        app.sqs_state.current_queue =
13799            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13800        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13801        app.sqs_state.input_focus = InputFocus::Filter;
13802
13803        assert_eq!(app.sqs_state.triggers.filter, "");
13804
13805        // Type characters
13806        app.handle_action(Action::FilterInput('t'));
13807        assert_eq!(app.sqs_state.triggers.filter, "t");
13808
13809        app.handle_action(Action::FilterInput('e'));
13810        assert_eq!(app.sqs_state.triggers.filter, "te");
13811
13812        app.handle_action(Action::FilterInput('s'));
13813        assert_eq!(app.sqs_state.triggers.filter, "tes");
13814
13815        app.handle_action(Action::FilterInput('t'));
13816        assert_eq!(app.sqs_state.triggers.filter, "test");
13817
13818        // Backspace
13819        app.handle_action(Action::FilterBackspace);
13820        assert_eq!(app.sqs_state.triggers.filter, "tes");
13821    }
13822
13823    #[test]
13824    fn test_sqs_lambda_triggers_filter_applied() {
13825        use crate::ui::sqs::filtered_lambda_triggers;
13826
13827        let mut app = test_app();
13828        app.current_service = Service::SqsQueues;
13829        app.service_selected = true;
13830        app.sqs_state.current_queue =
13831            Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13832        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13833        app.sqs_state.triggers.items = vec![
13834            LambdaTrigger {
13835                uuid: "uuid-1".to_string(),
13836                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
13837                status: "Enabled".to_string(),
13838                last_modified: "2024-01-01T00:00:00Z".to_string(),
13839            },
13840            LambdaTrigger {
13841                uuid: "uuid-2".to_string(),
13842                arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
13843                status: "Enabled".to_string(),
13844                last_modified: "2024-02-01T00:00:00Z".to_string(),
13845            },
13846            LambdaTrigger {
13847                uuid: "uuid-3".to_string(),
13848                arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
13849                status: "Enabled".to_string(),
13850                last_modified: "2024-03-01T00:00:00Z".to_string(),
13851            },
13852        ];
13853
13854        // No filter - all items
13855        let filtered = filtered_lambda_triggers(&app);
13856        assert_eq!(filtered.len(), 3);
13857
13858        // Filter by "alpha"
13859        app.sqs_state.triggers.filter = "alpha".to_string();
13860        let filtered = filtered_lambda_triggers(&app);
13861        assert_eq!(filtered.len(), 1);
13862        assert_eq!(
13863            filtered[0].arn,
13864            "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
13865        );
13866
13867        // Filter by "test" - matches 2
13868        app.sqs_state.triggers.filter = "test".to_string();
13869        let filtered = filtered_lambda_triggers(&app);
13870        assert_eq!(filtered.len(), 2);
13871        assert_eq!(
13872            filtered[0].arn,
13873            "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
13874        );
13875        assert_eq!(
13876            filtered[1].arn,
13877            "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
13878        );
13879
13880        // Filter by uuid
13881        app.sqs_state.triggers.filter = "uuid-3".to_string();
13882        let filtered = filtered_lambda_triggers(&app);
13883        assert_eq!(filtered.len(), 1);
13884        assert_eq!(filtered[0].uuid, "uuid-3");
13885    }
13886
13887    #[test]
13888    fn test_sqs_triggers_navigation() {
13889        let mut app = test_app();
13890        app.service_selected = true;
13891        app.mode = Mode::Normal;
13892        app.current_service = Service::SqsQueues;
13893        app.sqs_state.current_queue = Some("test-queue".to_string());
13894        app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13895        app.sqs_state.triggers.items = vec![
13896            LambdaTrigger {
13897                uuid: "1".to_string(),
13898                arn: "arn1".to_string(),
13899                status: "Enabled".to_string(),
13900                last_modified: "2024-01-01".to_string(),
13901            },
13902            LambdaTrigger {
13903                uuid: "2".to_string(),
13904                arn: "arn2".to_string(),
13905                status: "Enabled".to_string(),
13906                last_modified: "2024-01-02".to_string(),
13907            },
13908        ];
13909
13910        assert_eq!(app.sqs_state.triggers.selected, 0);
13911        app.next_item();
13912        assert_eq!(app.sqs_state.triggers.selected, 1);
13913        app.prev_item();
13914        assert_eq!(app.sqs_state.triggers.selected, 0);
13915    }
13916
13917    #[test]
13918    fn test_sqs_pipes_navigation() {
13919        let mut app = test_app();
13920        app.service_selected = true;
13921        app.mode = Mode::Normal;
13922        app.current_service = Service::SqsQueues;
13923        app.sqs_state.current_queue = Some("test-queue".to_string());
13924        app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
13925        app.sqs_state.pipes.items = vec![
13926            EventBridgePipe {
13927                name: "pipe1".to_string(),
13928                status: "RUNNING".to_string(),
13929                target: "target1".to_string(),
13930                last_modified: "2024-01-01".to_string(),
13931            },
13932            EventBridgePipe {
13933                name: "pipe2".to_string(),
13934                status: "RUNNING".to_string(),
13935                target: "target2".to_string(),
13936                last_modified: "2024-01-02".to_string(),
13937            },
13938        ];
13939
13940        assert_eq!(app.sqs_state.pipes.selected, 0);
13941        app.next_item();
13942        assert_eq!(app.sqs_state.pipes.selected, 1);
13943        app.prev_item();
13944        assert_eq!(app.sqs_state.pipes.selected, 0);
13945    }
13946
13947    #[test]
13948    fn test_sqs_tags_navigation() {
13949        let mut app = test_app();
13950        app.service_selected = true;
13951        app.mode = Mode::Normal;
13952        app.current_service = Service::SqsQueues;
13953        app.sqs_state.current_queue = Some("test-queue".to_string());
13954        app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
13955        app.sqs_state.tags.items = vec![
13956            SqsQueueTag {
13957                key: "Env".to_string(),
13958                value: "prod".to_string(),
13959            },
13960            SqsQueueTag {
13961                key: "Team".to_string(),
13962                value: "backend".to_string(),
13963            },
13964        ];
13965
13966        assert_eq!(app.sqs_state.tags.selected, 0);
13967        app.next_item();
13968        assert_eq!(app.sqs_state.tags.selected, 1);
13969        app.prev_item();
13970        assert_eq!(app.sqs_state.tags.selected, 0);
13971    }
13972
13973    #[test]
13974    fn test_sqs_queues_navigation() {
13975        let mut app = test_app();
13976        app.service_selected = true;
13977        app.mode = Mode::Normal;
13978        app.current_service = Service::SqsQueues;
13979        app.sqs_state.queues.items = vec![
13980            SqsQueue {
13981                name: "queue1".to_string(),
13982                url: "url1".to_string(),
13983                queue_type: "Standard".to_string(),
13984                created_timestamp: "".to_string(),
13985                messages_available: "0".to_string(),
13986                messages_in_flight: "0".to_string(),
13987                encryption: "Disabled".to_string(),
13988                content_based_deduplication: "Disabled".to_string(),
13989                last_modified_timestamp: "".to_string(),
13990                visibility_timeout: "".to_string(),
13991                message_retention_period: "".to_string(),
13992                maximum_message_size: "".to_string(),
13993                delivery_delay: "".to_string(),
13994                receive_message_wait_time: "".to_string(),
13995                high_throughput_fifo: "-".to_string(),
13996                deduplication_scope: "-".to_string(),
13997                fifo_throughput_limit: "-".to_string(),
13998                dead_letter_queue: "-".to_string(),
13999                messages_delayed: "0".to_string(),
14000                redrive_allow_policy: "-".to_string(),
14001                redrive_policy: "".to_string(),
14002                redrive_task_id: "-".to_string(),
14003                redrive_task_start_time: "-".to_string(),
14004                redrive_task_status: "-".to_string(),
14005                redrive_task_percent: "-".to_string(),
14006                redrive_task_destination: "-".to_string(),
14007            },
14008            SqsQueue {
14009                name: "queue2".to_string(),
14010                url: "url2".to_string(),
14011                queue_type: "Standard".to_string(),
14012                created_timestamp: "".to_string(),
14013                messages_available: "0".to_string(),
14014                messages_in_flight: "0".to_string(),
14015                encryption: "Disabled".to_string(),
14016                content_based_deduplication: "Disabled".to_string(),
14017                last_modified_timestamp: "".to_string(),
14018                visibility_timeout: "".to_string(),
14019                message_retention_period: "".to_string(),
14020                maximum_message_size: "".to_string(),
14021                delivery_delay: "".to_string(),
14022                receive_message_wait_time: "".to_string(),
14023                high_throughput_fifo: "-".to_string(),
14024                deduplication_scope: "-".to_string(),
14025                fifo_throughput_limit: "-".to_string(),
14026                dead_letter_queue: "-".to_string(),
14027                messages_delayed: "0".to_string(),
14028                redrive_allow_policy: "-".to_string(),
14029                redrive_policy: "".to_string(),
14030                redrive_task_id: "-".to_string(),
14031                redrive_task_start_time: "-".to_string(),
14032                redrive_task_status: "-".to_string(),
14033                redrive_task_percent: "-".to_string(),
14034                redrive_task_destination: "-".to_string(),
14035            },
14036        ];
14037
14038        assert_eq!(app.sqs_state.queues.selected, 0);
14039        app.next_item();
14040        assert_eq!(app.sqs_state.queues.selected, 1);
14041        app.prev_item();
14042        assert_eq!(app.sqs_state.queues.selected, 0);
14043    }
14044
14045    #[test]
14046    fn test_sqs_subscriptions_navigation() {
14047        let mut app = test_app();
14048        app.service_selected = true;
14049        app.mode = Mode::Normal;
14050        app.current_service = Service::SqsQueues;
14051        app.sqs_state.current_queue = Some("test-queue".to_string());
14052        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14053        app.sqs_state.subscriptions.items = vec![
14054            SnsSubscription {
14055                subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
14056                topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
14057            },
14058            SnsSubscription {
14059                subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
14060                topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
14061            },
14062        ];
14063
14064        assert_eq!(app.sqs_state.subscriptions.selected, 0);
14065        app.next_item();
14066        assert_eq!(app.sqs_state.subscriptions.selected, 1);
14067        app.prev_item();
14068        assert_eq!(app.sqs_state.subscriptions.selected, 0);
14069    }
14070
14071    #[test]
14072    fn test_sqs_subscription_region_dropdown_navigation() {
14073        let mut app = test_app();
14074        app.service_selected = true;
14075        app.mode = Mode::FilterInput;
14076        app.current_service = Service::SqsQueues;
14077        app.sqs_state.current_queue = Some("test-queue".to_string());
14078        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14079        app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
14080
14081        assert_eq!(app.sqs_state.subscription_region_selected, 0);
14082        app.next_item();
14083        assert_eq!(app.sqs_state.subscription_region_selected, 1);
14084        app.next_item();
14085        assert_eq!(app.sqs_state.subscription_region_selected, 2);
14086        app.prev_item();
14087        assert_eq!(app.sqs_state.subscription_region_selected, 1);
14088        app.prev_item();
14089        assert_eq!(app.sqs_state.subscription_region_selected, 0);
14090    }
14091
14092    #[test]
14093    fn test_sqs_subscription_region_selection() {
14094        let mut app = test_app();
14095        app.service_selected = true;
14096        app.mode = Mode::FilterInput;
14097        app.current_service = Service::SqsQueues;
14098        app.sqs_state.current_queue = Some("test-queue".to_string());
14099        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14100        app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
14101        app.sqs_state.subscription_region_selected = 2; // us-west-1
14102
14103        assert_eq!(app.sqs_state.subscription_region_filter, "");
14104        app.handle_action(Action::ApplyFilter);
14105        assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
14106        assert_eq!(app.mode, Mode::Normal);
14107    }
14108
14109    #[test]
14110    fn test_sqs_subscription_region_change_resets_selection() {
14111        let mut app = test_app();
14112        app.service_selected = true;
14113        app.mode = Mode::FilterInput;
14114        app.current_service = Service::SqsQueues;
14115        app.sqs_state.current_queue = Some("test-queue".to_string());
14116        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14117        app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
14118        app.sqs_state.subscription_region_selected = 0;
14119        app.sqs_state.subscriptions.selected = 5;
14120
14121        app.handle_action(Action::NextItem);
14122
14123        assert_eq!(app.sqs_state.subscription_region_selected, 1);
14124        assert_eq!(app.sqs_state.subscriptions.selected, 0);
14125    }
14126
14127    #[test]
14128    fn test_s3_object_filter_resets_selection() {
14129        let mut app = test_app();
14130        app.service_selected = true;
14131        app.current_service = Service::S3Buckets;
14132        app.s3_state.current_bucket = Some("test-bucket".to_string());
14133        app.s3_state.selected_row = 5;
14134        app.mode = Mode::FilterInput;
14135
14136        app.handle_action(Action::CloseMenu);
14137
14138        assert_eq!(app.s3_state.selected_row, 0);
14139        assert_eq!(app.mode, Mode::Normal);
14140    }
14141
14142    #[test]
14143    fn test_s3_bucket_filter_resets_selection() {
14144        let mut app = test_app();
14145        app.service_selected = true;
14146        app.current_service = Service::S3Buckets;
14147        app.s3_state.selected_row = 10;
14148        app.mode = Mode::FilterInput;
14149
14150        app.handle_action(Action::CloseMenu);
14151
14152        assert_eq!(app.s3_state.selected_row, 0);
14153        assert_eq!(app.mode, Mode::Normal);
14154    }
14155
14156    #[test]
14157    fn test_s3_selection_stays_in_bounds() {
14158        let mut app = test_app();
14159        app.service_selected = true;
14160        app.current_service = Service::S3Buckets;
14161        app.s3_state.selected_row = 0;
14162        app.s3_state.selected_object = 0;
14163
14164        // Simulate going up from row 0
14165        app.prev_item();
14166
14167        // Should stay at 0, not wrap to negative
14168        assert_eq!(app.s3_state.selected_row, 0);
14169        assert_eq!(app.s3_state.selected_object, 0);
14170    }
14171
14172    #[test]
14173    fn test_cfn_filter_resets_selection() {
14174        let mut app = test_app();
14175        app.service_selected = true;
14176        app.current_service = Service::CloudFormationStacks;
14177        app.cfn_state.table.selected = 10;
14178        app.mode = Mode::FilterInput;
14179
14180        app.handle_action(Action::CloseMenu);
14181
14182        assert_eq!(app.cfn_state.table.selected, 0);
14183        assert_eq!(app.mode, Mode::Normal);
14184    }
14185
14186    #[test]
14187    fn test_lambda_filter_resets_selection() {
14188        let mut app = test_app();
14189        app.service_selected = true;
14190        app.current_service = Service::LambdaFunctions;
14191        app.lambda_state.table.selected = 8;
14192        app.mode = Mode::FilterInput;
14193
14194        app.handle_action(Action::CloseMenu);
14195
14196        assert_eq!(app.lambda_state.table.selected, 0);
14197        assert_eq!(app.mode, Mode::Normal);
14198    }
14199
14200    #[test]
14201    fn test_sqs_filter_resets_selection() {
14202        let mut app = test_app();
14203        app.service_selected = true;
14204        app.current_service = Service::SqsQueues;
14205        app.sqs_state.queues.selected = 7;
14206        app.mode = Mode::FilterInput;
14207
14208        app.handle_action(Action::CloseMenu);
14209
14210        assert_eq!(app.sqs_state.queues.selected, 0);
14211        assert_eq!(app.mode, Mode::Normal);
14212    }
14213
14214    #[test]
14215    fn test_sqs_queues_list_shows_preferences() {
14216        let mut app = test_app();
14217        app.service_selected = true;
14218        app.current_service = Service::SqsQueues;
14219        app.mode = Mode::Normal;
14220
14221        app.handle_action(Action::OpenColumnSelector);
14222
14223        assert_eq!(app.mode, Mode::ColumnSelector);
14224    }
14225
14226    #[test]
14227    fn test_sqs_queue_policies_tab_no_preferences() {
14228        let mut app = test_app();
14229        app.service_selected = true;
14230        app.current_service = Service::SqsQueues;
14231        app.sqs_state.current_queue = Some("test-queue".to_string());
14232        app.sqs_state.detail_tab = SqsQueueDetailTab::QueuePolicies;
14233        app.mode = Mode::Normal;
14234
14235        app.handle_action(Action::OpenColumnSelector);
14236
14237        assert_eq!(app.mode, Mode::Normal);
14238    }
14239
14240    #[test]
14241    fn test_sqs_sns_subscriptions_tab_shows_preferences() {
14242        let mut app = test_app();
14243        app.service_selected = true;
14244        app.current_service = Service::SqsQueues;
14245        app.sqs_state.current_queue = Some("test-queue".to_string());
14246        app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14247        app.mode = Mode::Normal;
14248
14249        app.handle_action(Action::OpenColumnSelector);
14250
14251        assert_eq!(app.mode, Mode::ColumnSelector);
14252    }
14253
14254    #[test]
14255    fn test_sqs_monitoring_tab_no_preferences() {
14256        let mut app = test_app();
14257        app.service_selected = true;
14258        app.current_service = Service::SqsQueues;
14259        app.sqs_state.current_queue = Some("test-queue".to_string());
14260        app.sqs_state.detail_tab = SqsQueueDetailTab::Monitoring;
14261        app.mode = Mode::Normal;
14262
14263        app.handle_action(Action::OpenColumnSelector);
14264
14265        assert_eq!(app.mode, Mode::Normal);
14266    }
14267
14268    #[test]
14269    fn test_cfn_status_filter_change_resets_selection() {
14270        use crate::ui::cfn::STATUS_FILTER;
14271        let mut app = test_app();
14272        app.service_selected = true;
14273        app.current_service = Service::CloudFormationStacks;
14274        app.mode = Mode::FilterInput;
14275        app.cfn_state.input_focus = STATUS_FILTER;
14276        app.cfn_state.status_filter = CfnStatusFilter::All;
14277        app.cfn_state.table.items = vec![
14278            CfnStack {
14279                name: "stack1".to_string(),
14280                stack_id: "id1".to_string(),
14281                status: "CREATE_COMPLETE".to_string(),
14282                created_time: "2024-01-01".to_string(),
14283                updated_time: String::new(),
14284                deleted_time: String::new(),
14285                drift_status: String::new(),
14286                last_drift_check_time: String::new(),
14287                status_reason: String::new(),
14288                description: String::new(),
14289                detailed_status: String::new(),
14290                root_stack: String::new(),
14291                parent_stack: String::new(),
14292                termination_protection: false,
14293                iam_role: String::new(),
14294                tags: Vec::new(),
14295                stack_policy: String::new(),
14296                rollback_monitoring_time: String::new(),
14297                rollback_alarms: Vec::new(),
14298                notification_arns: Vec::new(),
14299            },
14300            CfnStack {
14301                name: "stack2".to_string(),
14302                stack_id: "id2".to_string(),
14303                status: "UPDATE_IN_PROGRESS".to_string(),
14304                created_time: "2024-01-02".to_string(),
14305                updated_time: String::new(),
14306                deleted_time: String::new(),
14307                drift_status: String::new(),
14308                last_drift_check_time: String::new(),
14309                status_reason: String::new(),
14310                description: String::new(),
14311                detailed_status: String::new(),
14312                root_stack: String::new(),
14313                parent_stack: String::new(),
14314                termination_protection: false,
14315                iam_role: String::new(),
14316                tags: Vec::new(),
14317                stack_policy: String::new(),
14318                rollback_monitoring_time: String::new(),
14319                rollback_alarms: Vec::new(),
14320                notification_arns: Vec::new(),
14321            },
14322        ];
14323        app.cfn_state.table.selected = 1;
14324
14325        app.handle_action(Action::NextItem);
14326
14327        assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
14328        assert_eq!(app.cfn_state.table.selected, 0);
14329    }
14330
14331    #[test]
14332    fn test_cfn_view_nested_toggle_resets_selection() {
14333        use crate::ui::cfn::VIEW_NESTED;
14334        let mut app = test_app();
14335        app.service_selected = true;
14336        app.current_service = Service::CloudFormationStacks;
14337        app.mode = Mode::FilterInput;
14338        app.cfn_state.input_focus = VIEW_NESTED;
14339        app.cfn_state.view_nested = false;
14340        app.cfn_state.table.items = vec![CfnStack {
14341            name: "stack1".to_string(),
14342            stack_id: "id1".to_string(),
14343            status: "CREATE_COMPLETE".to_string(),
14344            created_time: "2024-01-01".to_string(),
14345            updated_time: String::new(),
14346            deleted_time: String::new(),
14347            drift_status: String::new(),
14348            last_drift_check_time: String::new(),
14349            status_reason: String::new(),
14350            description: String::new(),
14351            detailed_status: String::new(),
14352            root_stack: String::new(),
14353            parent_stack: String::new(),
14354            termination_protection: false,
14355            iam_role: String::new(),
14356            tags: Vec::new(),
14357            stack_policy: String::new(),
14358            rollback_monitoring_time: String::new(),
14359            rollback_alarms: Vec::new(),
14360            notification_arns: Vec::new(),
14361        }];
14362        app.cfn_state.table.selected = 5;
14363
14364        app.handle_action(Action::ToggleFilterCheckbox);
14365
14366        assert!(app.cfn_state.view_nested, "view_nested must be toggled on");
14367        assert_eq!(app.cfn_state.table.selected, 0, "selection must reset");
14368        // Regression fix: toggling view_nested must clear items and set loading=true
14369        // so main.rs triggers a reload with the new include_nested value.
14370        // Items stay visible during reload (no flash of empty)
14371        assert!(
14372            app.cfn_state.table.loading,
14373            "loading must be set to trigger reload in main.rs"
14374        );
14375        assert!(
14376            app.cfn_state.table.loading,
14377            "loading must be set to trigger reload in main.rs"
14378        );
14379    }
14380
14381    #[test]
14382    fn test_cfn_view_nested_toggle_off_also_triggers_reload() {
14383        // Toggling view_nested OFF must also reload (hides nested stacks).
14384        use crate::ui::cfn::VIEW_NESTED;
14385        let mut app = test_app();
14386        app.service_selected = true;
14387        app.current_service = Service::CloudFormationStacks;
14388        app.mode = Mode::FilterInput;
14389        app.cfn_state.input_focus = VIEW_NESTED;
14390        app.cfn_state.view_nested = true; // already on, toggling off
14391        app.cfn_state.table.items = vec![CfnStack {
14392            name: "nested-stack".to_string(),
14393            stack_id: "id2".to_string(),
14394            status: "CREATE_COMPLETE".to_string(),
14395            created_time: "2024-01-01".to_string(),
14396            updated_time: String::new(),
14397            deleted_time: String::new(),
14398            drift_status: String::new(),
14399            last_drift_check_time: String::new(),
14400            status_reason: String::new(),
14401            description: String::new(),
14402            detailed_status: String::new(),
14403            root_stack: String::new(),
14404            parent_stack: String::new(),
14405            termination_protection: false,
14406            iam_role: String::new(),
14407            tags: Vec::new(),
14408            stack_policy: String::new(),
14409            rollback_monitoring_time: String::new(),
14410            rollback_alarms: Vec::new(),
14411            notification_arns: Vec::new(),
14412        }];
14413
14414        app.handle_action(Action::ToggleFilterCheckbox);
14415
14416        assert!(
14417            !app.cfn_state.view_nested,
14418            "view_nested must be toggled off"
14419        );
14420        // Items stay visible during reload
14421        assert!(
14422            !app.cfn_state.table.items.is_empty(),
14423            "items must stay visible during reload (no flash)"
14424        );
14425        assert!(
14426            app.cfn_state.table.loading,
14427            "loading must be set to trigger reload"
14428        );
14429    }
14430
14431    #[test]
14432    fn test_cfn_view_nested_hierarchical_shows_children_when_expanded() {
14433        // When view_nested=true and a parent stack is expanded, its children
14434        // must appear immediately after the parent in filtered_cloudformation_stacks.
14435        use crate::ui::cfn::filtered_cloudformation_stacks;
14436
14437        let mut app = test_app();
14438        app.current_service = Service::CloudFormationStacks;
14439        app.service_selected = true;
14440        app.cfn_state.view_nested = true;
14441
14442        let parent_id = "arn:aws:cloudformation:us-east-1:123:stack/parent/abc123".to_string();
14443        let child_id =
14444            "arn:aws:cloudformation:us-east-1:123:stack/parent-nested/def456".to_string();
14445
14446        app.cfn_state.table.items = vec![
14447            CfnStack {
14448                name: "parent-stack".to_string(),
14449                stack_id: parent_id.clone(),
14450                status: "CREATE_COMPLETE".to_string(),
14451                parent_stack: String::new(), // no parent = root
14452                root_stack: parent_id.clone(),
14453                created_time: "2024-01-02".to_string(),
14454                ..Default::default()
14455            },
14456            CfnStack {
14457                name: "child-stack".to_string(),
14458                stack_id: child_id.clone(),
14459                status: "CREATE_COMPLETE".to_string(),
14460                parent_stack: parent_id.clone(), // parent = root stack
14461                root_stack: parent_id.clone(),
14462                created_time: "2024-01-01".to_string(),
14463                ..Default::default()
14464            },
14465        ];
14466
14467        // With view_nested=true: both root and child visible (expand all by default)
14468        let filtered = filtered_cloudformation_stacks(&app);
14469        assert_eq!(
14470            filtered.len(),
14471            2,
14472            "Root + child shown when view_nested=true"
14473        );
14474        assert_eq!(filtered[0].name, "parent-stack", "Root first");
14475        assert_eq!(filtered[1].name, "child-stack", "Child after root");
14476    }
14477
14478    #[test]
14479    fn test_cfn_template_scroll_up() {
14480        let mut app = test_app();
14481        app.service_selected = true;
14482        app.current_service = Service::CloudFormationStacks;
14483        app.cfn_state.current_stack = Some("test-stack".to_string());
14484        app.cfn_state.detail_tab = CfnDetailTab::Template;
14485        app.cfn_state.template_scroll = 20;
14486
14487        app.page_up();
14488
14489        assert_eq!(app.cfn_state.template_scroll, 10);
14490    }
14491
14492    #[test]
14493    fn test_cfn_template_scroll_down() {
14494        let mut app = test_app();
14495        app.service_selected = true;
14496        app.current_service = Service::CloudFormationStacks;
14497        app.cfn_state.current_stack = Some("test-stack".to_string());
14498        app.cfn_state.detail_tab = CfnDetailTab::Template;
14499        app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
14500        app.cfn_state.template_scroll = 0;
14501
14502        app.page_down();
14503
14504        assert_eq!(app.cfn_state.template_scroll, 10);
14505    }
14506
14507    #[test]
14508    fn test_cfn_template_scroll_down_respects_max() {
14509        let mut app = test_app();
14510        app.service_selected = true;
14511        app.current_service = Service::CloudFormationStacks;
14512        app.cfn_state.current_stack = Some("test-stack".to_string());
14513        app.cfn_state.detail_tab = CfnDetailTab::Template;
14514        app.cfn_state.template_body = "line1\nline2\nline3".to_string();
14515        app.cfn_state.template_scroll = 0;
14516
14517        app.page_down();
14518
14519        // Should not scroll past the last line (3 lines = max scroll of 2)
14520        assert_eq!(app.cfn_state.template_scroll, 2);
14521    }
14522
14523    #[test]
14524    fn test_cfn_template_arrow_up() {
14525        let mut app = test_app();
14526        app.service_selected = true;
14527        app.current_service = Service::CloudFormationStacks;
14528        app.mode = Mode::Normal;
14529        app.cfn_state.current_stack = Some("test-stack".to_string());
14530        app.cfn_state.detail_tab = CfnDetailTab::Template;
14531        app.cfn_state.template_scroll = 5;
14532
14533        app.prev_item();
14534
14535        assert_eq!(app.cfn_state.template_scroll, 4);
14536    }
14537
14538    #[test]
14539    fn test_cfn_template_arrow_down() {
14540        let mut app = test_app();
14541        app.service_selected = true;
14542        app.current_service = Service::CloudFormationStacks;
14543        app.mode = Mode::Normal;
14544        app.cfn_state.current_stack = Some("test-stack".to_string());
14545        app.cfn_state.detail_tab = CfnDetailTab::Template;
14546        app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
14547        app.cfn_state.template_scroll = 2;
14548
14549        app.next_item();
14550
14551        assert_eq!(app.cfn_state.template_scroll, 3);
14552    }
14553
14554    #[test]
14555    fn test_cfn_template_arrow_down_respects_max() {
14556        let mut app = test_app();
14557        app.service_selected = true;
14558        app.current_service = Service::CloudFormationStacks;
14559        app.mode = Mode::Normal;
14560        app.cfn_state.current_stack = Some("test-stack".to_string());
14561        app.cfn_state.detail_tab = CfnDetailTab::Template;
14562        app.cfn_state.template_body = "line1\nline2".to_string();
14563        app.cfn_state.template_scroll = 1;
14564
14565        app.next_item();
14566
14567        // Should stay at max (2 lines = max scroll of 1)
14568        assert_eq!(app.cfn_state.template_scroll, 1);
14569    }
14570
14571    #[test]
14572    fn test_cfn_yank_on_template_tab_copies_template_body() {
14573        // Regression: yank() always copied stack_id regardless of active tab.
14574        // On Template tab it must copy the template body.
14575        let mut app = test_app();
14576        app.current_service = Service::CloudFormationStacks;
14577        app.cfn_state.current_stack = Some("my-stack".to_string());
14578        app.cfn_state.detail_tab = CfnDetailTab::Template;
14579        app.cfn_state.template_body = "AWSTemplateFormatVersion: '2010-09-09'".to_string();
14580        // The yank function should not panic and should handle template tab.
14581        // We can't assert clipboard contents in tests, so verify it doesn't
14582        // call stack_id path by ensuring template_body is non-empty.
14583        assert!(!app.cfn_state.template_body.is_empty());
14584        crate::cfn::actions::yank(&app); // must not panic
14585    }
14586
14587    #[test]
14588    fn test_cfn_change_sets_console_url_includes_events_view_graph() {
14589        use crate::cfn::console_url_stack_detail_with_tab;
14590        let url = console_url_stack_detail_with_tab(
14591            "us-east-1",
14592            "arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack/abc123",
14593            &CfnDetailTab::ChangeSets,
14594        );
14595        assert!(
14596            url.contains("eventsView=graph"),
14597            "ChangeSets URL must include eventsView=graph, got: {url}"
14598        );
14599        assert!(url.contains("changesets"), "URL must contain 'changesets'");
14600    }
14601
14602    #[test]
14603    fn test_cfn_other_tabs_console_url_no_events_view_graph() {
14604        use crate::cfn::console_url_stack_detail_with_tab;
14605        for tab in [
14606            CfnDetailTab::StackInfo,
14607            CfnDetailTab::Events,
14608            CfnDetailTab::Resources,
14609        ] {
14610            let url = console_url_stack_detail_with_tab(
14611                "us-east-1",
14612                "arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack/abc123",
14613                &tab,
14614            );
14615            assert!(
14616                !url.contains("eventsView=graph"),
14617                "{tab:?} URL must NOT include eventsView=graph, got: {url}"
14618            );
14619        }
14620    }
14621
14622    #[test]
14623    fn test_cfn_change_sets_expand_collapse() {
14624        let mut app = test_app();
14625        app.current_service = Service::CloudFormationStacks;
14626        app.service_selected = true;
14627        app.cfn_state.current_stack = Some("my-stack".to_string());
14628        app.cfn_state.detail_tab = CfnDetailTab::ChangeSets;
14629        app.cfn_state.change_sets.items = vec![rusticity_core::cfn::StackChangeSet {
14630            name: "cs-001".to_string(),
14631            change_set_id: "id-001".to_string(),
14632            created_time: String::new(),
14633            status: "CREATE_COMPLETE".to_string(),
14634            description: String::new(),
14635            root_change_set_id: String::new(),
14636            parent_change_set_id: String::new(),
14637        }];
14638        app.cfn_state.change_sets.selected = 0;
14639
14640        assert_eq!(app.cfn_state.change_sets.expanded_item, None);
14641        app.handle_action(Action::ExpandRow);
14642        assert_eq!(
14643            app.cfn_state.change_sets.expanded_item,
14644            Some(0),
14645            "Enter must expand change set row"
14646        );
14647        app.handle_action(Action::CollapseRow);
14648        assert_eq!(
14649            app.cfn_state.change_sets.expanded_item, None,
14650            "Left arrow must collapse change set row"
14651        );
14652    }
14653
14654    #[test]
14655    fn test_cfn_events_toggle_column_hides_visible_column() {
14656        // Regression: the inline toggle_column block in app.rs had no Events or
14657        // ChangeSets branch — ToggleColumn on Events tab was a no-op.
14658        let mut app = test_app();
14659        app.current_service = Service::CloudFormationStacks;
14660        app.mode = Mode::ColumnSelector;
14661        app.cfn_state.current_stack = Some("my-stack".to_string());
14662        app.cfn_state.detail_tab = CfnDetailTab::Events;
14663
14664        // column_selector_index 1 = first column (OperationId, which is visible by default)
14665        let first_col = app.cfn_event_column_ids[0];
14666        assert!(
14667            app.cfn_event_visible_column_ids.contains(&first_col),
14668            "First column must be visible before toggle"
14669        );
14670
14671        app.column_selector_index = 1; // index 1 = first column
14672        app.handle_action(Action::ToggleColumn);
14673
14674        assert!(
14675            !app.cfn_event_visible_column_ids.contains(&first_col),
14676            "ToggleColumn must hide the first Events column"
14677        );
14678
14679        // Toggle again → should re-add it
14680        app.handle_action(Action::ToggleColumn);
14681        assert!(
14682            app.cfn_event_visible_column_ids.contains(&first_col),
14683            "ToggleColumn again must re-show the column"
14684        );
14685    }
14686
14687    #[test]
14688    fn test_cfn_events_toggle_column_keeps_at_least_one_visible() {
14689        // At least 1 column must remain visible
14690        let mut app = test_app();
14691        app.current_service = Service::CloudFormationStacks;
14692        app.mode = Mode::ColumnSelector;
14693        app.cfn_state.current_stack = Some("my-stack".to_string());
14694        app.cfn_state.detail_tab = CfnDetailTab::Events;
14695
14696        // Force only 1 visible column
14697        app.cfn_event_visible_column_ids = vec![app.cfn_event_column_ids[0]];
14698        app.column_selector_index = 1;
14699
14700        app.handle_action(Action::ToggleColumn);
14701
14702        // Must still have at least 1
14703        assert!(
14704            !app.cfn_event_visible_column_ids.is_empty(),
14705            "At least one column must remain visible"
14706        );
14707    }
14708
14709    #[test]
14710    fn test_space_menu_o_opens_service_picker() {
14711        // Verify the full flow: SpaceMenu → 'o' → ServicePicker mode
14712        let mut app = test_app();
14713        app.handle_action(Action::OpenSpaceMenu);
14714        assert_eq!(app.mode, Mode::SpaceMenu);
14715
14716        app.handle_action(Action::OpenServicePicker);
14717        assert_eq!(
14718            app.mode,
14719            Mode::ServicePicker,
14720            "'o' from SpaceMenu must open ServicePicker"
14721        );
14722    }
14723
14724    #[test]
14725    fn test_space_menu_c_closes_service() {
14726        let mut app = test_app();
14727        app.current_service = Service::CloudFormationStacks;
14728        app.service_selected = true;
14729        app.tabs.push(Tab {
14730            service: Service::CloudFormationStacks,
14731            title: "CFN".to_string(),
14732            breadcrumb: "CloudFormation".to_string(),
14733        });
14734        app.current_tab = 0;
14735
14736        app.handle_action(Action::OpenSpaceMenu);
14737        app.handle_action(Action::CloseService);
14738        // Mode should return to normal
14739        assert_ne!(
14740            app.mode,
14741            Mode::SpaceMenu,
14742            "CloseService must exit SpaceMenu"
14743        );
14744    }
14745}
14746
14747#[cfg(test)]
14748mod lambda_version_tab_tests {
14749    use super::*;
14750    use crate::ui::iam::POLICY_TYPE_DROPDOWN;
14751    use test_helpers::*;
14752
14753    #[test]
14754    fn test_lambda_version_tab_cycling_next() {
14755        let mut app = test_app();
14756        app.current_service = Service::LambdaFunctions;
14757        app.lambda_state.current_function = Some("test-function".to_string());
14758        app.lambda_state.current_version = Some("1".to_string());
14759        app.lambda_state.detail_tab = LambdaDetailTab::Code;
14760
14761        // Code -> Monitor
14762        app.handle_action(Action::NextDetailTab);
14763        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14764        assert!(app.lambda_state.metrics_loading);
14765
14766        // Monitor -> Configuration
14767        app.lambda_state.metrics_loading = false;
14768        app.handle_action(Action::NextDetailTab);
14769        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14770
14771        // Configuration -> Code
14772        app.handle_action(Action::NextDetailTab);
14773        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14774    }
14775
14776    #[test]
14777    fn test_lambda_version_tab_cycling_prev() {
14778        let mut app = test_app();
14779        app.current_service = Service::LambdaFunctions;
14780        app.lambda_state.current_function = Some("test-function".to_string());
14781        app.lambda_state.current_version = Some("1".to_string());
14782        app.lambda_state.detail_tab = LambdaDetailTab::Code;
14783
14784        // Code -> Configuration
14785        app.handle_action(Action::PrevDetailTab);
14786        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14787
14788        // Configuration -> Monitor
14789        app.handle_action(Action::PrevDetailTab);
14790        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14791        assert!(app.lambda_state.metrics_loading);
14792
14793        // Monitor -> Code
14794        app.lambda_state.metrics_loading = false;
14795        app.handle_action(Action::PrevDetailTab);
14796        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14797    }
14798
14799    #[test]
14800    fn test_lambda_version_monitor_clears_metrics() {
14801        let mut app = test_app();
14802        app.current_service = Service::LambdaFunctions;
14803        app.lambda_state.current_function = Some("test-function".to_string());
14804        app.lambda_state.current_version = Some("1".to_string());
14805        app.lambda_state.detail_tab = LambdaDetailTab::Code;
14806
14807        // Add some fake metric data
14808        app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
14809        app.lambda_state.monitoring_scroll = 5;
14810
14811        // Switch to Monitor tab
14812        app.handle_action(Action::NextDetailTab);
14813
14814        assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14815        assert!(app.lambda_state.metrics_loading);
14816        assert_eq!(app.lambda_state.monitoring_scroll, 0);
14817        assert!(app.lambda_state.metric_data_invocations.is_empty());
14818    }
14819
14820    #[test]
14821    fn test_cfn_parameters_expand_collapse() {
14822        let mut app = test_app();
14823        app.current_service = Service::CloudFormationStacks;
14824        app.service_selected = true;
14825        app.cfn_state.current_stack = Some("test-stack".to_string());
14826        app.cfn_state.detail_tab = CfnDetailTab::Parameters;
14827        app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
14828            key: "Param1".to_string(),
14829            value: "Value1".to_string(),
14830            resolved_value: "Resolved1".to_string(),
14831        }];
14832        app.cfn_state.parameters.reset();
14833
14834        assert_eq!(app.cfn_state.parameters.expanded_item, None);
14835
14836        // Right arrow expands
14837        app.handle_action(Action::NextPane);
14838        assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
14839
14840        // Left arrow collapses
14841        app.handle_action(Action::PrevPane);
14842        assert_eq!(app.cfn_state.parameters.expanded_item, None);
14843    }
14844
14845    #[test]
14846    fn test_cfn_parameters_filter_resets_selection() {
14847        let mut app = test_app();
14848        app.current_service = Service::CloudFormationStacks;
14849        app.service_selected = true;
14850        app.cfn_state.current_stack = Some("test-stack".to_string());
14851        app.cfn_state.detail_tab = CfnDetailTab::Parameters;
14852        app.cfn_state.parameters.items = vec![
14853            rusticity_core::cfn::StackParameter {
14854                key: "DatabaseName".to_string(),
14855                value: "mydb".to_string(),
14856                resolved_value: "mydb".to_string(),
14857            },
14858            rusticity_core::cfn::StackParameter {
14859                key: "InstanceType".to_string(),
14860                value: "t2.micro".to_string(),
14861                resolved_value: "t2.micro".to_string(),
14862            },
14863            rusticity_core::cfn::StackParameter {
14864                key: "Environment".to_string(),
14865                value: "production".to_string(),
14866                resolved_value: "production".to_string(),
14867            },
14868        ];
14869        app.cfn_state.parameters.selected = 2; // Select third item
14870        app.mode = Mode::FilterInput;
14871        app.cfn_state.parameters_input_focus = InputFocus::Filter;
14872
14873        // Type a filter character - should reset selection
14874        app.handle_action(Action::FilterInput('D'));
14875        assert_eq!(app.cfn_state.parameters.selected, 0);
14876        assert_eq!(app.cfn_state.parameters.filter, "D");
14877
14878        // Select another item
14879        app.cfn_state.parameters.selected = 1;
14880
14881        // Type another character - should reset again
14882        app.handle_action(Action::FilterInput('a'));
14883        assert_eq!(app.cfn_state.parameters.selected, 0);
14884        assert_eq!(app.cfn_state.parameters.filter, "Da");
14885
14886        // Select another item
14887        app.cfn_state.parameters.selected = 1;
14888
14889        // Backspace - should also reset
14890        app.handle_action(Action::FilterBackspace);
14891        assert_eq!(app.cfn_state.parameters.selected, 0);
14892        assert_eq!(app.cfn_state.parameters.filter, "D");
14893    }
14894
14895    #[test]
14896    fn test_cfn_template_tab_no_preferences() {
14897        let mut app = test_app();
14898        app.current_service = Service::CloudFormationStacks;
14899        app.service_selected = true;
14900        app.cfn_state.current_stack = Some("test-stack".to_string());
14901        app.cfn_state.detail_tab = CfnDetailTab::Template;
14902        app.mode = Mode::Normal;
14903
14904        // Try to open preferences - should be ignored
14905        app.handle_action(Action::OpenColumnSelector);
14906        assert_eq!(app.mode, Mode::Normal); // Should stay in Normal mode
14907
14908        // GitSync tab should also not allow preferences
14909        app.cfn_state.detail_tab = CfnDetailTab::GitSync;
14910        app.handle_action(Action::OpenColumnSelector);
14911        assert_eq!(app.mode, Mode::Normal); // Should stay in Normal mode
14912
14913        // Parameters tab should allow preferences
14914        app.cfn_state.detail_tab = CfnDetailTab::Parameters;
14915        app.handle_action(Action::OpenColumnSelector);
14916        assert_eq!(app.mode, Mode::ColumnSelector); // Should open preferences
14917
14918        // Outputs tab should allow preferences
14919        app.mode = Mode::Normal;
14920        app.cfn_state.detail_tab = CfnDetailTab::Outputs;
14921        app.handle_action(Action::OpenColumnSelector);
14922        assert_eq!(app.mode, Mode::ColumnSelector); // Should open preferences
14923    }
14924
14925    #[test]
14926    fn test_iam_user_groups_tab_shows_preferences() {
14927        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14928        app.current_service = Service::IamUsers;
14929        app.service_selected = true;
14930        app.mode = Mode::Normal;
14931        app.iam_state.current_user = Some("test-user".to_string());
14932        app.iam_state.user_tab = UserTab::Groups;
14933
14934        // Should allow opening preferences
14935        app.handle_action(Action::OpenColumnSelector);
14936        assert_eq!(app.mode, Mode::ColumnSelector);
14937    }
14938
14939    #[test]
14940    fn test_iam_user_tags_tab_shows_preferences() {
14941        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14942        app.current_service = Service::IamUsers;
14943        app.service_selected = true;
14944        app.mode = Mode::Normal;
14945        app.iam_state.current_user = Some("test-user".to_string());
14946        app.iam_state.user_tab = UserTab::Tags;
14947
14948        // Should allow opening preferences
14949        app.handle_action(Action::OpenColumnSelector);
14950        assert_eq!(app.mode, Mode::ColumnSelector);
14951    }
14952
14953    #[test]
14954    fn test_iam_user_last_accessed_tab_shows_preferences() {
14955        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14956        app.current_service = Service::IamUsers;
14957        app.service_selected = true;
14958        app.mode = Mode::Normal;
14959        app.iam_state.current_user = Some("test-user".to_string());
14960        app.iam_state.user_tab = UserTab::LastAccessed;
14961
14962        // Should allow opening preferences
14963        app.handle_action(Action::OpenColumnSelector);
14964        assert_eq!(app.mode, Mode::ColumnSelector);
14965    }
14966
14967    #[test]
14968    fn test_iam_user_security_credentials_tab_no_preferences() {
14969        let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14970        app.current_service = Service::IamUsers;
14971        app.service_selected = true;
14972        app.mode = Mode::Normal;
14973        app.iam_state.current_user = Some("test-user".to_string());
14974        app.iam_state.user_tab = UserTab::SecurityCredentials;
14975
14976        // Should NOT allow opening preferences
14977        app.handle_action(Action::OpenColumnSelector);
14978        assert_eq!(app.mode, Mode::Normal);
14979    }
14980
14981    #[test]
14982    fn test_iam_user_tabs_without_column_preferences() {
14983        let mut app = test_app();
14984        app.current_service = Service::IamUsers;
14985        app.service_selected = true;
14986        app.iam_state.current_user = Some("test-user".to_string());
14987        app.mode = Mode::Normal;
14988
14989        // Groups tab should allow preferences (page size)
14990        app.iam_state.user_tab = UserTab::Groups;
14991        app.handle_action(Action::OpenColumnSelector);
14992        assert_eq!(app.mode, Mode::ColumnSelector);
14993        app.mode = Mode::Normal;
14994
14995        // Tags tab should allow preferences (page size)
14996        app.iam_state.user_tab = UserTab::Tags;
14997        app.handle_action(Action::OpenColumnSelector);
14998        assert_eq!(app.mode, Mode::ColumnSelector);
14999        app.mode = Mode::Normal;
15000
15001        // SecurityCredentials tab should not allow preferences
15002        app.iam_state.user_tab = UserTab::SecurityCredentials;
15003        app.handle_action(Action::OpenColumnSelector);
15004        assert_eq!(app.mode, Mode::Normal);
15005
15006        // LastAccessed tab should allow preferences (page size)
15007        app.iam_state.user_tab = UserTab::LastAccessed;
15008        app.handle_action(Action::OpenColumnSelector);
15009        assert_eq!(app.mode, Mode::ColumnSelector);
15010        app.mode = Mode::Normal;
15011
15012        // Permissions tab should allow preferences
15013        app.iam_state.user_tab = UserTab::Permissions;
15014        app.handle_action(Action::OpenColumnSelector);
15015        assert_eq!(app.mode, Mode::ColumnSelector);
15016
15017        // User list (no current_user) should allow preferences
15018        app.mode = Mode::Normal;
15019        app.iam_state.current_user = None;
15020        app.handle_action(Action::OpenColumnSelector);
15021        assert_eq!(app.mode, Mode::ColumnSelector);
15022    }
15023
15024    #[test]
15025    fn test_iam_role_policies_dropdown_cycling() {
15026        let mut app = test_app();
15027        app.current_service = Service::IamRoles;
15028        app.service_selected = true;
15029        app.iam_state.current_role = Some("test-role".to_string());
15030        app.iam_state.role_tab = RoleTab::Permissions;
15031        app.mode = Mode::FilterInput;
15032        app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
15033        app.iam_state.policy_type_filter = "All types".to_string();
15034
15035        // Test next cycling
15036        app.next_item();
15037        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
15038        app.next_item();
15039        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
15040        app.next_item();
15041        assert_eq!(app.iam_state.policy_type_filter, "All types");
15042
15043        // Test prev cycling
15044        app.prev_item();
15045        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
15046        app.prev_item();
15047        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
15048        app.prev_item();
15049        assert_eq!(app.iam_state.policy_type_filter, "All types");
15050    }
15051
15052    #[test]
15053    fn test_iam_user_policies_dropdown_cycling() {
15054        let mut app = test_app();
15055        app.current_service = Service::IamUsers;
15056        app.service_selected = true;
15057        app.iam_state.current_user = Some("test-user".to_string());
15058        app.iam_state.user_tab = UserTab::Permissions;
15059        app.mode = Mode::FilterInput;
15060        app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
15061        app.iam_state.policy_type_filter = "All types".to_string();
15062
15063        // Test next cycling
15064        app.next_item();
15065        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
15066        app.next_item();
15067        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
15068        app.next_item();
15069        assert_eq!(app.iam_state.policy_type_filter, "All types");
15070
15071        // Test prev cycling
15072        app.prev_item();
15073        assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
15074        app.prev_item();
15075        assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
15076        app.prev_item();
15077        assert_eq!(app.iam_state.policy_type_filter, "All types");
15078    }
15079
15080    #[test]
15081    fn test_iam_role_tabs_without_column_preferences() {
15082        let mut app = test_app();
15083        app.current_service = Service::IamRoles;
15084        app.service_selected = true;
15085        app.iam_state.current_role = Some("test-role".to_string());
15086        app.mode = Mode::Normal;
15087
15088        // TrustRelationships tab should not allow preferences
15089        app.iam_state.role_tab = RoleTab::TrustRelationships;
15090        app.handle_action(Action::OpenColumnSelector);
15091        assert_eq!(app.mode, Mode::Normal);
15092
15093        // RevokeSessions tab should not allow preferences
15094        app.iam_state.role_tab = RoleTab::RevokeSessions;
15095        app.handle_action(Action::OpenColumnSelector);
15096        assert_eq!(app.mode, Mode::Normal);
15097
15098        // LastAccessed tab should allow preferences
15099        app.iam_state.role_tab = RoleTab::LastAccessed;
15100        app.handle_action(Action::OpenColumnSelector);
15101        assert_eq!(app.mode, Mode::ColumnSelector);
15102
15103        // Permissions tab should allow preferences
15104        app.mode = Mode::Normal;
15105        app.iam_state.role_tab = RoleTab::Permissions;
15106        app.handle_action(Action::OpenColumnSelector);
15107        assert_eq!(app.mode, Mode::ColumnSelector);
15108
15109        // Tags tab should allow preferences
15110        app.mode = Mode::Normal;
15111        app.iam_state.role_tab = RoleTab::Tags;
15112        app.handle_action(Action::OpenColumnSelector);
15113        assert_eq!(app.mode, Mode::ColumnSelector);
15114
15115        // Role list (no current_role) should allow preferences
15116        app.mode = Mode::Normal;
15117        app.iam_state.current_role = None;
15118        app.handle_action(Action::OpenColumnSelector);
15119        assert_eq!(app.mode, Mode::ColumnSelector);
15120    }
15121
15122    #[test]
15123    fn test_iam_role_tags_tab_cycling() {
15124        let mut app = test_app();
15125        app.current_service = Service::IamRoles;
15126        app.service_selected = true;
15127        app.iam_state.current_role = Some("test-role".to_string());
15128        app.iam_state.role_tab = RoleTab::Tags;
15129        app.mode = Mode::ColumnSelector;
15130        app.column_selector_index = 0;
15131
15132        // NextPreferences from column section -> page size
15133        app.handle_action(Action::NextPreferences);
15134        assert_eq!(app.column_selector_index, 4);
15135
15136        // NextPreferences from page size -> column section
15137        app.handle_action(Action::NextPreferences);
15138        assert_eq!(app.column_selector_index, 0);
15139
15140        // PrevPreferences from column section -> page size
15141        app.handle_action(Action::PrevPreferences);
15142        assert_eq!(app.column_selector_index, 4);
15143
15144        // PrevPreferences from page size -> column section
15145        app.handle_action(Action::PrevPreferences);
15146        assert_eq!(app.column_selector_index, 0);
15147    }
15148
15149    #[test]
15150    fn test_cfn_outputs_expand_collapse() {
15151        let mut app = test_app();
15152        app.current_service = Service::CloudFormationStacks;
15153        app.service_selected = true;
15154        app.cfn_state.current_stack = Some("test-stack".to_string());
15155        app.cfn_state.detail_tab = CfnDetailTab::Outputs;
15156        app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
15157            key: "Output1".to_string(),
15158            value: "Value1".to_string(),
15159            description: "Description1".to_string(),
15160            export_name: "Export1".to_string(),
15161        }];
15162        app.cfn_state.outputs.reset();
15163
15164        assert_eq!(app.cfn_state.outputs.expanded_item, None);
15165
15166        // Right arrow expands
15167        app.handle_action(Action::NextPane);
15168        assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
15169
15170        // Left arrow collapses
15171        app.handle_action(Action::PrevPane);
15172        assert_eq!(app.cfn_state.outputs.expanded_item, None);
15173    }
15174
15175    #[test]
15176    fn test_cfn_outputs_filter_resets_selection() {
15177        let mut app = test_app();
15178        app.current_service = Service::CloudFormationStacks;
15179        app.service_selected = true;
15180        app.cfn_state.current_stack = Some("test-stack".to_string());
15181        app.cfn_state.detail_tab = CfnDetailTab::Outputs;
15182        app.cfn_state.outputs.items = vec![
15183            rusticity_core::cfn::StackOutput {
15184                key: "ApiUrl".to_string(),
15185                value: "https://api.example.com".to_string(),
15186                description: "API endpoint".to_string(),
15187                export_name: "MyApiUrl".to_string(),
15188            },
15189            rusticity_core::cfn::StackOutput {
15190                key: "BucketName".to_string(),
15191                value: "my-bucket".to_string(),
15192                description: "S3 bucket".to_string(),
15193                export_name: "MyBucket".to_string(),
15194            },
15195        ];
15196        app.cfn_state.outputs.reset();
15197        app.cfn_state.outputs.selected = 1;
15198
15199        // Start filter mode
15200        app.handle_action(Action::StartFilter);
15201        assert_eq!(app.mode, Mode::FilterInput);
15202
15203        // Type a character - should reset selection
15204        app.handle_action(Action::FilterInput('A'));
15205        assert_eq!(app.cfn_state.outputs.selected, 0);
15206        assert_eq!(app.cfn_state.outputs.filter, "A");
15207
15208        // Type more
15209        app.cfn_state.outputs.selected = 1;
15210        app.handle_action(Action::FilterInput('p'));
15211        assert_eq!(app.cfn_state.outputs.selected, 0);
15212
15213        // Backspace should also reset selection
15214        app.cfn_state.outputs.selected = 1;
15215        app.handle_action(Action::FilterBackspace);
15216        assert_eq!(app.cfn_state.outputs.selected, 0);
15217    }
15218
15219    #[test]
15220    fn test_ec2_service_in_picker() {
15221        let app = test_app();
15222        assert!(app.service_picker.services.contains(&"EC2 › Instances"));
15223    }
15224
15225    #[test]
15226    fn test_ec2_state_filter_cycles() {
15227        let mut app = test_app();
15228        app.current_service = Service::Ec2Instances;
15229        app.service_selected = true;
15230        app.mode = Mode::FilterInput;
15231        app.ec2_state.input_focus = EC2_STATE_FILTER;
15232
15233        let initial = app.ec2_state.state_filter;
15234        assert_eq!(initial, Ec2StateFilter::AllStates);
15235
15236        // Cycle through filters using ToggleFilterCheckbox
15237        app.handle_action(Action::ToggleFilterCheckbox);
15238        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15239
15240        app.handle_action(Action::ToggleFilterCheckbox);
15241        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
15242
15243        app.handle_action(Action::ToggleFilterCheckbox);
15244        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
15245
15246        app.handle_action(Action::ToggleFilterCheckbox);
15247        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
15248
15249        app.handle_action(Action::ToggleFilterCheckbox);
15250        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
15251
15252        app.handle_action(Action::ToggleFilterCheckbox);
15253        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
15254
15255        app.handle_action(Action::ToggleFilterCheckbox);
15256        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15257    }
15258
15259    #[test]
15260    fn test_ec2_filter_resets_table() {
15261        let mut app = test_app();
15262        app.current_service = Service::Ec2Instances;
15263        app.service_selected = true;
15264        app.mode = Mode::FilterInput;
15265        app.ec2_state.input_focus = EC2_STATE_FILTER;
15266        app.ec2_state.table.selected = 5;
15267
15268        app.handle_action(Action::ToggleFilterCheckbox);
15269        assert_eq!(app.ec2_state.table.selected, 0);
15270    }
15271
15272    #[test]
15273    fn test_ec2_columns_visible() {
15274        let app = test_app();
15275        assert_eq!(app.ec2_visible_column_ids.len(), 16); // Default visible columns
15276        assert_eq!(app.ec2_column_ids.len(), 52); // Total available columns
15277    }
15278
15279    #[test]
15280    fn test_ec2_breadcrumbs() {
15281        let mut app = test_app();
15282        app.current_service = Service::Ec2Instances;
15283        app.service_selected = true;
15284        let breadcrumb = app.breadcrumbs();
15285        assert_eq!(breadcrumb, "EC2 > Instances");
15286    }
15287
15288    #[test]
15289    fn test_ec2_console_url() {
15290        let mut app = test_app();
15291        app.current_service = Service::Ec2Instances;
15292        app.service_selected = true;
15293        let url = app.get_console_url();
15294        assert!(url.contains("ec2"));
15295        assert!(url.contains("Instances"));
15296    }
15297
15298    #[test]
15299    fn test_ec2_filter_handling() {
15300        let mut app = test_app();
15301        app.current_service = Service::Ec2Instances;
15302        app.service_selected = true;
15303        app.mode = Mode::FilterInput;
15304
15305        app.handle_action(Action::FilterInput('t'));
15306        app.handle_action(Action::FilterInput('e'));
15307        app.handle_action(Action::FilterInput('s'));
15308        app.handle_action(Action::FilterInput('t'));
15309
15310        assert_eq!(app.ec2_state.table.filter, "test");
15311
15312        app.handle_action(Action::FilterBackspace);
15313        assert_eq!(app.ec2_state.table.filter, "tes");
15314    }
15315
15316    #[test]
15317    fn test_column_selector_page_down_ec2() {
15318        let mut app = test_app();
15319        app.current_service = Service::Ec2Instances;
15320        app.service_selected = true;
15321        app.mode = Mode::ColumnSelector;
15322        app.column_selector_index = 0;
15323
15324        app.handle_action(Action::PageDown);
15325        assert_eq!(app.column_selector_index, 10);
15326
15327        app.handle_action(Action::PageDown);
15328        assert_eq!(app.column_selector_index, 20);
15329    }
15330
15331    #[test]
15332    fn test_column_selector_page_up_ec2() {
15333        let mut app = test_app();
15334        app.current_service = Service::Ec2Instances;
15335        app.service_selected = true;
15336        app.mode = Mode::ColumnSelector;
15337        app.column_selector_index = 30;
15338
15339        app.handle_action(Action::PageUp);
15340        assert_eq!(app.column_selector_index, 20);
15341
15342        app.handle_action(Action::PageUp);
15343        assert_eq!(app.column_selector_index, 10);
15344    }
15345
15346    #[test]
15347    fn test_ec2_state_filter_dropdown_focus() {
15348        let mut app = test_app();
15349        app.current_service = Service::Ec2Instances;
15350        app.service_selected = true;
15351        app.mode = Mode::FilterInput;
15352
15353        // Tab to state filter
15354        app.handle_action(Action::NextFilterFocus);
15355        assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
15356
15357        // Dropdown should show when focused (tested in render)
15358        // Verify we can cycle the filter
15359        app.handle_action(Action::ToggleFilterCheckbox);
15360        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15361    }
15362
15363    #[test]
15364    fn test_column_selector_ctrl_d_scrolling() {
15365        let mut app = test_app();
15366        app.current_service = Service::LambdaFunctions;
15367        app.mode = Mode::ColumnSelector;
15368        app.column_selector_index = 0;
15369
15370        // PageDown from 0 by 10 lands on blank row (10), skips to 11
15371        app.handle_action(Action::PageDown);
15372        assert_eq!(app.column_selector_index, 11);
15373
15374        // Second PageDown should be capped at max
15375        let max = app.get_column_selector_max();
15376        app.handle_action(Action::PageDown);
15377        assert_eq!(app.column_selector_index, max);
15378    }
15379
15380    #[test]
15381    fn test_column_selector_ctrl_u_scrolling() {
15382        let mut app = test_app();
15383        app.current_service = Service::CloudFormationStacks;
15384        app.mode = Mode::ColumnSelector;
15385        app.column_selector_index = 25;
15386
15387        app.handle_action(Action::PageUp);
15388        assert_eq!(app.column_selector_index, 15);
15389
15390        app.handle_action(Action::PageUp);
15391        assert_eq!(app.column_selector_index, 5);
15392    }
15393
15394    #[test]
15395    fn test_prev_preferences_lambda() {
15396        let mut app = test_app();
15397        app.current_service = Service::LambdaFunctions;
15398        app.mode = Mode::ColumnSelector;
15399        let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
15400        app.column_selector_index = page_size_idx;
15401
15402        app.handle_action(Action::PrevPreferences);
15403        assert_eq!(app.column_selector_index, 0);
15404
15405        app.handle_action(Action::PrevPreferences);
15406        assert_eq!(app.column_selector_index, page_size_idx);
15407    }
15408
15409    #[test]
15410    fn test_prev_preferences_cloudformation() {
15411        let mut app = test_app();
15412        app.current_service = Service::CloudFormationStacks;
15413        app.mode = Mode::ColumnSelector;
15414        let page_size_idx = app.cfn_column_ids.len() + 2;
15415        app.column_selector_index = page_size_idx;
15416
15417        app.handle_action(Action::PrevPreferences);
15418        assert_eq!(app.column_selector_index, 0);
15419
15420        app.handle_action(Action::PrevPreferences);
15421        assert_eq!(app.column_selector_index, page_size_idx);
15422    }
15423
15424    #[test]
15425    fn test_prev_preferences_alarms() {
15426        let mut app = test_app();
15427        app.current_service = Service::CloudWatchAlarms;
15428        app.mode = Mode::ColumnSelector;
15429        app.column_selector_index = 28; // WrapLines
15430
15431        app.handle_action(Action::PrevPreferences);
15432        assert_eq!(app.column_selector_index, 22); // PageSize
15433
15434        app.handle_action(Action::PrevPreferences);
15435        assert_eq!(app.column_selector_index, 18); // ViewAs
15436
15437        app.handle_action(Action::PrevPreferences);
15438        assert_eq!(app.column_selector_index, 0); // Columns
15439
15440        app.handle_action(Action::PrevPreferences);
15441        assert_eq!(app.column_selector_index, 28); // Wrap to WrapLines
15442    }
15443
15444    #[test]
15445    fn test_ec2_page_size_in_preferences() {
15446        let mut app = test_app();
15447        app.current_service = Service::Ec2Instances;
15448        app.mode = Mode::ColumnSelector;
15449        app.ec2_state.table.page_size = PageSize::Fifty;
15450
15451        // Navigate to page size section
15452        let page_size_idx = app.ec2_column_ids.len() + 3; // First page size option (10)
15453        app.column_selector_index = page_size_idx;
15454        app.handle_action(Action::ToggleColumn);
15455
15456        assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
15457    }
15458
15459    #[test]
15460    fn test_ec2_next_preferences_with_page_size() {
15461        let mut app = test_app();
15462        app.current_service = Service::Ec2Instances;
15463        app.mode = Mode::ColumnSelector;
15464        app.column_selector_index = 0;
15465
15466        let page_size_idx = app.ec2_column_ids.len() + 2;
15467        app.handle_action(Action::NextPreferences);
15468        assert_eq!(app.column_selector_index, page_size_idx);
15469
15470        app.handle_action(Action::NextPreferences);
15471        assert_eq!(app.column_selector_index, 0);
15472    }
15473
15474    #[test]
15475    fn test_ec2_dropdown_next_item() {
15476        let mut app = test_app();
15477        app.current_service = Service::Ec2Instances;
15478        app.mode = Mode::FilterInput;
15479        app.ec2_state.input_focus = EC2_STATE_FILTER;
15480        app.ec2_state.state_filter = Ec2StateFilter::AllStates;
15481
15482        app.handle_action(Action::NextItem);
15483        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15484
15485        app.handle_action(Action::NextItem);
15486        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
15487    }
15488
15489    #[test]
15490    fn test_ec2_dropdown_prev_item() {
15491        let mut app = test_app();
15492        app.current_service = Service::Ec2Instances;
15493        app.mode = Mode::FilterInput;
15494        app.ec2_state.input_focus = EC2_STATE_FILTER;
15495        app.ec2_state.state_filter = Ec2StateFilter::Stopped;
15496
15497        app.handle_action(Action::PrevItem);
15498        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15499
15500        app.handle_action(Action::PrevItem);
15501        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15502    }
15503
15504    #[test]
15505    fn test_ec2_dropdown_cycles_with_arrows() {
15506        let mut app = test_app();
15507        app.current_service = Service::Ec2Instances;
15508        app.mode = Mode::FilterInput;
15509        app.ec2_state.input_focus = EC2_STATE_FILTER;
15510        app.ec2_state.state_filter = Ec2StateFilter::Stopping;
15511
15512        // Next wraps to AllStates
15513        app.handle_action(Action::NextItem);
15514        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15515
15516        // Prev wraps to Stopping
15517        app.handle_action(Action::PrevItem);
15518        assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
15519    }
15520
15521    #[test]
15522    fn test_collapse_row_ec2_instances() {
15523        let mut app = test_app();
15524        app.current_service = Service::Ec2Instances;
15525        app.ec2_state.table.expanded_item = Some(0);
15526
15527        app.handle_action(Action::CollapseRow);
15528        assert_eq!(app.ec2_state.table.expanded_item, None);
15529    }
15530
15531    #[test]
15532    fn test_collapse_row_ec2_tags() {
15533        let mut app = test_app();
15534        app.current_service = Service::Ec2Instances;
15535        app.ec2_state.current_instance = Some("i-123".to_string());
15536        app.ec2_state.detail_tab = Ec2DetailTab::Tags;
15537        app.ec2_state.tags.expanded_item = Some(1);
15538
15539        app.handle_action(Action::CollapseRow);
15540        assert_eq!(app.ec2_state.tags.expanded_item, None);
15541    }
15542
15543    #[test]
15544    fn test_collapse_row_cloudwatch_log_groups() {
15545        let mut app = test_app();
15546        app.current_service = Service::CloudWatchLogGroups;
15547        app.log_groups_state.log_groups.expanded_item = Some(2);
15548
15549        app.handle_action(Action::CollapseRow);
15550        assert_eq!(app.log_groups_state.log_groups.expanded_item, None);
15551    }
15552
15553    #[test]
15554    fn test_collapse_row_cloudwatch_alarms() {
15555        let mut app = test_app();
15556        app.current_service = Service::CloudWatchAlarms;
15557        app.alarms_state.table.expanded_item = Some(0);
15558
15559        app.handle_action(Action::CollapseRow);
15560        assert_eq!(app.alarms_state.table.expanded_item, None);
15561    }
15562
15563    #[test]
15564    fn test_collapse_row_lambda_functions() {
15565        let mut app = test_app();
15566        app.current_service = Service::LambdaFunctions;
15567        app.lambda_state.table.expanded_item = Some(1);
15568
15569        app.handle_action(Action::CollapseRow);
15570        assert_eq!(app.lambda_state.table.expanded_item, None);
15571    }
15572
15573    #[test]
15574    fn test_collapse_row_cfn_stacks() {
15575        let mut app = test_app();
15576        app.current_service = Service::CloudFormationStacks;
15577        app.cfn_state.table.expanded_item = Some(0);
15578
15579        app.handle_action(Action::CollapseRow);
15580        assert_eq!(app.cfn_state.table.expanded_item, None);
15581    }
15582
15583    #[test]
15584    fn test_collapse_row_cfn_resources() {
15585        let mut app = test_app();
15586        app.current_service = Service::CloudFormationStacks;
15587        app.cfn_state.current_stack = Some("test-stack".to_string());
15588        app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Resources;
15589        app.cfn_state.resources.expanded_item = Some(2);
15590
15591        app.handle_action(Action::CollapseRow);
15592        assert_eq!(app.cfn_state.resources.expanded_item, None);
15593    }
15594
15595    #[test]
15596    fn test_collapse_row_iam_users() {
15597        let mut app = test_app();
15598        app.current_service = Service::IamUsers;
15599        app.iam_state.users.expanded_item = Some(1);
15600
15601        app.handle_action(Action::CollapseRow);
15602        assert_eq!(app.iam_state.users.expanded_item, None);
15603    }
15604
15605    #[test]
15606    fn test_collapse_row_does_nothing_when_not_expanded() {
15607        let mut app = test_app();
15608        app.current_service = Service::Ec2Instances;
15609        app.ec2_state.table.expanded_item = None;
15610
15611        app.handle_action(Action::CollapseRow);
15612        assert_eq!(app.ec2_state.table.expanded_item, None);
15613    }
15614
15615    #[test]
15616    fn test_s3_collapse_expanded_folder_moves_to_parent() {
15617        let mut app = test_app();
15618        app.current_service = Service::S3Buckets;
15619        app.service_selected = true;
15620        app.mode = Mode::Normal;
15621
15622        // Add bucket with folder
15623        app.s3_state.buckets.items = vec![S3Bucket {
15624            name: "bucket1".to_string(),
15625            region: "us-east-1".to_string(),
15626            creation_date: "2024-01-01T00:00:00Z".to_string(),
15627        }];
15628
15629        // Expand bucket
15630        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
15631        app.s3_state.bucket_preview.insert(
15632            "bucket1".to_string(),
15633            vec![S3Object {
15634                key: "folder1/".to_string(),
15635                size: 0,
15636                last_modified: "2024-01-01T00:00:00Z".to_string(),
15637                is_prefix: true,
15638                storage_class: String::new(),
15639            }],
15640        );
15641
15642        // Expand folder
15643        app.s3_state
15644            .expanded_prefixes
15645            .insert("folder1/".to_string());
15646        app.s3_state.prefix_preview.insert(
15647            "folder1/".to_string(),
15648            vec![S3Object {
15649                key: "folder1/file.txt".to_string(),
15650                size: 0,
15651                last_modified: "2024-01-01T00:00:00Z".to_string(),
15652                is_prefix: false,
15653                storage_class: String::new(),
15654            }],
15655        );
15656
15657        // Select the expanded folder (row 1)
15658        app.s3_state.selected_row = 1;
15659
15660        // Press Left to collapse
15661        app.handle_action(Action::PrevPane);
15662
15663        // Folder should be collapsed
15664        assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
15665        // Selection should move to parent (bucket at row 0)
15666        assert_eq!(app.s3_state.selected_row, 0);
15667    }
15668
15669    #[test]
15670    fn test_s3_collapse_hierarchy_level_by_level() {
15671        let mut app = test_app();
15672        app.current_service = Service::S3Buckets;
15673        app.service_selected = true;
15674        app.mode = Mode::Normal;
15675
15676        // Add bucket with 3-level hierarchy
15677        app.s3_state.buckets.items = vec![S3Bucket {
15678            name: "bucket1".to_string(),
15679            region: "us-east-1".to_string(),
15680            creation_date: "2024-01-01T00:00:00Z".to_string(),
15681        }];
15682
15683        // Level 1: bucket -> level1/
15684        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
15685        app.s3_state.bucket_preview.insert(
15686            "bucket1".to_string(),
15687            vec![S3Object {
15688                key: "level1/".to_string(),
15689                size: 0,
15690                last_modified: "2024-01-01T00:00:00Z".to_string(),
15691                is_prefix: true,
15692                storage_class: String::new(),
15693            }],
15694        );
15695
15696        // Level 2: level1/ -> level2/
15697        app.s3_state.expanded_prefixes.insert("level1/".to_string());
15698        app.s3_state.prefix_preview.insert(
15699            "level1/".to_string(),
15700            vec![S3Object {
15701                key: "level1/level2/".to_string(),
15702                size: 0,
15703                last_modified: "2024-01-01T00:00:00Z".to_string(),
15704                is_prefix: true,
15705                storage_class: String::new(),
15706            }],
15707        );
15708
15709        // Level 3: level2/ -> file
15710        app.s3_state
15711            .expanded_prefixes
15712            .insert("level1/level2/".to_string());
15713        app.s3_state.prefix_preview.insert(
15714            "level1/level2/".to_string(),
15715            vec![S3Object {
15716                key: "level1/level2/file.txt".to_string(),
15717                size: 100,
15718                last_modified: "2024-01-01T00:00:00Z".to_string(),
15719                is_prefix: false,
15720                storage_class: String::new(),
15721            }],
15722        );
15723
15724        // Select deepest level (row 3: file)
15725        app.s3_state.selected_row = 3;
15726
15727        // First Left: move to parent (level2/ at row 2)
15728        app.handle_action(Action::PrevPane);
15729        assert_eq!(app.s3_state.selected_row, 2);
15730
15731        // Second Left: collapse level2/ and move to parent (level1/ at row 1)
15732        app.handle_action(Action::PrevPane);
15733        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
15734        assert_eq!(app.s3_state.selected_row, 1);
15735
15736        // Third Left: collapse level1/ and move to parent (bucket at row 0)
15737        app.handle_action(Action::PrevPane);
15738        assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
15739        assert_eq!(app.s3_state.selected_row, 0);
15740
15741        // Fourth Left: collapse bucket (stays at row 0)
15742        app.handle_action(Action::PrevPane);
15743        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
15744        assert_eq!(app.s3_state.selected_row, 0);
15745    }
15746
15747    #[test]
15748    fn test_ec2_instance_detail_tabs_no_preferences() {
15749        let mut app = test_app();
15750        app.current_service = Service::Ec2Instances;
15751        app.service_selected = true;
15752        app.ec2_state.table.expanded_item = Some(0);
15753        app.mode = Mode::Normal;
15754
15755        // Details tab should NOT allow preferences
15756        app.ec2_state.detail_tab = Ec2DetailTab::Details;
15757        app.handle_action(Action::OpenColumnSelector);
15758        assert_eq!(app.mode, Mode::Normal);
15759
15760        // StatusAndAlarms tab should NOT allow preferences
15761        app.ec2_state.detail_tab = Ec2DetailTab::StatusAndAlarms;
15762        app.handle_action(Action::OpenColumnSelector);
15763        assert_eq!(app.mode, Mode::Normal);
15764
15765        // Monitoring tab should NOT allow preferences
15766        app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
15767        app.handle_action(Action::OpenColumnSelector);
15768        assert_eq!(app.mode, Mode::Normal);
15769
15770        // Security tab should NOT allow preferences
15771        app.ec2_state.detail_tab = Ec2DetailTab::Security;
15772        app.handle_action(Action::OpenColumnSelector);
15773        assert_eq!(app.mode, Mode::Normal);
15774
15775        // Networking tab should NOT allow preferences
15776        app.ec2_state.detail_tab = Ec2DetailTab::Networking;
15777        app.handle_action(Action::OpenColumnSelector);
15778        assert_eq!(app.mode, Mode::Normal);
15779
15780        // Storage tab should NOT allow preferences
15781        app.ec2_state.detail_tab = Ec2DetailTab::Storage;
15782        app.handle_action(Action::OpenColumnSelector);
15783        assert_eq!(app.mode, Mode::Normal);
15784
15785        // Tags tab SHOULD allow preferences
15786        app.ec2_state.detail_tab = Ec2DetailTab::Tags;
15787        app.handle_action(Action::OpenColumnSelector);
15788        assert_eq!(app.mode, Mode::ColumnSelector);
15789    }
15790
15791    #[test]
15792    fn test_ec2_monitoring_ctrl_u_scrolls_up() {
15793        // Regression: Ctrl+D (PageDown) scrolls monitoring graphs down but
15794        // Ctrl+U (PageUp) did NOT scroll up — it called page_up_normal which
15795        // only pages the instance table, not the monitoring scroll.
15796        let mut app = test_app();
15797        app.current_service = Service::Ec2Instances;
15798        app.service_selected = true;
15799        app.mode = Mode::Normal;
15800        app.ec2_state.current_instance = Some("i-abc".to_string());
15801        app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
15802
15803        // Set scroll to non-zero
15804        app.ec2_state.set_monitoring_scroll(3);
15805        assert_eq!(app.ec2_state.monitoring_scroll(), 3);
15806
15807        // Ctrl+U (PageUp) must scroll monitoring up
15808        app.handle_action(Action::PageUp);
15809        assert_eq!(
15810            app.ec2_state.monitoring_scroll(),
15811            2,
15812            "Ctrl+U must scroll monitoring up by 1"
15813        );
15814
15815        // Ctrl+D (PageDown) scrolls down — verify symmetry
15816        app.handle_action(Action::PageDown);
15817        assert_eq!(
15818            app.ec2_state.monitoring_scroll(),
15819            3,
15820            "Ctrl+D must scroll monitoring down by 1"
15821        );
15822    }
15823
15824    #[test]
15825    fn test_ec2_instance_always_shows_when_state_filter_is_all_states() {
15826        // Regression: an instance must appear in the table when state_filter is AllStates,
15827        // regardless of instance state or whether name/private_ip are empty.
15828        use crate::app::Ec2Instance;
15829        use crate::ui::ec2::{filtered_ec2_instances, StateFilter};
15830        let mut app = test_app();
15831        app.current_service = Service::Ec2Instances;
15832        app.service_selected = true;
15833        app.ec2_state.table.filter.clear();
15834        app.ec2_state.state_filter = StateFilter::AllStates;
15835
15836        // Instance with only id set (no name, no private IP — like a real unnamed instance)
15837        app.ec2_state.table.items = vec![Ec2Instance {
15838            instance_id: "i-05f181d4aaabadf5a".to_string(),
15839            state: "running".to_string(),
15840            ..Default::default()
15841        }];
15842
15843        let filtered = filtered_ec2_instances(&app);
15844        assert_eq!(
15845            filtered.len(),
15846            1,
15847            "Instance must appear with AllStates filter and no text filter"
15848        );
15849        assert_eq!(filtered[0].instance_id, "i-05f181d4aaabadf5a");
15850    }
15851
15852    #[test]
15853    fn test_ec2_instance_private_ip_populated_from_load() {
15854        // Regression: load_ec2_instances was setting private_ip_address = String::new()
15855        // which means the render shows no private IP and filter by private IP never works.
15856        // Verify the field is mapped from the loaded instance data.
15857        use crate::app::Ec2Instance;
15858        let instance = Ec2Instance {
15859            instance_id: "i-abc".to_string(),
15860            name: "my-server".to_string(),
15861            state: "running".to_string(),
15862            private_dns_name: "ip-10-0-1-5.ec2.internal".to_string(),
15863            private_ip_address: "10.0.1.5".to_string(),
15864            security_group_ids: "sg-abc123".to_string(),
15865            ..Default::default()
15866        };
15867        // These fields must be set (not empty) for display and filter to work
15868        assert!(
15869            !instance.private_ip_address.is_empty(),
15870            "private_ip_address must be populated"
15871        );
15872        assert!(
15873            !instance.private_dns_name.is_empty(),
15874            "private_dns_name must be populated"
15875        );
15876    }
15877
15878    #[test]
15879    fn test_log_streams_filter_only_updates_when_focused() {
15880        let mut app = test_app();
15881        app.current_service = Service::CloudWatchLogGroups;
15882        app.service_selected = true;
15883        app.view_mode = ViewMode::Detail;
15884        app.mode = Mode::FilterInput;
15885        app.log_groups_state.stream_filter = "test".to_string();
15886
15887        // When filter is focused, typing should update filter
15888        app.log_groups_state.input_focus = InputFocus::Filter;
15889        app.handle_action(Action::FilterInput('x'));
15890        assert_eq!(app.log_groups_state.stream_filter, "testx");
15891
15892        // When pagination is focused, typing should NOT update filter
15893        app.log_groups_state.input_focus = InputFocus::Pagination;
15894        app.handle_action(Action::FilterInput('y'));
15895        assert_eq!(app.log_groups_state.stream_filter, "testx"); // unchanged
15896    }
15897
15898    #[test]
15899    fn test_log_streams_backspace_only_updates_when_focused() {
15900        let mut app = test_app();
15901        app.current_service = Service::CloudWatchLogGroups;
15902        app.service_selected = true;
15903        app.view_mode = ViewMode::Detail;
15904        app.mode = Mode::FilterInput;
15905        app.log_groups_state.stream_filter = "test".to_string();
15906
15907        // When filter is focused, backspace should update filter
15908        app.log_groups_state.input_focus = InputFocus::Filter;
15909        app.handle_action(Action::FilterBackspace);
15910        assert_eq!(app.log_groups_state.stream_filter, "tes");
15911
15912        // When pagination is focused, backspace should NOT update filter
15913        app.log_groups_state.input_focus = InputFocus::Pagination;
15914        app.handle_action(Action::FilterBackspace);
15915        assert_eq!(app.log_groups_state.stream_filter, "tes"); // unchanged
15916    }
15917
15918    #[test]
15919    fn test_log_groups_filter_only_updates_when_focused() {
15920        let mut app = test_app();
15921        app.current_service = Service::CloudWatchLogGroups;
15922        app.service_selected = true;
15923        app.view_mode = ViewMode::List;
15924        app.mode = Mode::FilterInput;
15925        app.log_groups_state.log_groups.filter = "test".to_string();
15926
15927        // When filter is focused, typing should update filter
15928        app.log_groups_state.input_focus = InputFocus::Filter;
15929        app.handle_action(Action::FilterInput('x'));
15930        assert_eq!(app.log_groups_state.log_groups.filter, "testx");
15931
15932        // When pagination is focused, typing should NOT update filter
15933        app.log_groups_state.input_focus = InputFocus::Pagination;
15934        app.handle_action(Action::FilterInput('y'));
15935        assert_eq!(app.log_groups_state.log_groups.filter, "testx"); // unchanged
15936    }
15937
15938    #[test]
15939    fn test_s3_bucket_collapse_nested_prefix_jumps_to_parent() {
15940        use S3Bucket;
15941        use S3Object;
15942
15943        let mut app = test_app();
15944        app.current_service = Service::S3Buckets;
15945        app.service_selected = true;
15946
15947        // Create bucket with nested prefixes
15948        app.s3_state.buckets.items = vec![S3Bucket {
15949            name: "test-bucket".to_string(),
15950            region: "us-east-1".to_string(),
15951            creation_date: String::new(),
15952        }];
15953
15954        // Expand bucket with folder1/
15955        app.s3_state
15956            .expanded_prefixes
15957            .insert("test-bucket".to_string());
15958        app.s3_state.bucket_preview.insert(
15959            "test-bucket".to_string(),
15960            vec![S3Object {
15961                key: "folder1/".to_string(),
15962                is_prefix: true,
15963                size: 0,
15964                last_modified: String::new(),
15965                storage_class: String::new(),
15966            }],
15967        );
15968
15969        // Expand folder1/ with folder2/
15970        app.s3_state
15971            .expanded_prefixes
15972            .insert("folder1/".to_string());
15973        app.s3_state.prefix_preview.insert(
15974            "folder1/".to_string(),
15975            vec![S3Object {
15976                key: "folder1/folder2/".to_string(),
15977                is_prefix: true,
15978                size: 0,
15979                last_modified: String::new(),
15980                storage_class: String::new(),
15981            }],
15982        );
15983
15984        // Select folder2/ (row 0: bucket, row 1: folder1, row 2: folder2)
15985        app.s3_state.selected_row = 2;
15986
15987        // Press left arrow - should collapse folder2 and jump to folder1
15988        app.handle_action(Action::CollapseRow);
15989
15990        // folder2 should be collapsed
15991        assert!(!app.s3_state.expanded_prefixes.contains("folder1/folder2/"));
15992        // Selection should move to folder1 (row 1)
15993        assert_eq!(app.s3_state.selected_row, 1);
15994    }
15995
15996    #[test]
15997    fn test_s3_bucket_collapse_expanded_folder_moves_to_parent() {
15998        use S3Bucket;
15999        use S3Object;
16000
16001        let mut app = test_app();
16002        app.current_service = Service::S3Buckets;
16003        app.service_selected = true;
16004
16005        // Create bucket with folder
16006        app.s3_state.buckets.items = vec![S3Bucket {
16007            name: "test-bucket".to_string(),
16008            region: "us-east-1".to_string(),
16009            creation_date: String::new(),
16010        }];
16011
16012        // Expand bucket with folder1/
16013        app.s3_state
16014            .expanded_prefixes
16015            .insert("test-bucket".to_string());
16016        app.s3_state.bucket_preview.insert(
16017            "test-bucket".to_string(),
16018            vec![S3Object {
16019                key: "folder1/".to_string(),
16020                is_prefix: true,
16021                size: 0,
16022                last_modified: String::new(),
16023                storage_class: String::new(),
16024            }],
16025        );
16026
16027        // Expand folder1/
16028        app.s3_state
16029            .expanded_prefixes
16030            .insert("folder1/".to_string());
16031        app.s3_state.prefix_preview.insert(
16032            "folder1/".to_string(),
16033            vec![S3Object {
16034                key: "folder1/file.txt".to_string(),
16035                is_prefix: false,
16036                size: 100,
16037                last_modified: String::new(),
16038                storage_class: String::new(),
16039            }],
16040        );
16041
16042        // Select folder1/ (row 0: bucket, row 1: folder1)
16043        app.s3_state.selected_row = 1;
16044
16045        // Press left arrow - should collapse folder1 and jump to bucket
16046        app.handle_action(Action::CollapseRow);
16047
16048        // folder1 should be collapsed
16049        assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
16050        // Selection should move to bucket (row 0)
16051        assert_eq!(app.s3_state.selected_row, 0);
16052    }
16053
16054    #[test]
16055    fn test_log_streams_pagination_limits_table_content() {
16056        let mut app = test_app();
16057        app.current_service = Service::CloudWatchLogGroups;
16058        app.service_selected = true;
16059        app.view_mode = ViewMode::Detail;
16060
16061        // Create 50 log streams
16062        app.log_groups_state.log_streams = (0..50)
16063            .map(|i| rusticity_core::LogStream {
16064                name: format!("stream-{}", i),
16065                creation_time: None,
16066                last_event_time: None,
16067            })
16068            .collect();
16069
16070        // Set page size to 10
16071        app.log_groups_state.stream_page_size = 10;
16072        app.log_groups_state.stream_current_page = 0;
16073
16074        // First page should show streams 0-9
16075        // (This is tested by rendering, but we can verify pagination logic)
16076        assert_eq!(app.log_groups_state.stream_page_size, 10);
16077        assert_eq!(app.log_groups_state.stream_current_page, 0);
16078
16079        // Navigate to page 2
16080        app.log_groups_state.stream_current_page = 1;
16081        assert_eq!(app.log_groups_state.stream_current_page, 1);
16082    }
16083
16084    #[test]
16085    fn test_log_streams_page_size_change_resets_page() {
16086        let mut app = test_app();
16087        app.current_service = Service::CloudWatchLogGroups;
16088        app.service_selected = true;
16089        app.view_mode = ViewMode::Detail;
16090        app.mode = Mode::ColumnSelector;
16091
16092        app.log_groups_state.stream_page_size = 10;
16093        app.log_groups_state.stream_current_page = 3;
16094
16095        // Change page size - should reset to page 0
16096        app.column_selector_index = app.cw_log_stream_column_ids.len() + 4; // 25 items
16097        app.handle_action(Action::ToggleColumn);
16098
16099        assert_eq!(app.log_groups_state.stream_page_size, 25);
16100        assert_eq!(app.log_groups_state.stream_current_page, 0);
16101    }
16102
16103    #[test]
16104    fn test_s3_objects_expanded_rows_stay_visible() {
16105        use S3Object;
16106
16107        let mut app = test_app();
16108        app.current_service = Service::S3Buckets;
16109        app.service_selected = true;
16110        app.mode = Mode::Normal;
16111        app.s3_state.current_bucket = Some("test-bucket".to_string());
16112
16113        // Create a folder with many nested items
16114        app.s3_state.objects = vec![S3Object {
16115            key: "folder1/".to_string(),
16116            is_prefix: true,
16117            size: 0,
16118            last_modified: String::new(),
16119            storage_class: String::new(),
16120        }];
16121
16122        // Expand folder1 with 20 files
16123        app.s3_state
16124            .expanded_prefixes
16125            .insert("folder1/".to_string());
16126        app.s3_state.prefix_preview.insert(
16127            "folder1/".to_string(),
16128            (0..20)
16129                .map(|i| S3Object {
16130                    key: format!("folder1/file{}.txt", i),
16131                    is_prefix: false,
16132                    size: 100,
16133                    last_modified: String::new(),
16134                    storage_class: String::new(),
16135                })
16136                .collect(),
16137        );
16138
16139        // Set viewport to show 10 rows
16140        app.s3_state.object_visible_rows.set(10);
16141        app.s3_state.object_scroll_offset = 0;
16142        app.s3_state.selected_object = 0; // folder1
16143
16144        // Navigate down through all items
16145        for i in 1..=20 {
16146            app.handle_action(Action::NextItem);
16147            assert_eq!(app.s3_state.selected_object, i);
16148
16149            // Check that selection is within visible range
16150            let visible_start = app.s3_state.object_scroll_offset;
16151            let visible_end = visible_start + app.s3_state.object_visible_rows.get();
16152            assert!(
16153                app.s3_state.selected_object >= visible_start
16154                    && app.s3_state.selected_object < visible_end,
16155                "Selection {} should be visible in range [{}, {})",
16156                app.s3_state.selected_object,
16157                visible_start,
16158                visible_end
16159            );
16160        }
16161    }
16162
16163    #[test]
16164    fn test_s3_bucket_error_rows_counted_in_total() {
16165        use S3Bucket;
16166
16167        let mut app = test_app();
16168        app.current_service = Service::S3Buckets;
16169        app.service_selected = true;
16170
16171        // Create buckets
16172        app.s3_state.buckets.items = vec![
16173            S3Bucket {
16174                name: "bucket1".to_string(),
16175                region: "us-east-1".to_string(),
16176                creation_date: String::new(),
16177            },
16178            S3Bucket {
16179                name: "bucket2".to_string(),
16180                region: "us-east-1".to_string(),
16181                creation_date: String::new(),
16182            },
16183        ];
16184
16185        // Expand bucket1 with error (long error message that will wrap)
16186        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16187        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();
16188        app.s3_state
16189            .bucket_errors
16190            .insert("bucket1".to_string(), long_error.clone());
16191
16192        // Calculate total rows
16193        let total = app.calculate_total_bucket_rows();
16194
16195        // Should be: 2 buckets + error rows (long_error.len() / 120 rounded up)
16196        let error_rows = long_error.len().div_ceil(120);
16197        assert_eq!(total, 2 + error_rows);
16198    }
16199
16200    #[test]
16201    fn test_s3_bucket_with_error_can_be_collapsed() {
16202        use S3Bucket;
16203
16204        let mut app = test_app();
16205        app.current_service = Service::S3Buckets;
16206        app.service_selected = true;
16207        app.mode = Mode::Normal;
16208
16209        // Create bucket
16210        app.s3_state.buckets.items = vec![S3Bucket {
16211            name: "bucket1".to_string(),
16212            region: "us-east-1".to_string(),
16213            creation_date: String::new(),
16214        }];
16215
16216        // Expand bucket with error
16217        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16218        let error = "service error: PermanentRedirect".to_string();
16219        app.s3_state
16220            .bucket_errors
16221            .insert("bucket1".to_string(), error);
16222
16223        // Select the bucket row (row 0) - error rows are not selectable
16224        app.s3_state.selected_row = 0;
16225
16226        // Press left arrow to collapse
16227        app.handle_action(Action::CollapseRow);
16228
16229        // Bucket should be collapsed
16230        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
16231        // Selection should stay on bucket
16232        assert_eq!(app.s3_state.selected_row, 0);
16233    }
16234
16235    #[test]
16236    fn test_s3_bucket_collapse_on_bucket_row() {
16237        use S3Bucket;
16238
16239        let mut app = test_app();
16240        app.current_service = Service::S3Buckets;
16241        app.service_selected = true;
16242        app.mode = Mode::Normal;
16243
16244        // Create bucket
16245        app.s3_state.buckets.items = vec![S3Bucket {
16246            name: "bucket1".to_string(),
16247            region: "us-east-1".to_string(),
16248            creation_date: String::new(),
16249        }];
16250
16251        // Expand bucket with error
16252        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16253        let error = "service error: PermanentRedirect".to_string();
16254        app.s3_state
16255            .bucket_errors
16256            .insert("bucket1".to_string(), error);
16257
16258        // Select the bucket row itself (row 0)
16259        app.s3_state.selected_row = 0;
16260
16261        // Press left arrow to collapse
16262        app.handle_action(Action::CollapseRow);
16263
16264        // Bucket should be collapsed
16265        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
16266        // Selection should stay on bucket
16267        assert_eq!(app.s3_state.selected_row, 0);
16268    }
16269
16270    #[test]
16271    fn test_s3_bucket_collapse_adjusts_scroll_offset() {
16272        use S3Bucket;
16273
16274        let mut app = test_app();
16275        app.current_service = Service::S3Buckets;
16276        app.service_selected = true;
16277        app.mode = Mode::Normal;
16278
16279        // Create multiple buckets
16280        app.s3_state.buckets.items = (0..20)
16281            .map(|i| S3Bucket {
16282                name: format!("bucket{}", i),
16283                region: "us-east-1".to_string(),
16284                creation_date: String::new(),
16285            })
16286            .collect();
16287
16288        // Expand bucket 10 with a long error
16289        app.s3_state
16290            .expanded_prefixes
16291            .insert("bucket10".to_string());
16292        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();
16293        app.s3_state
16294            .bucket_errors
16295            .insert("bucket10".to_string(), long_error.clone());
16296
16297        // Set viewport to 10 rows and scroll so bucket10 is at top
16298        app.s3_state.bucket_visible_rows.set(10);
16299        app.s3_state.bucket_scroll_offset = 10; // bucket10 is at row 10
16300
16301        // Select bucket10 (row 10) - error rows are not selectable
16302        app.s3_state.selected_row = 10;
16303
16304        // Press left arrow to collapse
16305        app.handle_action(Action::CollapseRow);
16306
16307        // Bucket should be collapsed
16308        assert!(!app.s3_state.expanded_prefixes.contains("bucket10"));
16309        // Selection should stay on bucket10 (row 10)
16310        assert_eq!(app.s3_state.selected_row, 10);
16311        // Scroll offset should be adjusted to show bucket10
16312        assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
16313        assert!(
16314            app.s3_state.selected_row
16315                < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
16316        );
16317    }
16318
16319    #[test]
16320    fn test_s3_collapse_second_to_last_bucket_with_last_having_error() {
16321        use S3Bucket;
16322
16323        let mut app = test_app();
16324        app.current_service = Service::S3Buckets;
16325        app.service_selected = true;
16326        app.mode = Mode::Normal;
16327
16328        // Create 3 buckets
16329        app.s3_state.buckets.items = vec![
16330            S3Bucket {
16331                name: "bucket1".to_string(),
16332                region: "us-east-1".to_string(),
16333                creation_date: String::new(),
16334            },
16335            S3Bucket {
16336                name: "bucket2".to_string(),
16337                region: "us-east-1".to_string(),
16338                creation_date: String::new(),
16339            },
16340            S3Bucket {
16341                name: "bucket3".to_string(),
16342                region: "us-east-1".to_string(),
16343                creation_date: String::new(),
16344            },
16345        ];
16346
16347        // Expand bucket2 (second to last) with preview
16348        app.s3_state.expanded_prefixes.insert("bucket2".to_string());
16349        app.s3_state.bucket_preview.insert(
16350            "bucket2".to_string(),
16351            vec![
16352                S3Object {
16353                    key: "folder1/".to_string(),
16354                    is_prefix: true,
16355                    size: 0,
16356                    last_modified: String::new(),
16357                    storage_class: String::new(),
16358                },
16359                S3Object {
16360                    key: "file1.txt".to_string(),
16361                    is_prefix: false,
16362                    size: 100,
16363                    last_modified: String::new(),
16364                    storage_class: String::new(),
16365                },
16366            ],
16367        );
16368
16369        // Expand bucket3 (last) with error
16370        app.s3_state.expanded_prefixes.insert("bucket3".to_string());
16371        let error = "service error: PermanentRedirect".to_string();
16372        app.s3_state
16373            .bucket_errors
16374            .insert("bucket3".to_string(), error);
16375
16376        // Set viewport
16377        app.s3_state.bucket_visible_rows.set(10);
16378        app.s3_state.bucket_scroll_offset = 0;
16379
16380        // Select last item in bucket2 (row 3: bucket1, bucket2, folder1, file1)
16381        app.s3_state.selected_row = 3;
16382
16383        // Collapse - should move to parent (bucket2)
16384        app.handle_action(Action::CollapseRow);
16385
16386        // bucket2 should still be expanded (we only moved to parent, didn't collapse)
16387        assert!(app.s3_state.expanded_prefixes.contains("bucket2"));
16388        // Selection should move to bucket2
16389        assert_eq!(app.s3_state.selected_row, 1);
16390        // Selection should be visible
16391        assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
16392        assert!(
16393            app.s3_state.selected_row
16394                < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
16395        );
16396    }
16397
16398    #[test]
16399    fn test_s3_collapse_bucket_with_error() {
16400        use S3Bucket;
16401
16402        let mut app = test_app();
16403        app.current_service = Service::S3Buckets;
16404        app.service_selected = true;
16405        app.mode = Mode::Normal;
16406
16407        app.s3_state.buckets.items = vec![
16408            S3Bucket {
16409                name: "bucket1".to_string(),
16410                region: "us-east-1".to_string(),
16411                creation_date: String::new(),
16412            },
16413            S3Bucket {
16414                name: "bucket2".to_string(),
16415                region: "us-east-1".to_string(),
16416                creation_date: String::new(),
16417            },
16418        ];
16419
16420        let error = "service error: unhandled error (PermanentRedirect)".to_string();
16421        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16422        app.s3_state
16423            .bucket_errors
16424            .insert("bucket1".to_string(), error);
16425
16426        app.s3_state.bucket_visible_rows.set(10);
16427        app.s3_state.bucket_scroll_offset = 0;
16428
16429        // Select bucket1 (row 0) - error rows are not selectable
16430        app.s3_state.selected_row = 0;
16431
16432        // Collapse
16433        app.handle_action(Action::CollapseRow);
16434
16435        // bucket1 should be collapsed
16436        assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
16437        assert_eq!(app.s3_state.selected_row, 0);
16438    }
16439
16440    #[test]
16441    fn test_s3_collapse_row_with_multiple_error_buckets() {
16442        use S3Bucket;
16443
16444        let mut app = test_app();
16445        app.current_service = Service::S3Buckets;
16446        app.service_selected = true;
16447        app.mode = Mode::Normal;
16448
16449        app.s3_state.buckets.items = vec![
16450            S3Bucket {
16451                name: "bucket1".to_string(),
16452                region: "us-east-1".to_string(),
16453                creation_date: String::new(),
16454            },
16455            S3Bucket {
16456                name: "bucket2".to_string(),
16457                region: "us-east-1".to_string(),
16458                creation_date: String::new(),
16459            },
16460            S3Bucket {
16461                name: "bucket3".to_string(),
16462                region: "us-east-1".to_string(),
16463                creation_date: String::new(),
16464            },
16465        ];
16466
16467        let error = "service error: unhandled error (PermanentRedirect)".to_string();
16468
16469        // Expand bucket1 with error
16470        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16471        app.s3_state
16472            .bucket_errors
16473            .insert("bucket1".to_string(), error.clone());
16474
16475        // Expand bucket3 with error
16476        app.s3_state.expanded_prefixes.insert("bucket3".to_string());
16477        app.s3_state
16478            .bucket_errors
16479            .insert("bucket3".to_string(), error.clone());
16480
16481        app.s3_state.bucket_visible_rows.set(30);
16482        app.s3_state.bucket_scroll_offset = 0;
16483
16484        // Row 0: bucket1 (expanded with error - error rows not selectable)
16485        // Row 1: bucket2
16486        // Row 2: bucket3 (expanded with error - error rows not selectable)
16487        // Select bucket3
16488        app.s3_state.selected_row = 2;
16489
16490        app.handle_action(Action::CollapseRow);
16491
16492        // bucket3 should be collapsed, NOT bucket1
16493        assert!(
16494            !app.s3_state.expanded_prefixes.contains("bucket3"),
16495            "bucket3 should be collapsed"
16496        );
16497        assert!(
16498            app.s3_state.expanded_prefixes.contains("bucket1"),
16499            "bucket1 should still be expanded"
16500        );
16501        assert_eq!(app.s3_state.selected_row, 2);
16502    }
16503
16504    #[test]
16505    fn test_s3_collapse_row_nested_only_collapses_one_level() {
16506        use S3Bucket;
16507
16508        let mut app = test_app();
16509        app.current_service = Service::S3Buckets;
16510        app.service_selected = true;
16511        app.mode = Mode::Normal;
16512
16513        app.s3_state.buckets.items = vec![S3Bucket {
16514            name: "bucket1".to_string(),
16515            region: "us-east-1".to_string(),
16516            creation_date: String::new(),
16517        }];
16518
16519        // Level 1: bucket -> level1/
16520        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16521        app.s3_state.bucket_preview.insert(
16522            "bucket1".to_string(),
16523            vec![S3Object {
16524                key: "level1/".to_string(),
16525                size: 0,
16526                last_modified: "2024-01-01T00:00:00Z".to_string(),
16527                is_prefix: true,
16528                storage_class: String::new(),
16529            }],
16530        );
16531
16532        // Level 2: level1/ -> level2/
16533        app.s3_state.expanded_prefixes.insert("level1/".to_string());
16534        app.s3_state.prefix_preview.insert(
16535            "level1/".to_string(),
16536            vec![S3Object {
16537                key: "level1/level2/".to_string(),
16538                size: 0,
16539                last_modified: "2024-01-01T00:00:00Z".to_string(),
16540                is_prefix: true,
16541                storage_class: String::new(),
16542            }],
16543        );
16544
16545        // Level 3: level2/ -> file
16546        app.s3_state
16547            .expanded_prefixes
16548            .insert("level1/level2/".to_string());
16549        app.s3_state.prefix_preview.insert(
16550            "level1/level2/".to_string(),
16551            vec![S3Object {
16552                key: "level1/level2/file.txt".to_string(),
16553                size: 100,
16554                last_modified: "2024-01-01T00:00:00Z".to_string(),
16555                is_prefix: false,
16556                storage_class: String::new(),
16557            }],
16558        );
16559
16560        app.s3_state.bucket_visible_rows.set(10);
16561
16562        // Select level2/ (row 2: bucket, level1, level2)
16563        app.s3_state.selected_row = 2;
16564
16565        // Collapse - should only collapse level2/, not the entire bucket
16566        app.handle_action(Action::CollapseRow);
16567
16568        // level2/ should be collapsed
16569        assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
16570        // level1/ should still be expanded
16571        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
16572        // bucket should still be expanded
16573        assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
16574        // Selection should move to parent (level1/ at row 1)
16575        assert_eq!(app.s3_state.selected_row, 1);
16576    }
16577
16578    #[test]
16579    fn test_s3_collapse_row_deeply_nested_file() {
16580        use S3Bucket;
16581
16582        let mut app = test_app();
16583        app.current_service = Service::S3Buckets;
16584        app.service_selected = true;
16585        app.mode = Mode::Normal;
16586
16587        app.s3_state.buckets.items = vec![S3Bucket {
16588            name: "bucket1".to_string(),
16589            region: "us-east-1".to_string(),
16590            creation_date: String::new(),
16591        }];
16592
16593        // Level 1: bucket -> level1/
16594        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16595        app.s3_state.bucket_preview.insert(
16596            "bucket1".to_string(),
16597            vec![S3Object {
16598                key: "level1/".to_string(),
16599                size: 0,
16600                last_modified: "2024-01-01T00:00:00Z".to_string(),
16601                is_prefix: true,
16602                storage_class: String::new(),
16603            }],
16604        );
16605
16606        // Level 2: level1/ -> level2/
16607        app.s3_state.expanded_prefixes.insert("level1/".to_string());
16608        app.s3_state.prefix_preview.insert(
16609            "level1/".to_string(),
16610            vec![S3Object {
16611                key: "level1/level2/".to_string(),
16612                size: 0,
16613                last_modified: "2024-01-01T00:00:00Z".to_string(),
16614                is_prefix: true,
16615                storage_class: String::new(),
16616            }],
16617        );
16618
16619        // Level 3: level2/ -> file
16620        app.s3_state
16621            .expanded_prefixes
16622            .insert("level1/level2/".to_string());
16623        app.s3_state.prefix_preview.insert(
16624            "level1/level2/".to_string(),
16625            vec![S3Object {
16626                key: "level1/level2/file.txt".to_string(),
16627                size: 100,
16628                last_modified: "2024-01-01T00:00:00Z".to_string(),
16629                is_prefix: false,
16630                storage_class: String::new(),
16631            }],
16632        );
16633
16634        app.s3_state.bucket_visible_rows.set(10);
16635
16636        // Select file (row 3: bucket, level1, level2, file)
16637        app.s3_state.selected_row = 3;
16638
16639        // Collapse - should move to parent (level2/)
16640        app.handle_action(Action::CollapseRow);
16641
16642        // All levels should still be expanded
16643        assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
16644        assert!(app.s3_state.expanded_prefixes.contains("level1/"));
16645        assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
16646        // Selection should move to parent (level2/ at row 2)
16647        assert_eq!(app.s3_state.selected_row, 2);
16648    }
16649
16650    #[test]
16651    fn test_s3_bucket_pagination_adjusts_scroll() {
16652        use S3Bucket;
16653
16654        let mut app = test_app();
16655        app.current_service = Service::S3Buckets;
16656        app.service_selected = true;
16657        app.mode = Mode::Normal;
16658
16659        // Create 150 buckets (3 pages with page size 50)
16660        app.s3_state.buckets.items = (0..150)
16661            .map(|i| S3Bucket {
16662                name: format!("bucket{:03}", i),
16663                region: "us-east-1".to_string(),
16664                creation_date: String::new(),
16665            })
16666            .collect();
16667
16668        app.s3_state.bucket_visible_rows.set(20);
16669        app.s3_state.selected_row = 0;
16670        app.s3_state.bucket_scroll_offset = 0;
16671
16672        // Go to page 2 (should select row 50 and scroll to show it)
16673        app.go_to_page(2);
16674
16675        assert_eq!(app.s3_state.selected_row, 50);
16676        // Scroll offset should be adjusted to show page 2
16677        assert_eq!(app.s3_state.bucket_scroll_offset, 50);
16678
16679        // Go to page 3 (should select row 100 and scroll to show it)
16680        app.go_to_page(3);
16681
16682        assert_eq!(app.s3_state.selected_row, 100);
16683        assert_eq!(app.s3_state.bucket_scroll_offset, 100);
16684
16685        // Go to page 1 (should select row 0 and scroll to top)
16686        app.go_to_page(1);
16687
16688        assert_eq!(app.s3_state.selected_row, 0);
16689        assert_eq!(app.s3_state.bucket_scroll_offset, 0);
16690    }
16691
16692    #[test]
16693    fn test_s3_bucket_pagination_uses_page_size() {
16694        use S3Bucket;
16695
16696        let mut app = test_app();
16697        app.current_service = Service::S3Buckets;
16698        app.service_selected = true;
16699        app.mode = Mode::Normal;
16700
16701        // Create 100 buckets
16702        app.s3_state.buckets.items = (0..100)
16703            .map(|i| S3Bucket {
16704                name: format!("bucket{:03}", i),
16705                region: "us-east-1".to_string(),
16706                creation_date: String::new(),
16707            })
16708            .collect();
16709
16710        app.s3_state.bucket_visible_rows.set(20);
16711        app.s3_state.selected_row = 0;
16712
16713        // Default page size is 50
16714        assert_eq!(app.s3_state.buckets.page_size.value(), 50);
16715
16716        // Go to page 2 with default page size (50)
16717        app.go_to_page(2);
16718        assert_eq!(app.s3_state.selected_row, 50);
16719        assert_eq!(app.s3_state.bucket_scroll_offset, 50);
16720
16721        // Change page size to 25
16722        app.s3_state.buckets.page_size = crate::common::PageSize::TwentyFive;
16723        assert_eq!(app.s3_state.buckets.page_size.value(), 25);
16724
16725        // Go to page 2 with new page size (25)
16726        app.go_to_page(2);
16727        assert_eq!(app.s3_state.selected_row, 25);
16728        assert_eq!(app.s3_state.bucket_scroll_offset, 25);
16729    }
16730
16731    #[test]
16732    fn test_s3_bucket_page_size_limits_visible_rows() {
16733        use S3Bucket;
16734
16735        let mut app = test_app();
16736        app.current_service = Service::S3Buckets;
16737        app.service_selected = true;
16738        app.mode = Mode::Normal;
16739
16740        // Create 100 buckets
16741        app.s3_state.buckets.items = (0..100)
16742            .map(|i| S3Bucket {
16743                name: format!("bucket{:03}", i),
16744                region: "us-east-1".to_string(),
16745                creation_date: String::new(),
16746            })
16747            .collect();
16748
16749        // Set page size to 10
16750        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16751        assert_eq!(app.s3_state.buckets.page_size.value(), 10);
16752
16753        // Calculate total rows - should only count buckets on current page
16754        let total_rows = app.calculate_total_bucket_rows();
16755        // With 100 buckets and page size 10, we should see 10 buckets per page
16756        // But calculate_total_bucket_rows returns ALL rows, not just current page
16757        // This is the issue - we need to paginate the display
16758        assert!(total_rows >= 10, "Should have at least 10 rows");
16759    }
16760
16761    #[test]
16762    fn test_s3_bucket_tab_cycling_in_filter() {
16763        use crate::common::InputFocus;
16764
16765        let mut app = test_app();
16766        app.current_service = Service::S3Buckets;
16767        app.mode = Mode::FilterInput;
16768
16769        // Start at Filter
16770        assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
16771
16772        // Tab to Pagination
16773        app.handle_action(Action::NextFilterFocus);
16774        assert_eq!(app.s3_state.input_focus, InputFocus::Pagination);
16775
16776        // Tab back to Filter
16777        app.handle_action(Action::NextFilterFocus);
16778        assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
16779    }
16780
16781    #[test]
16782    fn test_s3_bucket_pagination_navigation_with_arrows() {
16783        use S3Bucket;
16784
16785        let mut app = test_app();
16786        app.current_service = Service::S3Buckets;
16787        app.mode = Mode::FilterInput;
16788
16789        // Create 100 buckets
16790        app.s3_state.buckets.items = (0..100)
16791            .map(|i| S3Bucket {
16792                name: format!("bucket{:03}", i),
16793                region: "us-east-1".to_string(),
16794                creation_date: String::new(),
16795            })
16796            .collect();
16797
16798        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16799        app.s3_state.selected_row = 0;
16800
16801        // Focus pagination
16802        app.s3_state.input_focus = crate::common::InputFocus::Pagination;
16803
16804        // Right arrow should go to next page (row 10)
16805        app.handle_action(Action::NextItem);
16806        assert_eq!(app.s3_state.selected_row, 10);
16807
16808        // Right arrow again (row 20)
16809        app.handle_action(Action::NextItem);
16810        assert_eq!(app.s3_state.selected_row, 20);
16811
16812        // Left arrow should go back (row 10)
16813        app.handle_action(Action::PrevItem);
16814        assert_eq!(app.s3_state.selected_row, 10);
16815    }
16816
16817    #[test]
16818    fn test_s3_bucket_go_to_page_shows_correct_buckets() {
16819        use S3Bucket;
16820
16821        let mut app = test_app();
16822        app.current_service = Service::S3Buckets;
16823        app.service_selected = true;
16824        app.mode = Mode::Normal;
16825
16826        // Create 100 buckets
16827        app.s3_state.buckets.items = (0..100)
16828            .map(|i| S3Bucket {
16829                name: format!("bucket{:03}", i),
16830                region: "us-east-1".to_string(),
16831                creation_date: String::new(),
16832            })
16833            .collect();
16834
16835        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16836
16837        // Go to page 2 (should show buckets 10-19)
16838        app.go_to_page(2);
16839        assert_eq!(app.s3_state.selected_row, 10);
16840
16841        // Go to page 5 (should show buckets 40-49)
16842        app.go_to_page(5);
16843        assert_eq!(app.s3_state.selected_row, 40);
16844    }
16845
16846    #[test]
16847    fn test_s3_bucket_left_right_arrows_change_pages() {
16848        use S3Bucket;
16849
16850        let mut app = test_app();
16851        app.current_service = Service::S3Buckets;
16852        app.mode = Mode::FilterInput;
16853
16854        // Create 100 buckets
16855        app.s3_state.buckets.items = (0..100)
16856            .map(|i| S3Bucket {
16857                name: format!("bucket{:03}", i),
16858                region: "us-east-1".to_string(),
16859                creation_date: String::new(),
16860            })
16861            .collect();
16862
16863        app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16864        app.s3_state.selected_row = 0;
16865        app.s3_state.input_focus = crate::common::InputFocus::Pagination;
16866
16867        // Right arrow (PageDown) should go to next page
16868        app.handle_action(Action::PageDown);
16869        assert_eq!(app.s3_state.selected_row, 10);
16870
16871        // Right arrow again
16872        app.handle_action(Action::PageDown);
16873        assert_eq!(app.s3_state.selected_row, 20);
16874
16875        // Left arrow (PageUp) should go back
16876        app.handle_action(Action::PageUp);
16877        assert_eq!(app.s3_state.selected_row, 10);
16878    }
16879
16880    #[test]
16881    fn test_s3_bucket_preview_uses_bucket_region_not_config_region() {
16882        // Regression: load_bucket_preview was falling back to self.config.region when
16883        // the bucket's stored region was empty (which it always is from list_buckets).
16884        // This caused IllegalLocationConstraintException for cross-region buckets.
16885        // The fix: resolve the actual region via get_bucket_location when region is empty.
16886        //
16887        // This test verifies the region stored in the bucket is used when available,
16888        // not the app's configured region.
16889        use S3Bucket;
16890        let mut app = test_app();
16891        app.config.region = "ap-southeast-3".to_string(); // user's configured region
16892
16893        // Bucket is actually in us-east-1 — different from app region
16894        app.s3_state.buckets.items = vec![S3Bucket {
16895            name: "my-bucket".to_string(),
16896            region: "us-east-1".to_string(), // correct bucket region stored
16897            creation_date: String::new(),
16898        }];
16899
16900        // The stored region must take precedence over config.region
16901        let bucket = app
16902            .s3_state
16903            .buckets
16904            .items
16905            .iter()
16906            .find(|b| b.name == "my-bucket")
16907            .unwrap();
16908        let effective_region = if bucket.region.is_empty() {
16909            app.config.region.as_str()
16910        } else {
16911            bucket.region.as_str()
16912        };
16913        assert_eq!(
16914            effective_region, "us-east-1",
16915            "load_bucket_preview must use bucket's region, not app config region"
16916        );
16917        assert_ne!(
16918            effective_region, "ap-southeast-3",
16919            "Must NOT fall back to configured region for cross-region buckets"
16920        );
16921    }
16922
16923    #[test]
16924    fn test_s3_scroll_follows_selection_after_bucket_expand() {
16925        use S3Bucket;
16926        use S3Object;
16927        // Regression: expanding a bucket below the visible area didn't update bucket_scroll_offset,
16928        // so the selection (now pointing to the first child row) left the visible area.
16929        let mut app = test_app();
16930        app.current_service = Service::S3Buckets;
16931        app.service_selected = true;
16932        app.mode = Mode::Normal;
16933
16934        // 5 buckets, visible area is 3 rows (bucket_visible_rows = 3)
16935        app.s3_state.buckets.items = (0..5)
16936            .map(|i| S3Bucket {
16937                name: format!("bucket{}", i),
16938                region: "us-east-1".to_string(),
16939                creation_date: String::new(),
16940            })
16941            .collect();
16942
16943        app.s3_state.bucket_visible_rows = std::cell::Cell::new(3);
16944
16945        // Select row 2 (third bucket), scroll offset = 0 → row 2 is visible
16946        app.s3_state.selected_row = 2;
16947        app.s3_state.bucket_scroll_offset = 0;
16948
16949        // Expand bucket2 — it gets 2 children, selected_row moves to 3
16950        app.s3_state.bucket_preview.insert(
16951            "bucket2".to_string(),
16952            vec![
16953                S3Object {
16954                    key: "file1.txt".to_string(),
16955                    is_prefix: false,
16956                    size: 100,
16957                    last_modified: String::new(),
16958                    storage_class: String::new(),
16959                },
16960                S3Object {
16961                    key: "file2.txt".to_string(),
16962                    is_prefix: false,
16963                    size: 200,
16964                    last_modified: String::new(),
16965                    storage_class: String::new(),
16966                },
16967            ],
16968        );
16969
16970        // Simulate expand: selected_row moves to first child (row 3)
16971        // After expand, row 3 must be within [scroll_offset, scroll_offset + visible_rows)
16972        // i.e. bucket_scroll_offset must be adjusted so row 3 is visible
16973        app.handle_action(Action::CollapseRow); // first collapse to reset
16974        app.handle_action(Action::NextPane); // expand
16975
16976        // selected_row should now be 3 (first child), scroll_offset must keep it visible
16977        let visible = app.s3_state.bucket_visible_rows.get();
16978        let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
16979            && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
16980        assert!(
16981            in_view,
16982            "selected_row {} must be visible in [{}, {})",
16983            app.s3_state.selected_row,
16984            app.s3_state.bucket_scroll_offset,
16985            app.s3_state.bucket_scroll_offset + visible
16986        );
16987    }
16988
16989    #[test]
16990    fn test_s3_expand_without_preview_does_not_move_selection() {
16991        // Regression: when a bucket preview is not yet loaded (still loading),
16992        // right arrow must NOT move selection to "first child" (row_idx + 1).
16993        // Selection must stay on the bucket row until preview arrives.
16994        use S3Bucket;
16995        let mut app = test_app();
16996        app.current_service = Service::S3Buckets;
16997        app.service_selected = true;
16998        app.mode = Mode::Normal;
16999
17000        app.s3_state.buckets.items = vec![
17001            S3Bucket {
17002                name: "bucket0".to_string(),
17003                region: "us-east-1".to_string(),
17004                creation_date: String::new(),
17005            },
17006            S3Bucket {
17007                name: "bucket1".to_string(),
17008                region: "us-east-1".to_string(),
17009                creation_date: String::new(),
17010            },
17011        ];
17012        app.s3_state.selected_row = 0; // on bucket0
17013                                       // No preview loaded yet for bucket0
17014
17015        app.handle_action(Action::NextPane); // right arrow — expand, triggers loading
17016
17017        // Must stay on row 0 until preview arrives
17018        assert_eq!(
17019            app.s3_state.selected_row, 0,
17020            "Selection must not move before preview is loaded"
17021        );
17022        // Bucket must be marked as expanded and loading
17023        assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
17024        assert!(app.s3_state.buckets.loading);
17025    }
17026
17027    #[test]
17028    fn test_s3_expand_with_preview_moves_to_first_child() {
17029        // After preview is loaded, right arrow must move selection to first child immediately.
17030        use crate::s3::Object as S3Object;
17031        use S3Bucket;
17032        let mut app = test_app();
17033        app.current_service = Service::S3Buckets;
17034        app.service_selected = true;
17035        app.mode = Mode::Normal;
17036
17037        app.s3_state.buckets.items = vec![
17038            S3Bucket {
17039                name: "bucket0".to_string(),
17040                region: "us-east-1".to_string(),
17041                creation_date: String::new(),
17042            },
17043            S3Bucket {
17044                name: "bucket1".to_string(),
17045                region: "us-east-1".to_string(),
17046                creation_date: String::new(),
17047            },
17048        ];
17049        app.s3_state.selected_row = 0;
17050        app.s3_state.bucket_preview.insert(
17051            "bucket0".to_string(),
17052            vec![S3Object {
17053                key: "file.txt".to_string(),
17054                is_prefix: false,
17055                size: 0,
17056                last_modified: String::new(),
17057                storage_class: String::new(),
17058            }],
17059        );
17060        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17061
17062        // First right arrow: expands and immediately moves to first child (preview already loaded)
17063        app.handle_action(Action::NextPane);
17064        assert_eq!(app.s3_state.selected_row, 1, "Should move to first child");
17065    }
17066
17067    #[test]
17068    fn test_s3_expand_then_load_then_right_goes_to_first_child_not_sibling() {
17069        // Repro: right arrow → loading starts (selection stays on bucket) →
17070        // preview arrives → right arrow again → must go to first child (row 1),
17071        // NOT to next sibling bucket (row 2 if no children counted, or higher).
17072        use crate::s3::Object as S3Object;
17073        use S3Bucket;
17074        let mut app = test_app();
17075        app.current_service = Service::S3Buckets;
17076        app.service_selected = true;
17077        app.mode = Mode::Normal;
17078        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17079
17080        app.s3_state.buckets.items = vec![
17081            S3Bucket {
17082                name: "bucket0".to_string(),
17083                region: "us-east-1".to_string(),
17084                creation_date: String::new(),
17085            },
17086            S3Bucket {
17087                name: "bucket1".to_string(),
17088                region: "us-east-1".to_string(),
17089                creation_date: String::new(),
17090            },
17091        ];
17092        app.s3_state.selected_row = 0;
17093
17094        // Step 1: right arrow — no preview yet, expands + loading
17095        app.handle_action(Action::NextPane);
17096        assert_eq!(
17097            app.s3_state.selected_row, 0,
17098            "Must stay on bucket0 while loading"
17099        );
17100        assert!(app.s3_state.buckets.loading);
17101
17102        // Step 2: simulate preview arriving (like main.rs does after load_bucket_preview)
17103        app.s3_state.bucket_preview.insert(
17104            "bucket0".to_string(),
17105            vec![S3Object {
17106                key: "file.txt".to_string(),
17107                is_prefix: false,
17108                size: 0,
17109                last_modified: String::new(),
17110                storage_class: String::new(),
17111            }],
17112        );
17113        app.s3_state.buckets.loading = false;
17114
17115        // Step 3: right arrow again — must go to first child (row 1), not sibling (row 2)
17116        app.handle_action(Action::NextPane);
17117        assert_eq!(
17118            app.s3_state.selected_row, 1,
17119            "After preview loads, right arrow must select first child (row 1), not sibling (row 2)"
17120        );
17121    }
17122
17123    #[test]
17124    fn test_s3_first_expand_never_moves_selection_before_children_visible() {
17125        // Rule: selection must NEVER move to a new row until the children rows
17126        // are actually present in the data (preview loaded). This prevents
17127        // showing a "selected next sibling" before children appear.
17128        use S3Bucket;
17129        let mut app = test_app();
17130        app.current_service = Service::S3Buckets;
17131        app.service_selected = true;
17132        app.mode = Mode::Normal;
17133        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17134
17135        app.s3_state.buckets.items = vec![
17136            S3Bucket {
17137                name: "bucket0".to_string(),
17138                region: "us-east-1".to_string(),
17139                creation_date: String::new(),
17140            },
17141            S3Bucket {
17142                name: "bucket1".to_string(),
17143                region: "us-east-1".to_string(),
17144                creation_date: String::new(),
17145            },
17146        ];
17147        // No preview for bucket0
17148        app.s3_state.selected_row = 0;
17149
17150        // First expand (right arrow = ExpandRow): no preview → must NOT move selection
17151        app.handle_action(Action::ExpandRow);
17152        assert_eq!(
17153            app.s3_state.selected_row, 0,
17154            "Selection must stay on bucket0 until children are visible"
17155        );
17156        assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
17157        assert!(app.s3_state.buckets.loading);
17158    }
17159
17160    #[test]
17161    fn test_s3_after_preview_loads_selection_moves_to_first_child() {
17162        // After first expand triggers loading, once preview arrives the selection
17163        // must automatically advance to the first child.
17164        use crate::s3::Object as S3Object;
17165        use S3Bucket;
17166        let mut app = test_app();
17167        app.current_service = Service::S3Buckets;
17168        app.service_selected = true;
17169        app.mode = Mode::Normal;
17170        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17171        app.s3_state.buckets.items = vec![
17172            S3Bucket {
17173                name: "b0".to_string(),
17174                region: "us-east-1".to_string(),
17175                creation_date: String::new(),
17176            },
17177            S3Bucket {
17178                name: "b1".to_string(),
17179                region: "us-east-1".to_string(),
17180                creation_date: String::new(),
17181            },
17182        ];
17183        app.s3_state.selected_row = 0;
17184
17185        // Step 1: right arrow — no preview, expand + loading, selection stays
17186        app.handle_action(Action::ExpandRow);
17187        assert_eq!(
17188            app.s3_state.selected_row, 0,
17189            "Must stay on bucket while loading"
17190        );
17191
17192        // Step 2: simulate preview arriving (main.rs calls load_bucket_preview)
17193        app.s3_state.bucket_preview.insert(
17194            "b0".to_string(),
17195            vec![S3Object {
17196                key: "f.txt".to_string(),
17197                is_prefix: false,
17198                size: 0,
17199                last_modified: String::new(),
17200                storage_class: String::new(),
17201            }],
17202        );
17203        // After load, app must advance selection to first child
17204        app.after_bucket_preview_loaded("b0");
17205        assert_eq!(
17206            app.s3_state.selected_row, 1,
17207            "After preview loads, selection must advance to first child (row 1)"
17208        );
17209    }
17210
17211    #[test]
17212    fn test_s3_right_arrow_on_expanded_loaded_bucket_enters_first_child() {
17213        // When bucket is already expanded AND preview is loaded, right arrow
17214        // must move selection to first child — NOT collapse the bucket.
17215        use crate::s3::Object as S3Object;
17216        use S3Bucket;
17217        let mut app = test_app();
17218        app.current_service = Service::S3Buckets;
17219        app.service_selected = true;
17220        app.mode = Mode::Normal;
17221        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17222        app.s3_state.buckets.items = vec![
17223            S3Bucket {
17224                name: "b0".to_string(),
17225                region: "us-east-1".to_string(),
17226                creation_date: String::new(),
17227            },
17228            S3Bucket {
17229                name: "b1".to_string(),
17230                region: "us-east-1".to_string(),
17231                creation_date: String::new(),
17232            },
17233        ];
17234        app.s3_state.expanded_prefixes.insert("b0".to_string());
17235        app.s3_state.bucket_preview.insert(
17236            "b0".to_string(),
17237            vec![S3Object {
17238                key: "f.txt".to_string(),
17239                is_prefix: false,
17240                size: 0,
17241                last_modified: String::new(),
17242                storage_class: String::new(),
17243            }],
17244        );
17245        app.s3_state.selected_row = 0;
17246
17247        app.handle_action(Action::ExpandRow);
17248
17249        // Must enter first child, not collapse
17250        assert!(
17251            app.s3_state.expanded_prefixes.contains("b0"),
17252            "Must stay expanded"
17253        );
17254        assert_eq!(app.s3_state.selected_row, 1, "Must move to first child");
17255    }
17256
17257    #[test]
17258    fn test_s3_second_expand_press_collapses_when_already_expanded_with_children() {
17259        // Collapse is done via left arrow (CollapseRow), not right arrow.
17260        // Right arrow on expanded+loaded bucket enters first child.
17261        use crate::s3::Object as S3Object;
17262        use S3Bucket;
17263        let mut app = test_app();
17264        app.current_service = Service::S3Buckets;
17265        app.service_selected = true;
17266        app.mode = Mode::Normal;
17267        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17268        app.s3_state.buckets.items = vec![S3Bucket {
17269            name: "bucket0".to_string(),
17270            region: "us-east-1".to_string(),
17271            creation_date: String::new(),
17272        }];
17273        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17274        app.s3_state.bucket_preview.insert(
17275            "bucket0".to_string(),
17276            vec![S3Object {
17277                key: "file.txt".to_string(),
17278                is_prefix: false,
17279                size: 0,
17280                last_modified: String::new(),
17281                storage_class: String::new(),
17282            }],
17283        );
17284        app.s3_state.selected_row = 0;
17285
17286        // Left arrow collapses
17287        app.handle_action(Action::CollapseRow);
17288        assert!(
17289            !app.s3_state.expanded_prefixes.contains("bucket0"),
17290            "Left arrow must collapse the bucket"
17291        );
17292        assert_eq!(app.s3_state.selected_row, 0);
17293    }
17294
17295    #[test]
17296    fn test_s3_expand_row_index_correct_when_prior_bucket_has_error() {
17297        // Repro: bucket0 is expanded and has an error message (1+ error rows).
17298        // next_pane uses `continue` for error buckets, skipping their error rows.
17299        // But calculate_filtered_bucket_rows counts error rows.
17300        // This causes selected_row=2 to map to bucket2 in next_pane
17301        // but to the error row of bucket0 visually — wrong expansion target.
17302        use S3Bucket;
17303        let mut app = test_app();
17304        app.current_service = Service::S3Buckets;
17305        app.service_selected = true;
17306        app.mode = Mode::Normal;
17307        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17308
17309        app.s3_state.buckets.items = vec![
17310            S3Bucket {
17311                name: "bucket0".to_string(),
17312                region: "us-east-1".to_string(),
17313                creation_date: String::new(),
17314            },
17315            S3Bucket {
17316                name: "bucket1".to_string(),
17317                region: "us-east-1".to_string(),
17318                creation_date: String::new(),
17319            },
17320            S3Bucket {
17321                name: "bucket2".to_string(),
17322                region: "us-east-1".to_string(),
17323                creation_date: String::new(),
17324            },
17325        ];
17326
17327        // bucket0 is expanded with an error (1 error row)
17328        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17329        app.s3_state
17330            .bucket_errors
17331            .insert("bucket0".to_string(), "AccessDenied".to_string());
17332
17333        // Visual rows: 0=bucket0, 1=⚠️error, 2=bucket1, 3=bucket2
17334        // calculate_filtered_bucket_rows = 4
17335        let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
17336        assert_eq!(
17337            total, 4,
17338            "Should be 4 rows: bucket0 + error + bucket1 + bucket2"
17339        );
17340
17341        // User selects bucket2 (visual row 3)
17342        app.s3_state.selected_row = 3;
17343
17344        // bucket2 has a preview
17345        use crate::s3::Object as S3Object;
17346        app.s3_state.bucket_preview.insert(
17347            "bucket2".to_string(),
17348            vec![S3Object {
17349                key: "file.txt".to_string(),
17350                is_prefix: false,
17351                size: 0,
17352                last_modified: String::new(),
17353                storage_class: String::new(),
17354            }],
17355        );
17356
17357        app.handle_action(Action::NextPane);
17358
17359        // Must expand bucket2 (row 3), select its first child (row 4)
17360        assert!(
17361            app.s3_state.expanded_prefixes.contains("bucket2"),
17362            "Must expand bucket2, not bucket1"
17363        );
17364        assert_eq!(
17365            app.s3_state.selected_row, 4,
17366            "First child of bucket2 must be row 4"
17367        );
17368    }
17369
17370    #[test]
17371    fn test_s3_next_pane_row_count_matches_calculate_filtered_bucket_rows() {
17372        // The row index next_pane uses to find the selected bucket must match
17373        // the row index that calculate_filtered_bucket_rows produces.
17374        // If they diverge, selecting row N expands a different bucket than expected.
17375        use crate::s3::Object as S3Object;
17376        use S3Bucket;
17377        let mut app = test_app();
17378        app.current_service = Service::S3Buckets;
17379        app.service_selected = true;
17380        app.mode = Mode::Normal;
17381        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17382
17383        app.s3_state.buckets.items = vec![
17384            S3Bucket {
17385                name: "bucket0".to_string(),
17386                region: "us-east-1".to_string(),
17387                creation_date: String::new(),
17388            },
17389            S3Bucket {
17390                name: "bucket1".to_string(),
17391                region: "us-east-1".to_string(),
17392                creation_date: String::new(),
17393            },
17394            S3Bucket {
17395                name: "bucket2".to_string(),
17396                region: "us-east-1".to_string(),
17397                creation_date: String::new(),
17398            },
17399        ];
17400
17401        // bucket0 expanded + loaded with 3 children
17402        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17403        app.s3_state.bucket_preview.insert(
17404            "bucket0".to_string(),
17405            vec![
17406                S3Object {
17407                    key: "a.txt".to_string(),
17408                    is_prefix: false,
17409                    size: 0,
17410                    last_modified: String::new(),
17411                    storage_class: String::new(),
17412                },
17413                S3Object {
17414                    key: "b.txt".to_string(),
17415                    is_prefix: false,
17416                    size: 0,
17417                    last_modified: String::new(),
17418                    storage_class: String::new(),
17419                },
17420                S3Object {
17421                    key: "c.txt".to_string(),
17422                    is_prefix: false,
17423                    size: 0,
17424                    last_modified: String::new(),
17425                    storage_class: String::new(),
17426                },
17427            ],
17428        );
17429
17430        // bucket1 expanded + loading (no preview yet)
17431        app.s3_state.expanded_prefixes.insert("bucket1".to_string());
17432
17433        // calculate_filtered_bucket_rows: 3 buckets + 3 children of bucket0 + 0 loading = 6
17434        let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
17435        assert_eq!(
17436            total, 6,
17437            "Total rows: bucket0(1)+3children+bucket1(1)+bucket2(1)=6"
17438        );
17439
17440        // Visual rows: 0=bucket0, 1=a, 2=b, 3=c, 4=bucket1(loading), 5=bucket2
17441        // Select bucket2 (row 5)
17442        app.s3_state.selected_row = 5;
17443
17444        // bucket2 has preview
17445        app.s3_state.bucket_preview.insert(
17446            "bucket2".to_string(),
17447            vec![S3Object {
17448                key: "x.txt".to_string(),
17449                is_prefix: false,
17450                size: 0,
17451                last_modified: String::new(),
17452                storage_class: String::new(),
17453            }],
17454        );
17455
17456        app.handle_action(Action::NextPane);
17457
17458        assert!(
17459            app.s3_state.expanded_prefixes.contains("bucket2"),
17460            "Must expand bucket2, not bucket1"
17461        );
17462        assert_eq!(
17463            app.s3_state.selected_row, 6,
17464            "First child of bucket2 must be row 6"
17465        );
17466    }
17467
17468    #[test]
17469    fn test_s3_expand_row_index_correct_when_prior_bucket_has_loaded_children() {
17470        // Repro: bucket0 is expanded AND has loaded children (e.g. 2 objects).
17471        // This means visual rows are: 0=bucket0, 1=child0, 2=child1, 3=bucket1, 4=bucket2
17472        // If user selects bucket2 (visual row 4) and presses right, next_pane must
17473        // expand bucket2 — NOT a child of bucket0 (which is at row_idx 4 in old logic).
17474        use crate::s3::Object as S3Object;
17475        use S3Bucket;
17476        let mut app = test_app();
17477        app.current_service = Service::S3Buckets;
17478        app.service_selected = true;
17479        app.mode = Mode::Normal;
17480        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17481
17482        app.s3_state.buckets.items = vec![
17483            S3Bucket {
17484                name: "bucket0".to_string(),
17485                region: "us-east-1".to_string(),
17486                creation_date: String::new(),
17487            },
17488            S3Bucket {
17489                name: "bucket1".to_string(),
17490                region: "us-east-1".to_string(),
17491                creation_date: String::new(),
17492            },
17493            S3Bucket {
17494                name: "bucket2".to_string(),
17495                region: "us-east-1".to_string(),
17496                creation_date: String::new(),
17497            },
17498        ];
17499
17500        // bucket0 is expanded AND has 2 children loaded
17501        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17502        app.s3_state.bucket_preview.insert(
17503            "bucket0".to_string(),
17504            vec![
17505                S3Object {
17506                    key: "a.txt".to_string(),
17507                    is_prefix: false,
17508                    size: 0,
17509                    last_modified: String::new(),
17510                    storage_class: String::new(),
17511                },
17512                S3Object {
17513                    key: "b.txt".to_string(),
17514                    is_prefix: false,
17515                    size: 0,
17516                    last_modified: String::new(),
17517                    storage_class: String::new(),
17518                },
17519            ],
17520        );
17521
17522        // Visual rows: 0=bucket0, 1=a.txt, 2=b.txt, 3=bucket1, 4=bucket2
17523        // User selects bucket2 (visual row 4)
17524        app.s3_state.selected_row = 4;
17525
17526        // bucket2 has preview ready
17527        app.s3_state.bucket_preview.insert(
17528            "bucket2".to_string(),
17529            vec![S3Object {
17530                key: "c.txt".to_string(),
17531                is_prefix: false,
17532                size: 0,
17533                last_modified: String::new(),
17534                storage_class: String::new(),
17535            }],
17536        );
17537
17538        app.handle_action(Action::NextPane);
17539
17540        // Must expand bucket2 (row 4) and move to its first child (row 5)
17541        assert!(
17542            app.s3_state.expanded_prefixes.contains("bucket2"),
17543            "bucket2 must be expanded"
17544        );
17545        assert_eq!(
17546            app.s3_state.selected_row, 5,
17547            "Must select first child of bucket2 (row 5), not some wrong row"
17548        );
17549        assert!(
17550            !app.s3_state.expanded_prefixes.contains("bucket1"),
17551            "bucket1 must NOT be expanded"
17552        );
17553    }
17554
17555    #[test]
17556    fn test_s3_expand_with_empty_preview_stays_on_bucket() {
17557        // Repro: bucket has a preview but it's EMPTY (no objects at root).
17558        // After expand, selected_row must stay on the bucket row — NOT advance to next sibling.
17559        use S3Bucket;
17560        let mut app = test_app();
17561        app.current_service = Service::S3Buckets;
17562        app.service_selected = true;
17563        app.mode = Mode::Normal;
17564        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17565
17566        app.s3_state.buckets.items = vec![
17567            S3Bucket {
17568                name: "bucket0".to_string(),
17569                region: "us-east-1".to_string(),
17570                creation_date: String::new(),
17571            },
17572            S3Bucket {
17573                name: "bucket1".to_string(),
17574                region: "us-east-1".to_string(),
17575                creation_date: String::new(),
17576            },
17577            S3Bucket {
17578                name: "bucket2".to_string(),
17579                region: "us-east-1".to_string(),
17580                creation_date: String::new(),
17581            },
17582        ];
17583        // bucket1 has empty preview (bucket exists but no root-level objects)
17584        app.s3_state
17585            .bucket_preview
17586            .insert("bucket1".to_string(), vec![]);
17587        app.s3_state.selected_row = 1; // on bucket1
17588
17589        app.handle_action(Action::NextPane);
17590
17591        // Must stay on bucket1 (row 1) — no children to enter
17592        assert_eq!(
17593            app.s3_state.selected_row, 1,
17594            "Empty bucket: selection must stay on bucket row, not advance to next sibling"
17595        );
17596        assert!(
17597            app.s3_state.expanded_prefixes.contains("bucket1"),
17598            "Bucket must be expanded (showing ▼)"
17599        );
17600    }
17601
17602    #[test]
17603    fn test_s3_expand_with_preceding_loading_buckets_on_prior_page() {
17604        // Repro: user is on page 2 (scroll_offset=50). Some buckets on page 1
17605        // are expanded+loading. The next_pane traversal iterates all buckets from 0,
17606        // and each loading bucket on page 1 still adds a phantom row, shifting
17607        // the row where selected_row=55 actually lands.
17608        use crate::s3::Object as S3Object;
17609        use S3Bucket;
17610        let mut app = test_app();
17611        app.current_service = Service::S3Buckets;
17612        app.service_selected = true;
17613        app.mode = Mode::Normal;
17614        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17615
17616        // 60 buckets
17617        app.s3_state.buckets.items = (0..60)
17618            .map(|i| S3Bucket {
17619                name: format!("bucket{:02}", i),
17620                region: "us-east-1".to_string(),
17621                creation_date: String::new(),
17622            })
17623            .collect();
17624
17625        // Buckets 0-2 on page 1 are expanded+loading (no preview)
17626        for i in 0..3usize {
17627            app.s3_state
17628                .expanded_prefixes
17629                .insert(format!("bucket{:02}", i));
17630        }
17631
17632        app.s3_state.bucket_scroll_offset = 50;
17633
17634        // bucket55 has preview loaded
17635        app.s3_state.bucket_preview.insert(
17636            "bucket55".to_string(),
17637            vec![S3Object {
17638                key: "file.txt".to_string(),
17639                is_prefix: false,
17640                size: 0,
17641                last_modified: String::new(),
17642                storage_class: String::new(),
17643            }],
17644        );
17645
17646        // selected_row = 55 (bucket55, visible row 5 on page 2)
17647        app.s3_state.selected_row = 55;
17648
17649        app.handle_action(Action::NextPane);
17650
17651        // Must expand bucket55, select its first child (row 56)
17652        assert!(
17653            app.s3_state.expanded_prefixes.contains("bucket55"),
17654            "bucket55 must be expanded"
17655        );
17656        assert_eq!(
17657            app.s3_state.selected_row, 56,
17658            "Must select first child of bucket55 (row 56)"
17659        );
17660    }
17661
17662    #[test]
17663    fn test_s3_expand_with_multiple_preceding_loading_buckets() {
17664        // Repro: multiple buckets before the selected one are expanded+loading.
17665        // Each was incorrectly adding a phantom row causing selection to point to wrong bucket.
17666        use crate::s3::Object as S3Object;
17667        use S3Bucket;
17668        let mut app = test_app();
17669        app.current_service = Service::S3Buckets;
17670        app.service_selected = true;
17671        app.mode = Mode::Normal;
17672        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17673
17674        app.s3_state.buckets.items = (0..5)
17675            .map(|i| S3Bucket {
17676                name: format!("bucket{}", i),
17677                region: "us-east-1".to_string(),
17678                creation_date: String::new(),
17679            })
17680            .collect();
17681
17682        // Buckets 0, 1, 2 are expanded+loading (no preview)
17683        for i in 0..3 {
17684            app.s3_state
17685                .expanded_prefixes
17686                .insert(format!("bucket{}", i));
17687        }
17688
17689        // bucket3 has preview loaded
17690        app.s3_state.bucket_preview.insert(
17691            "bucket3".to_string(),
17692            vec![S3Object {
17693                key: "file.txt".to_string(),
17694                is_prefix: false,
17695                size: 0,
17696                last_modified: String::new(),
17697                storage_class: String::new(),
17698            }],
17699        );
17700
17701        // Visual: bucket0 (row 0), bucket1 (row 1), bucket2 (row 2), bucket3 (row 3), bucket4 (row 4)
17702        // No loading rows visible since previews not loaded
17703        app.s3_state.selected_row = 3; // bucket3
17704
17705        app.handle_action(Action::NextPane);
17706
17707        // Must expand bucket3, move to its first child (row 4)
17708        assert!(
17709            app.s3_state.expanded_prefixes.contains("bucket3"),
17710            "bucket3 must be expanded"
17711        );
17712        assert_eq!(
17713            app.s3_state.selected_row, 4,
17714            "Must select first child of bucket3 (row 4)"
17715        );
17716    }
17717
17718    #[test]
17719    fn test_s3_expand_with_preceding_loading_bucket_selects_correct_row() {
17720        // Repro: bucket0 is expanded+loading (in expanded_prefixes, no preview).
17721        // next_pane's traversal adds a phantom row for bucket0's loading state.
17722        // This shifts row_idx so that selecting bucket1 (visual row 1) actually
17723        // points to bucket2 (visual row 2) in the traversal — wrong expansion target.
17724        use crate::s3::Object as S3Object;
17725        use S3Bucket;
17726        let mut app = test_app();
17727        app.current_service = Service::S3Buckets;
17728        app.service_selected = true;
17729        app.mode = Mode::Normal;
17730        app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17731
17732        app.s3_state.buckets.items = vec![
17733            S3Bucket {
17734                name: "bucket0".to_string(),
17735                region: "us-east-1".to_string(),
17736                creation_date: String::new(),
17737            },
17738            S3Bucket {
17739                name: "bucket1".to_string(),
17740                region: "us-east-1".to_string(),
17741                creation_date: String::new(),
17742            },
17743            S3Bucket {
17744                name: "bucket2".to_string(),
17745                region: "us-east-1".to_string(),
17746                creation_date: String::new(),
17747            },
17748        ];
17749
17750        // bucket0 is expanded+loading (in expanded_prefixes but NO preview)
17751        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17752        app.s3_state.buckets.loading = true;
17753
17754        // bucket1's preview is loaded
17755        app.s3_state.bucket_preview.insert(
17756            "bucket1".to_string(),
17757            vec![S3Object {
17758                key: "file.txt".to_string(),
17759                is_prefix: false,
17760                size: 0,
17761                last_modified: String::new(),
17762                storage_class: String::new(),
17763            }],
17764        );
17765
17766        // User selects bucket1 (visual row 1 = bucket row, bucket0 has no visible children)
17767        app.s3_state.selected_row = 1;
17768
17769        // Expand bucket1
17770        app.handle_action(Action::NextPane);
17771
17772        // Must expand bucket1 (not bucket2), so selected_row = 2 (first child of bucket1)
17773        assert_eq!(
17774            app.s3_state.selected_row, 2,
17775            "Must expand bucket1's first child (row 2), not bucket2 (row 3)"
17776        );
17777        assert!(
17778            app.s3_state.expanded_prefixes.contains("bucket1"),
17779            "bucket1 must be in expanded_prefixes"
17780        );
17781    }
17782
17783    #[test]
17784    fn test_s3_expand_last_row_on_page_shows_first_child_not_next_page() {
17785        // Repro: user selects the last bucket on a page (e.g. row 49 with page size 50),
17786        // expands it. selected_row becomes 50 (first child). But scroll_offset is still 0,
17787        // so row 50 is off screen — visually the next bucket (row 50 on next page) appears selected.
17788        // Fix: after expanding the last row on a visible page, scroll must advance to show first child.
17789        use crate::s3::Object as S3Object;
17790        use S3Bucket;
17791        let mut app = test_app();
17792        app.current_service = Service::S3Buckets;
17793        app.service_selected = true;
17794        app.mode = Mode::Normal;
17795
17796        // 3 buckets, visible area = 2 rows (tight to force the issue)
17797        app.s3_state.buckets.items = vec![
17798            S3Bucket {
17799                name: "bucket0".to_string(),
17800                region: "us-east-1".to_string(),
17801                creation_date: String::new(),
17802            },
17803            S3Bucket {
17804                name: "bucket1".to_string(),
17805                region: "us-east-1".to_string(),
17806                creation_date: String::new(),
17807            },
17808            S3Bucket {
17809                name: "bucket2".to_string(),
17810                region: "us-east-1".to_string(),
17811                creation_date: String::new(),
17812            },
17813        ];
17814        app.s3_state.bucket_visible_rows = std::cell::Cell::new(2);
17815        // Select the last visible bucket (row 1), scroll_offset = 0 → rows 0 and 1 visible
17816        app.s3_state.selected_row = 1;
17817        app.s3_state.bucket_scroll_offset = 0;
17818
17819        // Preview already loaded
17820        app.s3_state.bucket_preview.insert(
17821            "bucket1".to_string(),
17822            vec![S3Object {
17823                key: "file.txt".to_string(),
17824                is_prefix: false,
17825                size: 0,
17826                last_modified: String::new(),
17827                storage_class: String::new(),
17828            }],
17829        );
17830
17831        app.handle_action(Action::NextPane); // right arrow — expand
17832
17833        // selected_row must be 2 (first child of bucket1)
17834        assert_eq!(app.s3_state.selected_row, 2, "Must select first child");
17835
17836        // scroll_offset must have advanced so row 2 is visible
17837        let visible = app.s3_state.bucket_visible_rows.get();
17838        let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
17839            && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
17840        assert!(
17841            in_view,
17842            "First child row {} must be visible in [{}, {})",
17843            app.s3_state.selected_row,
17844            app.s3_state.bucket_scroll_offset,
17845            app.s3_state.bucket_scroll_offset + visible
17846        );
17847    }
17848
17849    #[test]
17850    fn test_apig_detail_tab_navigation() {
17851        use crate::apig::api::RestApi;
17852        use crate::ui::apig::ApiDetailTab;
17853
17854        let mut app = test_app();
17855        app.current_service = Service::ApiGatewayApis;
17856        app.apig_state.current_api = Some(RestApi {
17857            id: "test123".to_string(),
17858            name: "Test API".to_string(),
17859            description: "Test".to_string(),
17860            created_date: "2024-01-01".to_string(),
17861            api_key_source: "HEADER".to_string(),
17862            endpoint_configuration: "REGIONAL".to_string(),
17863            protocol_type: "REST".to_string(),
17864            disable_execute_api_endpoint: false,
17865            status: "AVAILABLE".to_string(),
17866        });
17867
17868        // Should start on Routes tab
17869        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17870
17871        // Next tab - should stay on Routes (only tab)
17872        app.handle_action(Action::NextDetailTab);
17873        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17874
17875        // Prev tab - should stay on Routes (only tab)
17876        app.handle_action(Action::PrevDetailTab);
17877        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17878
17879        // Another next
17880        app.handle_action(Action::NextDetailTab);
17881        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17882
17883        // Prev tab should stay on Routes (only tab)
17884        app.handle_action(Action::PrevDetailTab);
17885        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17886
17887        // Next tab should stay on Routes (only tab)
17888        app.handle_action(Action::NextDetailTab);
17889        assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17890    }
17891
17892    #[test]
17893    fn test_apig_routes_expand_collapse() {
17894        use crate::apig::api::RestApi;
17895        use crate::apig::route::Route;
17896        use crate::ui::apig::ApiDetailTab;
17897
17898        let mut app = test_app();
17899        app.current_service = Service::ApiGatewayApis;
17900        app.apig_state.current_api = Some(RestApi {
17901            id: "test123".to_string(),
17902            name: "Test API".to_string(),
17903            description: "Test".to_string(),
17904            created_date: "2024-01-01".to_string(),
17905            api_key_source: "HEADER".to_string(),
17906            endpoint_configuration: "REGIONAL".to_string(),
17907            protocol_type: "HTTP".to_string(),
17908            disable_execute_api_endpoint: false,
17909            status: "AVAILABLE".to_string(),
17910        });
17911        app.apig_state.detail_tab = ApiDetailTab::Routes;
17912
17913        // Create hierarchy with virtual parent
17914        let virtual_parent = Route {
17915            route_id: "virtual_/api".to_string(),
17916            route_key: "/api".to_string(),
17917            target: String::new(), // Empty target = virtual parent
17918            authorization_type: String::new(),
17919            api_key_required: false,
17920            display_name: String::new(),
17921            arn: String::new(),
17922        };
17923        let child_route = Route {
17924            route_id: "1".to_string(),
17925            route_key: "/api/users".to_string(),
17926            target: "integration1".to_string(),
17927            authorization_type: "NONE".to_string(),
17928            api_key_required: false,
17929            display_name: String::new(),
17930            arn: String::new(),
17931        };
17932
17933        app.apig_state.routes.items = vec![virtual_parent];
17934        app.apig_state
17935            .route_children
17936            .insert("/api".to_string(), vec![child_route]);
17937
17938        // Initially no routes expanded
17939        assert!(app.apig_state.expanded_routes.is_empty());
17940
17941        // Expand virtual parent
17942        app.apig_state.routes.selected = 0;
17943        app.handle_action(Action::ExpandRow);
17944        assert!(app.apig_state.expanded_routes.contains("/api"));
17945
17946        // Collapse virtual parent
17947        app.handle_action(Action::CollapseRow);
17948        assert!(!app.apig_state.expanded_routes.contains("/api"));
17949
17950        // Toggle expand again
17951        app.handle_action(Action::ExpandRow);
17952        assert!(app.apig_state.expanded_routes.contains("/api"));
17953    }
17954
17955    #[test]
17956    fn test_apig_routes_navigation() {
17957        use crate::apig::api::RestApi;
17958        use crate::apig::route::Route;
17959        use crate::ui::apig::ApiDetailTab;
17960
17961        let mut app = test_app();
17962        app.mode = Mode::Normal;
17963        app.service_selected = true;
17964        app.current_service = Service::ApiGatewayApis;
17965        app.apig_state.current_api = Some(RestApi {
17966            id: "test123".to_string(),
17967            name: "Test API".to_string(),
17968            description: "Test".to_string(),
17969            created_date: "2024-01-01".to_string(),
17970            api_key_source: "HEADER".to_string(),
17971            endpoint_configuration: "REGIONAL".to_string(),
17972            protocol_type: "HTTP".to_string(),
17973            disable_execute_api_endpoint: false,
17974            status: "AVAILABLE".to_string(),
17975        });
17976        app.apig_state.detail_tab = ApiDetailTab::Routes;
17977        app.apig_state.routes.items = vec![
17978            Route {
17979                route_id: "1".to_string(),
17980                route_key: "/api/users".to_string(),
17981                target: "integration1".to_string(),
17982                authorization_type: "NONE".to_string(),
17983                api_key_required: false,
17984                display_name: String::new(),
17985                arn: String::new(),
17986            },
17987            Route {
17988                route_id: "2".to_string(),
17989                route_key: "/health".to_string(),
17990                target: "integration2".to_string(),
17991                authorization_type: "NONE".to_string(),
17992                api_key_required: false,
17993                display_name: String::new(),
17994                arn: String::new(),
17995            },
17996        ];
17997
17998        // Start at first route
17999        assert_eq!(app.apig_state.routes.selected, 0);
18000
18001        // Navigate down
18002        app.handle_action(Action::NextItem);
18003        assert_eq!(app.apig_state.routes.selected, 1);
18004
18005        // Navigate down (should stay at last)
18006        app.handle_action(Action::NextItem);
18007        assert_eq!(app.apig_state.routes.selected, 1);
18008
18009        // Navigate up
18010        app.handle_action(Action::PrevItem);
18011        assert_eq!(app.apig_state.routes.selected, 0);
18012
18013        // Navigate up (should stay at first)
18014        app.handle_action(Action::PrevItem);
18015        assert_eq!(app.apig_state.routes.selected, 0);
18016    }
18017
18018    #[test]
18019    fn test_apig_routes_expand_jumps_to_child() {
18020        use crate::apig::api::RestApi;
18021        use crate::apig::route::Route;
18022        use crate::ui::apig::ApiDetailTab;
18023        use std::collections::HashMap;
18024
18025        let mut app = test_app();
18026        app.mode = Mode::Normal;
18027        app.service_selected = true;
18028        app.current_service = Service::ApiGatewayApis;
18029        app.apig_state.current_api = Some(RestApi {
18030            id: "test123".to_string(),
18031            name: "Test API".to_string(),
18032            description: "Test".to_string(),
18033            created_date: "2024-01-01".to_string(),
18034            api_key_source: "HEADER".to_string(),
18035            endpoint_configuration: "REGIONAL".to_string(),
18036            protocol_type: "HTTP".to_string(),
18037            disable_execute_api_endpoint: false,
18038            status: "AVAILABLE".to_string(),
18039        });
18040        app.apig_state.detail_tab = ApiDetailTab::Routes;
18041
18042        // Set up hierarchy: /api with child /api/users
18043        app.apig_state.routes.items = vec![Route {
18044            route_id: "virtual_/api".to_string(),
18045            route_key: "/api".to_string(),
18046            target: String::new(),
18047            authorization_type: String::new(),
18048            api_key_required: false,
18049            display_name: String::new(),
18050            arn: String::new(),
18051        }];
18052
18053        let mut children = HashMap::new();
18054        children.insert(
18055            "/api".to_string(),
18056            vec![Route {
18057                route_id: "1".to_string(),
18058                route_key: "/api/users".to_string(),
18059                target: "integration1".to_string(),
18060                authorization_type: "NONE".to_string(),
18061                api_key_required: false,
18062                display_name: String::new(),
18063                arn: String::new(),
18064            }],
18065        );
18066        app.apig_state.route_children = children;
18067
18068        // Start at /api (row 0)
18069        assert_eq!(app.apig_state.routes.selected, 0);
18070        assert!(!app.apig_state.expanded_routes.contains("/api"));
18071
18072        // First expand - should expand /api
18073        app.handle_action(Action::ExpandRow);
18074        assert!(app.apig_state.expanded_routes.contains("/api"));
18075        assert_eq!(app.apig_state.routes.selected, 0);
18076
18077        // Second expand - should jump to first child (row 1)
18078        app.handle_action(Action::ExpandRow);
18079        assert_eq!(app.apig_state.routes.selected, 1);
18080    }
18081
18082    #[test]
18083    fn test_apig_filter_only_when_focused() {
18084        use crate::apig::api::RestApi;
18085        use crate::ui::apig::ApiDetailTab;
18086
18087        let mut app = test_app();
18088        app.current_service = Service::ApiGatewayApis;
18089        app.apig_state.current_api = Some(RestApi {
18090            id: "test".to_string(),
18091            name: "Test API".to_string(),
18092            description: "Test".to_string(),
18093            created_date: "2024-01-01".to_string(),
18094            api_key_source: "HEADER".to_string(),
18095            endpoint_configuration: "REGIONAL".to_string(),
18096            protocol_type: "HTTP".to_string(),
18097            disable_execute_api_endpoint: false,
18098            status: "AVAILABLE".to_string(),
18099        });
18100        app.apig_state.detail_tab = ApiDetailTab::Routes;
18101        app.mode = Mode::FilterInput;
18102
18103        // When input_focus is NOT on Filter, typing should not update filter
18104        app.apig_state.input_focus = InputFocus::Pagination;
18105        app.handle_action(Action::FilterInput('x'));
18106        assert_eq!(app.apig_state.route_filter, "");
18107
18108        // When input_focus IS on Filter, typing should update filter
18109        app.apig_state.input_focus = InputFocus::Filter;
18110        app.handle_action(Action::FilterInput('x'));
18111        assert_eq!(app.apig_state.route_filter, "x");
18112    }
18113
18114    #[test]
18115    fn test_apig_routes_and_resources_use_same_render_function() {
18116        // Both routes and resources must call crate::ui::table::render_tree_table
18117        let source = include_str!("ui/apig.rs");
18118        let render_calls: Vec<_> = source
18119            .match_indices("crate::ui::table::render_tree_table")
18120            .collect();
18121
18122        // Should have exactly 2 calls: one for resources, one for routes
18123        assert_eq!(
18124            render_calls.len(),
18125            2,
18126            "Both routes and resources must use render_tree_table"
18127        );
18128    }
18129
18130    #[test]
18131    fn test_s3_uses_same_render_function() {
18132        // S3 objects must also call crate::ui::table::render_tree_table
18133        let source = include_str!("ui/s3.rs");
18134        let render_calls: Vec<_> = source
18135            .match_indices("crate::ui::table::render_tree_table")
18136            .collect();
18137
18138        // Should have at least 1 call for objects table
18139        assert!(!render_calls.is_empty(), "S3 must use render_tree_table");
18140    }
18141
18142    #[test]
18143    fn test_s3_page_down_skips_expanded_children() {
18144        // PageDown should land on top-level bucket rows, not expanded child rows.
18145        // With 3 buckets where bucket0 is expanded and has 20 children,
18146        // the visual layout is: row0=bucket0, rows1-20=children, row21=bucket1, row22=bucket2.
18147        // PageDown (10 buckets) from bucket0 → should land on bucket1 (only 3 buckets total).
18148        use crate::keymap::Action;
18149
18150        let mut app = test_app();
18151        app.current_service = Service::S3Buckets;
18152        app.service_selected = true;
18153        app.mode = Mode::Normal;
18154        app.s3_state.bucket_visible_rows = std::cell::Cell::new(30);
18155
18156        app.s3_state.buckets.items = (0..3)
18157            .map(|i| S3Bucket {
18158                name: format!("bucket{}", i),
18159                region: "us-east-1".to_string(),
18160                creation_date: String::new(),
18161            })
18162            .collect();
18163
18164        // Expand bucket0 with 20 children
18165        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
18166        app.s3_state.bucket_preview.insert(
18167            "bucket0".to_string(),
18168            (0..20)
18169                .map(|i| crate::app::S3Object {
18170                    key: format!("key{}", i),
18171                    size: 0,
18172                    last_modified: String::new(),
18173                    storage_class: String::new(),
18174                    is_prefix: false,
18175                })
18176                .collect(),
18177        );
18178
18179        app.s3_state.selected_row = 0;
18180
18181        // PageDown: jump 10 top-level buckets, but only 3 exist → clamp to last (bucket2)
18182        app.handle_action(Action::PageDown);
18183        // selected_row should be on bucket2 (visual row 22), not row 10 (child of bucket0)
18184        let row = app.s3_state.selected_row;
18185        assert_eq!(
18186            row, 22,
18187            "PageDown must land on bucket2 (visual row 22), got {}",
18188            row
18189        );
18190
18191        // PageUp from bucket2 back to bucket0
18192        app.handle_action(Action::PageUp);
18193        assert_eq!(
18194            app.s3_state.selected_row, 0,
18195            "PageUp must return to bucket0"
18196        );
18197    }
18198
18199    #[test]
18200    fn test_s3_page_down_with_many_buckets_skips_expanded() {
18201        // With 25 buckets where bucket0 has 100 children expanded,
18202        // PageDown from bucket0 should land on bucket10, not on a child row.
18203        use crate::keymap::Action;
18204
18205        let mut app = test_app();
18206        app.current_service = Service::S3Buckets;
18207        app.service_selected = true;
18208        app.mode = Mode::Normal;
18209        app.s3_state.bucket_visible_rows = std::cell::Cell::new(30);
18210
18211        app.s3_state.buckets.items = (0..25)
18212            .map(|i| S3Bucket {
18213                name: format!("bucket{}", i),
18214                region: "us-east-1".to_string(),
18215                creation_date: String::new(),
18216            })
18217            .collect();
18218
18219        // Expand bucket0 with 100 children
18220        app.s3_state.expanded_prefixes.insert("bucket0".to_string());
18221        app.s3_state.bucket_preview.insert(
18222            "bucket0".to_string(),
18223            (0..100)
18224                .map(|i| crate::app::S3Object {
18225                    key: format!("key{}", i),
18226                    size: 0,
18227                    last_modified: String::new(),
18228                    storage_class: String::new(),
18229                    is_prefix: false,
18230                })
18231                .collect(),
18232        );
18233
18234        app.s3_state.selected_row = 0;
18235
18236        // PageDown: should jump to bucket10 (visual row 100 + 10 = 110)
18237        app.handle_action(Action::PageDown);
18238        let row = app.s3_state.selected_row;
18239        // bucket0 = row 0, 100 children = rows 1-100, bucket1 = row 101, ..., bucket10 = row 110
18240        assert_eq!(
18241            row, 110,
18242            "PageDown must land on bucket10 (row 110), got {}",
18243            row
18244        );
18245    }
18246
18247    #[test]
18248    fn test_search_icon_has_proper_border_spacing() {
18249        // SEARCH_ICON should have dashes on both sides for proper border rendering
18250        // Should be "─ 🔍 ─" not "─ 🔍 " to avoid "╭N" appearance
18251        use crate::ui::SEARCH_ICON;
18252
18253        assert!(SEARCH_ICON.starts_with("─"), "Should start with dash");
18254        assert!(
18255            SEARCH_ICON.ends_with("─"),
18256            "Should end with dash for proper border spacing"
18257        );
18258        assert!(SEARCH_ICON.contains("🔍"), "Should contain search icon");
18259    }
18260
18261    #[test]
18262    fn test_apig_expand_with_filter() {
18263        use crate::apig::api::RestApi;
18264        use crate::apig::route::Route;
18265        use crate::ui::apig::ApiDetailTab;
18266        use std::collections::HashMap;
18267
18268        let mut app = test_app();
18269        app.current_service = Service::ApiGatewayApis;
18270        app.apig_state.current_api = Some(RestApi {
18271            id: "test123".to_string(),
18272            name: "Test API".to_string(),
18273            description: "Test".to_string(),
18274            created_date: "2024-01-01".to_string(),
18275            api_key_source: "HEADER".to_string(),
18276            endpoint_configuration: "REGIONAL".to_string(),
18277            protocol_type: "HTTP".to_string(),
18278            disable_execute_api_endpoint: false,
18279            status: "AVAILABLE".to_string(),
18280        });
18281        app.apig_state.detail_tab = ApiDetailTab::Routes;
18282
18283        // Setup routes with parent and child
18284        app.apig_state.routes.items = vec![Route {
18285            route_id: "0".to_string(),
18286            route_key: "/api".to_string(),
18287            target: "".to_string(),
18288            authorization_type: "NONE".to_string(),
18289            api_key_required: false,
18290            display_name: "/api".to_string(),
18291            arn: String::new(),
18292        }];
18293
18294        let mut children = HashMap::new();
18295        children.insert(
18296            "/api".to_string(),
18297            vec![Route {
18298                route_id: "1".to_string(),
18299                route_key: "GET".to_string(),
18300                target: "integration1".to_string(),
18301                authorization_type: "NONE".to_string(),
18302                api_key_required: false,
18303                display_name: "GET".to_string(),
18304                arn: String::new(),
18305            }],
18306        );
18307        app.apig_state.route_children = children;
18308
18309        // Apply filter that matches child
18310        app.apig_state.route_filter = "GET".to_string();
18311
18312        // Parent should be visible (because child matches)
18313        // Expand should work on filtered parent
18314        assert_eq!(app.apig_state.routes.selected, 0);
18315        assert!(!app.apig_state.expanded_routes.contains("/api"));
18316
18317        app.handle_action(Action::ExpandRow);
18318
18319        // Should expand the parent
18320        assert!(app.apig_state.expanded_routes.contains("/api"));
18321    }
18322
18323    #[test]
18324    fn test_apig_console_url_routes() {
18325        use crate::apig::api::RestApi;
18326        use crate::apig::route::Route;
18327        use crate::ui::apig::ApiDetailTab;
18328
18329        let mut app = test_app();
18330        app.current_service = Service::ApiGatewayApis;
18331        app.config.region = "us-east-1".to_string();
18332
18333        // Test API list URL
18334        let url = app.get_console_url();
18335        assert!(url.contains("apigateway/main/apis"));
18336        assert!(url.contains("region=us-east-1"));
18337
18338        // Test routes URL with selection
18339        app.apig_state.current_api = Some(RestApi {
18340            id: "2todvod3n0".to_string(),
18341            name: "Test API".to_string(),
18342            description: "Test".to_string(),
18343            created_date: "2024-01-01".to_string(),
18344            api_key_source: "HEADER".to_string(),
18345            endpoint_configuration: "REGIONAL".to_string(),
18346            protocol_type: "HTTP".to_string(),
18347            disable_execute_api_endpoint: false,
18348            status: "AVAILABLE".to_string(),
18349        });
18350        app.apig_state.detail_tab = ApiDetailTab::Routes;
18351        app.apig_state.routes.items = vec![Route {
18352            route_id: "eizmisr".to_string(),
18353            route_key: "GET /test".to_string(),
18354            target: "integration1".to_string(),
18355            authorization_type: "NONE".to_string(),
18356            api_key_required: false,
18357            display_name: "GET /test".to_string(),
18358            arn: String::new(),
18359        }];
18360        app.apig_state.routes.selected = 0;
18361
18362        let url = app.get_console_url();
18363        assert!(url.contains("apigateway/main/develop/routes"));
18364        assert!(url.contains("api=2todvod3n0"));
18365        assert!(url.contains("routes=eizmisr"));
18366        assert!(url.contains("region=us-east-1"));
18367    }
18368
18369    #[test]
18370    fn test_apig_console_url_resources() {
18371        use crate::apig::api::RestApi;
18372        use crate::apig::resource::Resource;
18373        use crate::ui::apig::ApiDetailTab;
18374
18375        let mut app = test_app();
18376        app.current_service = Service::ApiGatewayApis;
18377        app.config.region = "us-east-1".to_string();
18378
18379        // Test resources URL with selection
18380        app.apig_state.current_api = Some(RestApi {
18381            id: "2j9j50ze47".to_string(),
18382            name: "Test API".to_string(),
18383            description: "Test".to_string(),
18384            created_date: "2024-01-01".to_string(),
18385            api_key_source: "HEADER".to_string(),
18386            endpoint_configuration: "REGIONAL".to_string(),
18387            protocol_type: "REST".to_string(),
18388            disable_execute_api_endpoint: false,
18389            status: "AVAILABLE".to_string(),
18390        });
18391        app.apig_state.detail_tab = ApiDetailTab::Routes;
18392        app.apig_state.resources.items = vec![Resource {
18393            id: "abc123".to_string(),
18394            path: "/test".to_string(),
18395            parent_id: None,
18396            methods: vec![],
18397            display_name: "/test".to_string(),
18398            arn: String::new(),
18399        }];
18400        app.apig_state.resources.selected = 0;
18401
18402        let url = app.get_console_url();
18403        assert!(url.contains("apigateway/main/apis/2j9j50ze47/resources"));
18404        assert!(url.contains("api=2j9j50ze47"));
18405        assert!(url.contains("#abc123"));
18406        assert!(url.contains("region=us-east-1"));
18407    }
18408
18409    #[test]
18410    fn test_apig_console_url_routes_parent_vs_leaf() {
18411        use crate::apig::api::RestApi;
18412        use crate::apig::route::Route;
18413        use crate::ui::apig::ApiDetailTab;
18414        use std::collections::HashMap;
18415
18416        let mut app = test_app();
18417        app.current_service = Service::ApiGatewayApis;
18418        app.config.region = "us-east-1".to_string();
18419
18420        app.apig_state.current_api = Some(RestApi {
18421            id: "2todvod3n0".to_string(),
18422            name: "Test API".to_string(),
18423            description: "Test".to_string(),
18424            created_date: "2024-01-01".to_string(),
18425            api_key_source: "HEADER".to_string(),
18426            endpoint_configuration: "REGIONAL".to_string(),
18427            protocol_type: "HTTP".to_string(),
18428            disable_execute_api_endpoint: false,
18429            status: "AVAILABLE".to_string(),
18430        });
18431        app.apig_state.detail_tab = ApiDetailTab::Routes;
18432
18433        // Parent node (virtual path, no target)
18434        app.apig_state.routes.items = vec![Route {
18435            route_id: "parent".to_string(),
18436            route_key: "/v1/get/jobs".to_string(),
18437            target: "".to_string(), // Empty target = parent node
18438            authorization_type: "NONE".to_string(),
18439            api_key_required: false,
18440            display_name: "/v1/get/jobs".to_string(),
18441            arn: String::new(),
18442        }];
18443
18444        // Child leaf node (actual route with target)
18445        let mut children = HashMap::new();
18446        children.insert(
18447            "/v1/get/jobs".to_string(),
18448            vec![Route {
18449                route_id: "1iz9vtl".to_string(),
18450                route_key: "GET".to_string(),
18451                target: "integration1".to_string(), // Has target = leaf node
18452                authorization_type: "NONE".to_string(),
18453                api_key_required: false,
18454                display_name: "GET".to_string(),
18455                arn: String::new(),
18456            }],
18457        );
18458        app.apig_state.route_children = children;
18459
18460        // Select parent - should NOT include routes parameter
18461        app.apig_state.routes.selected = 0;
18462        let url = app.get_console_url();
18463        assert!(url.contains("apigateway/main/develop/routes"));
18464        assert!(url.contains("api=2todvod3n0"));
18465        assert!(
18466            !url.contains("routes="),
18467            "Parent node should not include routes parameter"
18468        );
18469
18470        // Expand parent and select child leaf
18471        app.apig_state
18472            .expanded_routes
18473            .insert("/v1/get/jobs".to_string());
18474        app.apig_state.routes.selected = 1;
18475        let url = app.get_console_url();
18476        assert!(
18477            url.contains("routes=1iz9vtl"),
18478            "Leaf node should include routes parameter: {}",
18479            url
18480        );
18481    }
18482
18483    #[test]
18484    fn test_apig_preferences_context() {
18485        use crate::apig::api::RestApi;
18486        use crate::ui::apig::ApiDetailTab;
18487
18488        let mut app = test_app();
18489        app.current_service = Service::ApiGatewayApis;
18490
18491        // In API list view - current_api is None
18492        assert!(
18493            app.apig_state.current_api.is_none(),
18494            "Should be in list view"
18495        );
18496
18497        // In detail view - current_api is Some
18498        app.apig_state.current_api = Some(RestApi {
18499            id: "test123".to_string(),
18500            name: "Test API".to_string(),
18501            description: "Test".to_string(),
18502            created_date: "2024-01-01".to_string(),
18503            api_key_source: "HEADER".to_string(),
18504            endpoint_configuration: "REGIONAL".to_string(),
18505            protocol_type: "HTTP".to_string(),
18506            disable_execute_api_endpoint: false,
18507            status: "AVAILABLE".to_string(),
18508        });
18509        app.apig_state.detail_tab = ApiDetailTab::Routes;
18510
18511        assert!(
18512            app.apig_state.current_api.is_some(),
18513            "Should be in detail view"
18514        );
18515        // Preferences rendering will check current_api.is_none() to decide what to show
18516    }
18517
18518    #[test]
18519    fn test_apig_route_columns() {
18520        use crate::apig::route::Column as RouteColumn;
18521
18522        // Verify all columns exist
18523        let cols = RouteColumn::all();
18524        assert_eq!(cols.len(), 5);
18525
18526        // Verify column IDs
18527        assert_eq!(RouteColumn::RouteKey.id(), "route_key");
18528        assert_eq!(RouteColumn::RouteId.id(), "route_id");
18529        assert_eq!(RouteColumn::Arn.id(), "arn");
18530        assert_eq!(RouteColumn::AuthorizationType.id(), "authorization_type");
18531        assert_eq!(RouteColumn::Target.id(), "target");
18532
18533        // Verify from_id
18534        assert_eq!(
18535            RouteColumn::from_id("route_key"),
18536            Some(RouteColumn::RouteKey)
18537        );
18538        assert_eq!(RouteColumn::from_id("arn"), Some(RouteColumn::Arn));
18539        assert_eq!(RouteColumn::from_id("invalid"), None);
18540    }
18541
18542    #[test]
18543    fn test_apig_yank_copies_route_arn() {
18544        use crate::apig::api::RestApi;
18545        use crate::apig::route::Route;
18546        use crate::ui::apig::ApiDetailTab;
18547
18548        let mut app = test_app();
18549        app.current_service = Service::ApiGatewayApis;
18550        app.apig_state.current_api = Some(RestApi {
18551            id: "test123".to_string(),
18552            name: "Test API".to_string(),
18553            description: "Test".to_string(),
18554            created_date: "2024-01-01".to_string(),
18555            api_key_source: "HEADER".to_string(),
18556            endpoint_configuration: "REGIONAL".to_string(),
18557            protocol_type: "HTTP".to_string(),
18558            disable_execute_api_endpoint: false,
18559            status: "AVAILABLE".to_string(),
18560        });
18561        app.apig_state.detail_tab = ApiDetailTab::Routes;
18562
18563        // Add routes with ARNs
18564        app.apig_state.routes.items = vec![
18565            Route {
18566                route_id: "route1".to_string(),
18567                route_key: "GET /users".to_string(),
18568                target: "integrations/abc".to_string(),
18569                authorization_type: "NONE".to_string(),
18570                api_key_required: false,
18571                display_name: "GET /users".to_string(),
18572                arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1".to_string(),
18573            },
18574            Route {
18575                route_id: "route2".to_string(),
18576                route_key: "POST /users".to_string(),
18577                target: "integrations/def".to_string(),
18578                authorization_type: "AWS_IAM".to_string(),
18579                api_key_required: true,
18580                display_name: "POST /users".to_string(),
18581                arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route2".to_string(),
18582            },
18583        ];
18584
18585        // Select first route
18586        app.apig_state.routes.selected = 0;
18587
18588        // Yank should copy ARN (we can't test clipboard directly, but we can verify the logic path)
18589        assert_eq!(
18590            app.apig_state.routes.items[0].arn,
18591            "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1"
18592        );
18593    }
18594
18595    #[test]
18596    fn test_apig_yank_ignores_empty_arn() {
18597        use crate::apig::api::RestApi;
18598        use crate::apig::route::Route;
18599        use crate::ui::apig::ApiDetailTab;
18600
18601        let mut app = test_app();
18602        app.current_service = Service::ApiGatewayApis;
18603        app.apig_state.current_api = Some(RestApi {
18604            id: "test123".to_string(),
18605            name: "Test API".to_string(),
18606            description: "Test".to_string(),
18607            created_date: "2024-01-01".to_string(),
18608            api_key_source: "HEADER".to_string(),
18609            endpoint_configuration: "REGIONAL".to_string(),
18610            protocol_type: "HTTP".to_string(),
18611            disable_execute_api_endpoint: false,
18612            status: "AVAILABLE".to_string(),
18613        });
18614        app.apig_state.detail_tab = ApiDetailTab::Routes;
18615
18616        // Add virtual parent node with empty ARN
18617        app.apig_state.routes.items = vec![Route {
18618            route_id: String::new(),
18619            route_key: "/users".to_string(),
18620            target: String::new(), // Virtual parent
18621            authorization_type: String::new(),
18622            api_key_required: false,
18623            display_name: "/users".to_string(),
18624            arn: String::new(), // Empty ARN
18625        }];
18626
18627        app.apig_state.routes.selected = 0;
18628
18629        // Verify empty ARN won't be copied
18630        assert!(
18631            app.apig_state.routes.items[0].arn.is_empty(),
18632            "Virtual parent should have empty ARN"
18633        );
18634    }
18635
18636    #[test]
18637    fn test_apig_route_column_toggle() {
18638        use crate::apig::api::RestApi;
18639        use crate::apig::route::Column as RouteColumn;
18640        use crate::keymap::Action;
18641        use crate::ui::apig::ApiDetailTab;
18642
18643        let mut app = test_app();
18644        app.current_service = Service::ApiGatewayApis;
18645        app.mode = Mode::ColumnSelector;
18646        app.apig_state.current_api = Some(RestApi {
18647            id: "test123".to_string(),
18648            name: "Test API".to_string(),
18649            description: "Test".to_string(),
18650            created_date: "2024-01-01".to_string(),
18651            api_key_source: "HEADER".to_string(),
18652            endpoint_configuration: "REGIONAL".to_string(),
18653            protocol_type: "HTTP".to_string(),
18654            disable_execute_api_endpoint: false,
18655            status: "AVAILABLE".to_string(),
18656        });
18657        app.apig_state.detail_tab = ApiDetailTab::Routes;
18658
18659        // Initially all columns visible
18660        assert_eq!(app.apig_route_visible_column_ids.len(), 5);
18661
18662        // Try to toggle first column (Route) - should be locked, no effect
18663        app.column_selector_index = 1;
18664        app.handle_action(Action::ToggleColumn);
18665        assert_eq!(app.apig_route_visible_column_ids.len(), 5);
18666        assert!(app
18667            .apig_route_visible_column_ids
18668            .contains(&RouteColumn::RouteKey.id()));
18669
18670        // Select ARN column (index 3 in UI)
18671        app.column_selector_index = 3;
18672        app.handle_action(Action::ToggleColumn);
18673
18674        // ARN should be removed
18675        assert_eq!(app.apig_route_visible_column_ids.len(), 4);
18676        assert!(!app
18677            .apig_route_visible_column_ids
18678            .contains(&RouteColumn::Arn.id()));
18679
18680        // Toggle again to add it back
18681        app.handle_action(Action::ToggleColumn);
18682        assert_eq!(app.apig_route_visible_column_ids.len(), 5);
18683        assert!(app
18684            .apig_route_visible_column_ids
18685            .contains(&RouteColumn::Arn.id()));
18686    }
18687
18688    #[test]
18689    fn test_apig_resource_column_toggle() {
18690        use crate::apig::api::RestApi;
18691        use crate::apig::resource::Column as ResourceColumn;
18692        use crate::keymap::Action;
18693        use crate::ui::apig::ApiDetailTab;
18694
18695        let mut app = test_app();
18696        app.current_service = Service::ApiGatewayApis;
18697        app.mode = Mode::ColumnSelector;
18698        app.apig_state.current_api = Some(RestApi {
18699            id: "test123".to_string(),
18700            name: "Test API".to_string(),
18701            description: "Test".to_string(),
18702            created_date: "2024-01-01".to_string(),
18703            api_key_source: "HEADER".to_string(),
18704            endpoint_configuration: "REGIONAL".to_string(),
18705            protocol_type: "REST".to_string(), // REST API shows resources
18706            disable_execute_api_endpoint: false,
18707            status: "AVAILABLE".to_string(),
18708        });
18709        app.apig_state.detail_tab = ApiDetailTab::Routes; // Resources shown in Routes tab for REST APIs
18710
18711        // Initially all columns visible
18712        assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
18713
18714        // Try to toggle first column (Resource) - should be locked, no effect
18715        app.column_selector_index = 1;
18716        app.handle_action(Action::ToggleColumn);
18717        assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
18718        assert!(app
18719            .apig_resource_visible_column_ids
18720            .contains(&ResourceColumn::Path.id()));
18721
18722        // Select ARN column (index 3 in UI)
18723        app.column_selector_index = 3;
18724        app.handle_action(Action::ToggleColumn);
18725
18726        // ARN should be removed
18727        assert_eq!(app.apig_resource_visible_column_ids.len(), 2);
18728        assert!(!app
18729            .apig_resource_visible_column_ids
18730            .contains(&ResourceColumn::Arn.id()));
18731
18732        // Toggle again to add it back
18733        app.handle_action(Action::ToggleColumn);
18734        assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
18735        assert!(app
18736            .apig_resource_visible_column_ids
18737            .contains(&ResourceColumn::Arn.id()));
18738    }
18739
18740    #[test]
18741    fn test_cloudtrail_filter_input() {
18742        let mut app = App::new_without_client("default".to_string(), None);
18743        app.service_selected = true;
18744        app.current_service = Service::CloudTrailEvents;
18745
18746        app.handle_action(Action::StartFilter);
18747        assert_eq!(app.mode, Mode::FilterInput);
18748
18749        app.cloudtrail_state.table.filter = "test".to_string();
18750
18751        assert_eq!(app.cloudtrail_state.table.filter, "test");
18752    }
18753
18754    #[test]
18755    fn test_cloudtrail_row_expansion() {
18756        let mut app = App::new_without_client("default".to_string(), None);
18757        app.service_selected = true;
18758        app.current_service = Service::CloudTrailEvents;
18759        app.cloudtrail_state.table.items = vec![CloudTrailEvent {
18760            event_name: "Event1".to_string(),
18761            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18762            username: "user1".to_string(),
18763            event_source: "s3.amazonaws.com".to_string(),
18764            resource_type: "Bucket".to_string(),
18765            resource_name: "my-bucket".to_string(),
18766            read_only: "false".to_string(),
18767            aws_region: "us-east-1".to_string(),
18768            event_id: "abc123".to_string(),
18769            access_key_id: "AKIA...".to_string(),
18770            source_ip_address: "1.2.3.4".to_string(),
18771            error_code: "".to_string(),
18772            request_id: "req-123".to_string(),
18773            event_type: "AwsApiCall".to_string(),
18774            cloud_trail_event_json: "{}".to_string(),
18775        }];
18776
18777        assert_eq!(app.cloudtrail_state.table.expanded_item, None);
18778
18779        app.expand_row();
18780        assert_eq!(app.cloudtrail_state.table.expanded_item, Some(0));
18781
18782        app.collapse_row();
18783        assert_eq!(app.cloudtrail_state.table.expanded_item, None);
18784    }
18785
18786    #[test]
18787    fn test_cloudtrail_service_initialization() {
18788        let app = App::new_without_client("default".to_string(), None);
18789        assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
18790        assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
18791    }
18792
18793    #[test]
18794    fn test_cloudtrail_service_name() {
18795        assert_eq!(
18796            Service::CloudTrailEvents.name(),
18797            "CloudTrail › Event History"
18798        );
18799    }
18800
18801    #[test]
18802    fn test_cloudtrail_in_service_picker() {
18803        let app = App::new_without_client("default".to_string(), None);
18804        assert!(app
18805            .service_picker
18806            .services
18807            .contains(&"CloudTrail › Event History"));
18808    }
18809
18810    #[test]
18811    fn test_cloudtrail_service_selection() {
18812        let mut app = App::new_without_client("default".to_string(), None);
18813        app.current_service = Service::CloudTrailEvents;
18814        app.service_selected = true;
18815
18816        assert_eq!(app.current_service, Service::CloudTrailEvents);
18817        assert!(app.service_selected);
18818    }
18819
18820    #[test]
18821    fn test_cloudtrail_filter_resets_selection() {
18822        let mut app = App::new_without_client("default".to_string(), None);
18823        app.service_selected = true;
18824        app.current_service = Service::CloudTrailEvents;
18825        app.cloudtrail_state.table.selected = 5;
18826        app.cloudtrail_state.table.expanded_item = Some(3);
18827
18828        app.handle_action(Action::StartFilter);
18829        app.apply_filter_operation(|_| {});
18830
18831        assert_eq!(app.cloudtrail_state.table.selected, 0);
18832        assert_eq!(app.cloudtrail_state.table.expanded_item, None);
18833    }
18834
18835    #[test]
18836    fn test_cloudtrail_column_toggle() {
18837        let mut app = App::new_without_client("default".to_string(), None);
18838        app.service_selected = true;
18839        app.current_service = Service::CloudTrailEvents;
18840        app.mode = Mode::ColumnSelector;
18841
18842        // Initially 6 columns visible
18843        assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
18844
18845        // Toggle column at index 7 (ReadOnly - 7th column, index 1-14)
18846        app.column_selector_index = 7;
18847        app.handle_action(Action::ToggleColumn);
18848
18849        // Should now have 7 visible columns
18850        assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 7);
18851    }
18852
18853    #[test]
18854    fn test_cloudtrail_tab_cycles_filter_focus() {
18855        let mut app = App::new_without_client("default".to_string(), None);
18856        app.service_selected = true;
18857        app.current_service = Service::CloudTrailEvents;
18858        app.mode = Mode::FilterInput;
18859        app.cloudtrail_state.input_focus = InputFocus::Filter;
18860
18861        // Tab should cycle to Pagination
18862        app.handle_action(Action::NextFilterFocus);
18863        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
18864
18865        // Tab again should cycle back to Filter
18866        app.handle_action(Action::NextFilterFocus);
18867        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
18868    }
18869
18870    #[test]
18871    fn test_cloudtrail_shift_tab_cycles_filter_focus() {
18872        let mut app = App::new_without_client("default".to_string(), None);
18873        app.service_selected = true;
18874        app.current_service = Service::CloudTrailEvents;
18875        app.mode = Mode::FilterInput;
18876        app.cloudtrail_state.input_focus = InputFocus::Filter;
18877
18878        // Shift+Tab should cycle to Pagination
18879        app.handle_action(Action::PrevFilterFocus);
18880        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
18881
18882        // Shift+Tab again should cycle back to Filter
18883        app.handle_action(Action::PrevFilterFocus);
18884        assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
18885    }
18886
18887    #[test]
18888    fn test_cloudtrail_detail_view_tab_cycles_focus() {
18889        let mut app = App::new_without_client("default".to_string(), None);
18890        app.service_selected = true;
18891        app.current_service = Service::CloudTrailEvents;
18892        app.mode = Mode::Normal;
18893        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18894            event_name: "PutObject".to_string(),
18895            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18896            username: "user".to_string(),
18897            event_source: "s3.amazonaws.com".to_string(),
18898            resource_type: "AWS::S3::Bucket".to_string(),
18899            resource_name: "my-bucket".to_string(),
18900            read_only: "false".to_string(),
18901            aws_region: "us-east-1".to_string(),
18902            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18903            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18904            source_ip_address: "192.0.2.1".to_string(),
18905            error_code: "".to_string(),
18906            request_id: "req-123".to_string(),
18907            event_type: "AwsApiCall".to_string(),
18908            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18909        });
18910
18911        // Default focus is Resources
18912        assert_eq!(
18913            app.cloudtrail_state.detail_focus,
18914            CloudTrailDetailFocus::Resources
18915        );
18916
18917        // Tab cycles to EventRecord
18918        app.handle_action(Action::NextDetailTab);
18919        assert_eq!(
18920            app.cloudtrail_state.detail_focus,
18921            CloudTrailDetailFocus::EventRecord
18922        );
18923
18924        // Tab cycles back to Resources
18925        app.handle_action(Action::NextDetailTab);
18926        assert_eq!(
18927            app.cloudtrail_state.detail_focus,
18928            CloudTrailDetailFocus::Resources
18929        );
18930    }
18931
18932    #[test]
18933    fn test_cloudtrail_detail_view_shift_tab_cycles_focus() {
18934        let mut app = App::new_without_client("default".to_string(), None);
18935        app.service_selected = true;
18936        app.current_service = Service::CloudTrailEvents;
18937        app.mode = Mode::Normal;
18938        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18939            event_name: "PutObject".to_string(),
18940            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18941            username: "user".to_string(),
18942            event_source: "s3.amazonaws.com".to_string(),
18943            resource_type: "AWS::S3::Bucket".to_string(),
18944            resource_name: "my-bucket".to_string(),
18945            read_only: "false".to_string(),
18946            aws_region: "us-east-1".to_string(),
18947            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18948            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18949            source_ip_address: "192.0.2.1".to_string(),
18950            error_code: "".to_string(),
18951            request_id: "req-123".to_string(),
18952            event_type: "AwsApiCall".to_string(),
18953            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18954        });
18955
18956        // Default focus is Resources
18957        assert_eq!(
18958            app.cloudtrail_state.detail_focus,
18959            CloudTrailDetailFocus::Resources
18960        );
18961
18962        // Shift+Tab cycles to EventRecord
18963        app.handle_action(Action::PrevDetailTab);
18964        assert_eq!(
18965            app.cloudtrail_state.detail_focus,
18966            CloudTrailDetailFocus::EventRecord
18967        );
18968
18969        // Shift+Tab cycles back to Resources
18970        app.handle_action(Action::PrevDetailTab);
18971        assert_eq!(
18972            app.cloudtrail_state.detail_focus,
18973            CloudTrailDetailFocus::Resources
18974        );
18975    }
18976
18977    #[test]
18978    fn test_cloudtrail_json_scroll_with_arrow_keys() {
18979        let mut app = App::new_without_client("default".to_string(), None);
18980        app.service_selected = true;
18981        app.current_service = Service::CloudTrailEvents;
18982        app.mode = Mode::Normal;
18983        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18984            event_name: "PutObject".to_string(),
18985            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18986            username: "user".to_string(),
18987            event_source: "s3.amazonaws.com".to_string(),
18988            resource_type: "AWS::S3::Bucket".to_string(),
18989            resource_name: "my-bucket".to_string(),
18990            read_only: "false".to_string(),
18991            aws_region: "us-east-1".to_string(),
18992            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18993            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18994            source_ip_address: "192.0.2.1".to_string(),
18995            error_code: "".to_string(),
18996            request_id: "req-123".to_string(),
18997            event_type: "AwsApiCall".to_string(),
18998            cloud_trail_event_json: (0..50)
18999                .map(|i| format!("line {}", i))
19000                .collect::<Vec<_>>()
19001                .join("\n"),
19002        });
19003        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
19004        app.cloudtrail_state.event_json_scroll = 0;
19005
19006        // Down arrow scrolls down
19007        app.handle_action(Action::NextItem);
19008        assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
19009
19010        app.handle_action(Action::NextItem);
19011        assert_eq!(app.cloudtrail_state.event_json_scroll, 2);
19012
19013        // Up arrow scrolls up
19014        app.handle_action(Action::PrevItem);
19015        assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
19016
19017        app.handle_action(Action::PrevItem);
19018        assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
19019
19020        // Up at 0 stays at 0
19021        app.handle_action(Action::PrevItem);
19022        assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
19023    }
19024
19025    #[test]
19026    fn test_cloudtrail_tab_works_with_no_resources() {
19027        let mut app = App::new_without_client("default".to_string(), None);
19028        app.service_selected = true;
19029        app.current_service = Service::CloudTrailEvents;
19030        app.mode = Mode::Normal;
19031        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19032            event_name: "PutObject".to_string(),
19033            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19034            username: "user".to_string(),
19035            event_source: "s3.amazonaws.com".to_string(),
19036            resource_type: "".to_string(), // No resource
19037            resource_name: "".to_string(), // No resource
19038            read_only: "false".to_string(),
19039            aws_region: "us-east-1".to_string(),
19040            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
19041            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
19042            source_ip_address: "192.0.2.1".to_string(),
19043            error_code: "".to_string(),
19044            request_id: "req-123".to_string(),
19045            event_type: "AwsApiCall".to_string(),
19046            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19047        });
19048        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
19049
19050        // Tab cycles to Resources even with no resources
19051        app.handle_action(Action::NextDetailTab);
19052        assert_eq!(
19053            app.cloudtrail_state.detail_focus,
19054            CloudTrailDetailFocus::Resources
19055        );
19056
19057        // Tab cycles back to EventRecord
19058        app.handle_action(Action::NextDetailTab);
19059        assert_eq!(
19060            app.cloudtrail_state.detail_focus,
19061            CloudTrailDetailFocus::EventRecord
19062        );
19063    }
19064
19065    #[test]
19066    fn test_cloudtrail_resources_expand_collapse() {
19067        let mut app = App::new_without_client("default".to_string(), None);
19068        app.service_selected = true;
19069        app.current_service = Service::CloudTrailEvents;
19070        app.mode = Mode::Normal;
19071        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19072            event_name: "PutObject".to_string(),
19073            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19074            username: "user".to_string(),
19075            event_source: "s3.amazonaws.com".to_string(),
19076            resource_type: "AWS::S3::Bucket".to_string(),
19077            resource_name: "my-bucket".to_string(),
19078            read_only: "false".to_string(),
19079            aws_region: "us-east-1".to_string(),
19080            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
19081            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
19082            source_ip_address: "192.0.2.1".to_string(),
19083            error_code: "".to_string(),
19084            request_id: "req-123".to_string(),
19085            event_type: "AwsApiCall".to_string(),
19086            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19087        });
19088        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
19089        app.cloudtrail_state.resources_expanded_index = None;
19090
19091        // Right arrow expands
19092        app.handle_action(Action::ExpandRow);
19093        assert_eq!(app.cloudtrail_state.resources_expanded_index, Some(0));
19094
19095        // Left arrow collapses
19096        app.handle_action(Action::CollapseRow);
19097        assert_eq!(app.cloudtrail_state.resources_expanded_index, None);
19098    }
19099
19100    #[test]
19101    fn test_cloudtrail_event_json_no_column_selector() {
19102        let mut app = App::new_without_client("default".to_string(), None);
19103        app.service_selected = true;
19104        app.current_service = Service::CloudTrailEvents;
19105        app.mode = Mode::Normal;
19106        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19107            event_name: "PutObject".to_string(),
19108            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19109            username: "user".to_string(),
19110            event_source: "s3.amazonaws.com".to_string(),
19111            resource_type: "AWS::S3::Bucket".to_string(),
19112            resource_name: "my-bucket".to_string(),
19113            read_only: "false".to_string(),
19114            aws_region: "us-east-1".to_string(),
19115            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
19116            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
19117            source_ip_address: "192.0.2.1".to_string(),
19118            error_code: "".to_string(),
19119            request_id: "req-123".to_string(),
19120            event_type: "AwsApiCall".to_string(),
19121            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19122        });
19123        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
19124
19125        // 'p' should not open column selector on Event JSON
19126        app.handle_action(Action::OpenColumnSelector);
19127        assert_eq!(app.mode, Mode::Normal);
19128
19129        // But should work on Resources
19130        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
19131        app.handle_action(Action::OpenColumnSelector);
19132        assert_eq!(app.mode, Mode::ColumnSelector);
19133    }
19134
19135    #[test]
19136    fn test_cloudtrail_resources_preferences_show_only_3_columns() {
19137        let mut app = App::new_without_client("default".to_string(), None);
19138        app.service_selected = true;
19139        app.current_service = Service::CloudTrailEvents;
19140        app.mode = Mode::ColumnSelector;
19141        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19142            event_name: "PutObject".to_string(),
19143            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19144            username: "user".to_string(),
19145            event_source: "s3.amazonaws.com".to_string(),
19146            resource_type: "AWS::S3::Bucket".to_string(),
19147            resource_name: "my-bucket".to_string(),
19148            read_only: "false".to_string(),
19149            aws_region: "us-east-1".to_string(),
19150            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
19151            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
19152            source_ip_address: "192.0.2.1".to_string(),
19153            error_code: "".to_string(),
19154            request_id: "req-123".to_string(),
19155            event_type: "AwsApiCall".to_string(),
19156            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19157        });
19158        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
19159
19160        // Should show 3 columns
19161        assert_eq!(app.get_column_count(), 3);
19162        assert_eq!(app.get_column_selector_max(), 3);
19163
19164        // All 3 columns should be visible by default
19165        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 3);
19166
19167        // Toggle first column off
19168        app.column_selector_index = 1;
19169        app.handle_action(Action::ToggleColumn);
19170        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
19171
19172        // Toggle second column off
19173        app.column_selector_index = 2;
19174        app.handle_action(Action::ToggleColumn);
19175        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
19176
19177        // Try to toggle last column off - should NOT work (at least 1 must remain)
19178        app.column_selector_index = 3;
19179        app.handle_action(Action::ToggleColumn);
19180        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
19181
19182        // Toggle first column back on
19183        app.column_selector_index = 1;
19184        app.handle_action(Action::ToggleColumn);
19185        assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
19186    }
19187
19188    #[test]
19189    fn test_cloudtrail_resources_preferences_tab_cycles() {
19190        let mut app = App::new_without_client("default".to_string(), None);
19191        app.service_selected = true;
19192        app.current_service = Service::CloudTrailEvents;
19193        app.mode = Mode::ColumnSelector;
19194        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19195            event_name: "PutObject".to_string(),
19196            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19197            username: "user".to_string(),
19198            event_source: "s3.amazonaws.com".to_string(),
19199            resource_type: "AWS::S3::Bucket".to_string(),
19200            resource_name: "my-bucket".to_string(),
19201            read_only: "false".to_string(),
19202            aws_region: "us-east-1".to_string(),
19203            event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
19204            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
19205            source_ip_address: "192.0.2.1".to_string(),
19206            error_code: "".to_string(),
19207            request_id: "req-123".to_string(),
19208            event_type: "AwsApiCall".to_string(),
19209            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19210        });
19211        app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
19212        app.column_selector_index = 1;
19213
19214        // Tab wraps to start (no page size section)
19215        app.handle_action(Action::NextPreferences);
19216        assert_eq!(app.column_selector_index, 0);
19217
19218        // Shift+Tab wraps to start (no page size section)
19219        app.column_selector_index = 1;
19220        app.handle_action(Action::PrevPreferences);
19221        assert_eq!(app.column_selector_index, 0);
19222    }
19223
19224    #[test]
19225    fn test_cloudtrail_resources_height_stays_constant_when_expanding() {
19226        // This test verifies that table height is pre-allocated for expansion
19227        // so content doesn't jump around when expanding/collapsing
19228
19229        // Height formula: (n_rows + n_visible_cols - 1) + 1 table header + 2 borders + 1 title
19230        // For 1 row with 3 visible columns: (1 + 3 - 1) + 1 + 2 + 1 = 7
19231
19232        // With 3 visible columns
19233        let visible_cols_3 = 3;
19234        let height_3 = (1 + visible_cols_3 - 1 + 1 + 2 + 1) as u16;
19235        assert_eq!(height_3, 7);
19236
19237        // With 2 visible columns (one toggled off)
19238        let visible_cols_2 = 2;
19239        let height_2 = (1 + visible_cols_2 - 1 + 1 + 2 + 1) as u16;
19240        assert_eq!(height_2, 6);
19241
19242        // With 1 visible column (two toggled off)
19243        let visible_cols_1 = 1;
19244        let height_1 = (1 + visible_cols_1 - 1 + 1 + 2 + 1) as u16;
19245        assert_eq!(height_1, 5);
19246    }
19247
19248    #[test]
19249    fn test_cloudtrail_default_focus_is_resources() {
19250        let app = App::new_without_client("default".to_string(), None);
19251        assert_eq!(
19252            app.cloudtrail_state.detail_focus,
19253            CloudTrailDetailFocus::Resources
19254        );
19255    }
19256
19257    fn setup_cloudtrail_pagination_test() -> App {
19258        let mut app = App::new_without_client("default".to_string(), None);
19259        app.service_selected = true;
19260        app.current_service = Service::CloudTrailEvents;
19261        app.mode = Mode::FilterInput;
19262        app.cloudtrail_state.input_focus = InputFocus::Pagination;
19263        app.cloudtrail_state.table.page_size = PageSize::Ten;
19264        app.cloudtrail_state.table.items = (0..25)
19265            .map(|i| CloudTrailEvent {
19266                event_name: format!("Event{}", i),
19267                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19268                username: "user".to_string(),
19269                event_source: "s3.amazonaws.com".to_string(),
19270                resource_type: "Bucket".to_string(),
19271                resource_name: "bucket".to_string(),
19272                read_only: "false".to_string(),
19273                aws_region: "us-east-1".to_string(),
19274                event_id: "id".to_string(),
19275                access_key_id: "key".to_string(),
19276                source_ip_address: "1.2.3.4".to_string(),
19277                error_code: "".to_string(),
19278                request_id: "req".to_string(),
19279                event_type: "AwsApiCall".to_string(),
19280                cloud_trail_event_json: "{}".to_string(),
19281            })
19282            .collect();
19283        app
19284    }
19285
19286    #[test]
19287    fn test_cloudtrail_pagination_navigation() {
19288        // Test right arrow navigation
19289        let mut app = setup_cloudtrail_pagination_test();
19290        assert_eq!(app.cloudtrail_state.table.selected, 0);
19291        app.handle_action(Action::NextItem);
19292        assert_eq!(app.cloudtrail_state.table.selected, 10);
19293        app.handle_action(Action::NextItem);
19294        assert_eq!(app.cloudtrail_state.table.selected, 20);
19295        app.handle_action(Action::NextItem);
19296        assert_eq!(app.cloudtrail_state.table.selected, 20);
19297
19298        // Test left arrow navigation
19299        app.handle_action(Action::PrevItem);
19300        assert_eq!(app.cloudtrail_state.table.selected, 10);
19301        app.handle_action(Action::PrevItem);
19302        assert_eq!(app.cloudtrail_state.table.selected, 0);
19303        app.handle_action(Action::PrevItem);
19304        assert_eq!(app.cloudtrail_state.table.selected, 0);
19305    }
19306
19307    #[test]
19308    fn test_cloudtrail_pagination_navigation_right_arrow() {
19309        let mut app = setup_cloudtrail_pagination_test();
19310        app.current_service = Service::CloudTrailEvents;
19311        app.mode = Mode::FilterInput;
19312        app.cloudtrail_state.input_focus = InputFocus::Pagination;
19313        app.cloudtrail_state.table.page_size = PageSize::Ten;
19314
19315        // Create 25 items (3 pages)
19316        app.cloudtrail_state.table.items = (0..25)
19317            .map(|i| CloudTrailEvent {
19318                event_name: format!("Event{}", i),
19319                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19320                username: "user".to_string(),
19321                event_source: "s3.amazonaws.com".to_string(),
19322                resource_type: "Bucket".to_string(),
19323                resource_name: "bucket".to_string(),
19324                read_only: "false".to_string(),
19325                aws_region: "us-east-1".to_string(),
19326                event_id: "id".to_string(),
19327                access_key_id: "key".to_string(),
19328                source_ip_address: "1.2.3.4".to_string(),
19329                error_code: "".to_string(),
19330                request_id: "req".to_string(),
19331                event_type: "AwsApiCall".to_string(),
19332                cloud_trail_event_json: "{}".to_string(),
19333            })
19334            .collect();
19335
19336        // Start at page 0
19337        assert_eq!(app.cloudtrail_state.table.selected, 0);
19338
19339        // Right arrow should go to page 1
19340        app.handle_action(Action::NextItem);
19341        assert_eq!(app.cloudtrail_state.table.selected, 10);
19342
19343        // Right arrow should go to page 2
19344        app.handle_action(Action::NextItem);
19345        assert_eq!(app.cloudtrail_state.table.selected, 20);
19346
19347        // Right arrow at last page should stay
19348        app.handle_action(Action::NextItem);
19349        assert_eq!(app.cloudtrail_state.table.selected, 20);
19350    }
19351
19352    #[test]
19353    fn test_cloudtrail_pagination_navigation_left_arrow() {
19354        let mut app = App::new_without_client("default".to_string(), None);
19355        app.service_selected = true;
19356        app.current_service = Service::CloudTrailEvents;
19357        app.mode = Mode::FilterInput;
19358        app.cloudtrail_state.input_focus = InputFocus::Pagination;
19359        app.cloudtrail_state.table.page_size = PageSize::Ten;
19360
19361        // Create 25 items (3 pages)
19362        app.cloudtrail_state.table.items = (0..25)
19363            .map(|i| CloudTrailEvent {
19364                event_name: format!("Event{}", i),
19365                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19366                username: "user".to_string(),
19367                event_source: "s3.amazonaws.com".to_string(),
19368                resource_type: "Bucket".to_string(),
19369                resource_name: "bucket".to_string(),
19370                read_only: "false".to_string(),
19371                aws_region: "us-east-1".to_string(),
19372                event_id: "id".to_string(),
19373                access_key_id: "key".to_string(),
19374                source_ip_address: "1.2.3.4".to_string(),
19375                error_code: "".to_string(),
19376                request_id: "req".to_string(),
19377                event_type: "AwsApiCall".to_string(),
19378                cloud_trail_event_json: "{}".to_string(),
19379            })
19380            .collect();
19381
19382        // Start at page 2
19383        app.cloudtrail_state.table.selected = 20;
19384
19385        // Left arrow should go to page 1
19386        app.handle_action(Action::PrevItem);
19387        assert_eq!(app.cloudtrail_state.table.selected, 10);
19388
19389        // Left arrow should go to page 0
19390        app.handle_action(Action::PrevItem);
19391        assert_eq!(app.cloudtrail_state.table.selected, 0);
19392
19393        // Left arrow at first page should stay
19394        app.handle_action(Action::PrevItem);
19395        assert_eq!(app.cloudtrail_state.table.selected, 0);
19396    }
19397
19398    #[test]
19399    fn test_cloudtrail_arrow_navigation() {
19400        let mut app = App::new_without_client("default".to_string(), None);
19401        app.service_selected = true;
19402        app.current_service = Service::CloudTrailEvents;
19403        app.mode = Mode::Normal;
19404
19405        // Create 5 items
19406        app.cloudtrail_state.table.items = (0..5)
19407            .map(|i| CloudTrailEvent {
19408                event_name: format!("Event{}", i),
19409                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19410                username: "user".to_string(),
19411                event_source: "s3.amazonaws.com".to_string(),
19412                resource_type: "Bucket".to_string(),
19413                resource_name: "bucket".to_string(),
19414                read_only: "false".to_string(),
19415                aws_region: "us-east-1".to_string(),
19416                event_id: "id".to_string(),
19417                access_key_id: "key".to_string(),
19418                source_ip_address: "1.2.3.4".to_string(),
19419                error_code: "".to_string(),
19420                request_id: "req".to_string(),
19421                event_type: "AwsApiCall".to_string(),
19422                cloud_trail_event_json: "{}".to_string(),
19423            })
19424            .collect();
19425
19426        // Down arrow should move to next item
19427        app.handle_action(Action::NextItem);
19428        assert_eq!(app.cloudtrail_state.table.selected, 1);
19429
19430        app.handle_action(Action::NextItem);
19431        assert_eq!(app.cloudtrail_state.table.selected, 2);
19432
19433        // Up arrow should move to previous item
19434        app.handle_action(Action::PrevItem);
19435        assert_eq!(app.cloudtrail_state.table.selected, 1);
19436    }
19437
19438    #[test]
19439    fn test_cloudtrail_page_down_navigation() {
19440        let mut app = App::new_without_client("default".to_string(), None);
19441        app.service_selected = true;
19442        app.current_service = Service::CloudTrailEvents;
19443        app.mode = Mode::Normal;
19444        app.cloudtrail_state.table.page_size = PageSize::Ten;
19445
19446        // Create 25 items
19447        app.cloudtrail_state.table.items = (0..25)
19448            .map(|i| CloudTrailEvent {
19449                event_name: format!("Event{}", i),
19450                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19451                username: "user".to_string(),
19452                event_source: "s3.amazonaws.com".to_string(),
19453                resource_type: "Bucket".to_string(),
19454                resource_name: "bucket".to_string(),
19455                read_only: "false".to_string(),
19456                aws_region: "us-east-1".to_string(),
19457                event_id: "id".to_string(),
19458                access_key_id: "key".to_string(),
19459                source_ip_address: "1.2.3.4".to_string(),
19460                error_code: "".to_string(),
19461                request_id: "req".to_string(),
19462                event_type: "AwsApiCall".to_string(),
19463                cloud_trail_event_json: "{}".to_string(),
19464            })
19465            .collect();
19466
19467        // Ctrl+D should jump down a page
19468        app.handle_action(Action::PageDown);
19469        assert_eq!(app.cloudtrail_state.table.selected, 10);
19470
19471        app.handle_action(Action::PageDown);
19472        assert_eq!(app.cloudtrail_state.table.selected, 20);
19473    }
19474
19475    #[test]
19476    fn test_cloudtrail_page_up_navigation() {
19477        let mut app = App::new_without_client("default".to_string(), None);
19478        app.service_selected = true;
19479        app.current_service = Service::CloudTrailEvents;
19480        app.mode = Mode::Normal;
19481        app.cloudtrail_state.table.page_size = PageSize::Ten;
19482
19483        // Create 25 items
19484        app.cloudtrail_state.table.items = (0..25)
19485            .map(|i| CloudTrailEvent {
19486                event_name: format!("Event{}", i),
19487                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19488                username: "user".to_string(),
19489                event_source: "s3.amazonaws.com".to_string(),
19490                resource_type: "Bucket".to_string(),
19491                resource_name: "bucket".to_string(),
19492                read_only: "false".to_string(),
19493                aws_region: "us-east-1".to_string(),
19494                event_id: "id".to_string(),
19495                access_key_id: "key".to_string(),
19496                source_ip_address: "1.2.3.4".to_string(),
19497                error_code: "".to_string(),
19498                request_id: "req".to_string(),
19499                event_type: "AwsApiCall".to_string(),
19500                cloud_trail_event_json: "{}".to_string(),
19501            })
19502            .collect();
19503
19504        // Start at page 2
19505        app.cloudtrail_state.table.selected = 20;
19506
19507        // Ctrl+U should jump up a page
19508        app.handle_action(Action::PageUp);
19509        assert_eq!(app.cloudtrail_state.table.selected, 10);
19510
19511        app.handle_action(Action::PageUp);
19512        assert_eq!(app.cloudtrail_state.table.selected, 0);
19513    }
19514
19515    #[test]
19516    fn test_cloudtrail_page_size_change_updates_display() {
19517        let mut app = App::new_without_client("default".to_string(), None);
19518        app.service_selected = true;
19519        app.current_service = Service::CloudTrailEvents;
19520        app.mode = Mode::ColumnSelector;
19521
19522        // Create 50 items
19523        app.cloudtrail_state.table.items = (0..50)
19524            .map(|i| CloudTrailEvent {
19525                event_name: format!("Event{}", i),
19526                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19527                username: "user".to_string(),
19528                event_source: "s3.amazonaws.com".to_string(),
19529                resource_type: "Bucket".to_string(),
19530                resource_name: "bucket".to_string(),
19531                read_only: "false".to_string(),
19532                aws_region: "us-east-1".to_string(),
19533                event_id: "id".to_string(),
19534                access_key_id: "key".to_string(),
19535                source_ip_address: "1.2.3.4".to_string(),
19536                error_code: "".to_string(),
19537                request_id: "req".to_string(),
19538                event_type: "AwsApiCall".to_string(),
19539                cloud_trail_event_json: "{}".to_string(),
19540            })
19541            .collect();
19542
19543        // Default page size is 50
19544        assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Fifty);
19545
19546        // Change to page size 10 (index 17)
19547        app.column_selector_index = 17;
19548        app.handle_action(Action::ToggleColumn);
19549
19550        assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
19551        // snap_to_page should have been called to adjust display
19552    }
19553
19554    #[test]
19555    fn test_cloudtrail_all_columns_toggleable() {
19556        let mut app = App::new_without_client("default".to_string(), None);
19557        app.service_selected = true;
19558        app.current_service = Service::CloudTrailEvents;
19559        app.mode = Mode::ColumnSelector;
19560
19561        // Verify we have 14 columns
19562        assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
19563
19564        // Test toggling each column (indices 1-14)
19565        // Note: Cannot toggle off the last visible column
19566        for idx in 1..=14 {
19567            app.column_selector_index = idx;
19568            let initial_visible = app.cloudtrail_event_visible_column_ids.clone();
19569            let initial_count = initial_visible.len();
19570
19571            // Check if this column is currently visible
19572            let col = app.cloudtrail_event_column_ids.get(idx - 1).unwrap();
19573            let is_visible = initial_visible.contains(col);
19574
19575            app.handle_action(Action::ToggleColumn);
19576
19577            // If it was the last visible column, it should not be removed
19578            if is_visible && initial_count == 1 {
19579                assert_eq!(
19580                    app.cloudtrail_event_visible_column_ids, initial_visible,
19581                    "Last visible column at index {} should not be toggleable",
19582                    idx
19583                );
19584            } else {
19585                // Otherwise it should toggle
19586                assert_ne!(
19587                    app.cloudtrail_event_visible_column_ids, initial_visible,
19588                    "Column at index {} should be toggleable when not the last one",
19589                    idx
19590                );
19591            }
19592        }
19593    }
19594
19595    #[test]
19596    fn test_cloudtrail_readonly_column_toggleable() {
19597        use crate::cloudtrail::events::CloudTrailEventColumn;
19598
19599        let mut app = App::new_without_client("default".to_string(), None);
19600        app.service_selected = true;
19601        app.current_service = Service::CloudTrailEvents;
19602        app.mode = Mode::ColumnSelector;
19603
19604        // ReadOnly is at position 6 (0-indexed) in the all() array
19605        // So it's at index 7 in the column selector (1-indexed)
19606        let readonly_id = CloudTrailEventColumn::ReadOnly.id();
19607
19608        // Verify ReadOnly is in the column list
19609        assert!(app.cloudtrail_event_column_ids.contains(&readonly_id));
19610
19611        // Initially not visible
19612        assert!(!app
19613            .cloudtrail_event_visible_column_ids
19614            .contains(&readonly_id));
19615
19616        // Toggle it on (index 7)
19617        app.column_selector_index = 7;
19618        app.handle_action(Action::ToggleColumn);
19619
19620        // Should now be visible
19621        assert!(
19622            app.cloudtrail_event_visible_column_ids
19623                .contains(&readonly_id),
19624            "ReadOnly column should be toggleable at index 7"
19625        );
19626
19627        // Toggle it off
19628        app.handle_action(Action::ToggleColumn);
19629
19630        // Should be hidden again
19631        assert!(!app
19632            .cloudtrail_event_visible_column_ids
19633            .contains(&readonly_id));
19634    }
19635
19636    #[test]
19637    fn test_cloudtrail_pagination_limits_displayed_items() {
19638        let mut app = App::new_without_client("default".to_string(), None);
19639        app.service_selected = true;
19640        app.current_service = Service::CloudTrailEvents;
19641
19642        // Create 50 items
19643        app.cloudtrail_state.table.items = (0..50)
19644            .map(|i| CloudTrailEvent {
19645                event_name: format!("Event{}", i),
19646                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19647                username: "user".to_string(),
19648                event_source: "s3.amazonaws.com".to_string(),
19649                resource_type: "Bucket".to_string(),
19650                resource_name: "bucket".to_string(),
19651                read_only: "false".to_string(),
19652                aws_region: "us-east-1".to_string(),
19653                event_id: "id".to_string(),
19654                access_key_id: "key".to_string(),
19655                source_ip_address: "1.2.3.4".to_string(),
19656                error_code: "".to_string(),
19657                request_id: "req".to_string(),
19658                event_type: "AwsApiCall".to_string(),
19659                cloud_trail_event_json: "{}".to_string(),
19660            })
19661            .collect();
19662
19663        // Set page size to 10
19664        app.cloudtrail_state.table.page_size = PageSize::Ten;
19665
19666        // Render to a test backend to verify pagination
19667        use ratatui::backend::TestBackend;
19668        use ratatui::Terminal;
19669
19670        let backend = TestBackend::new(100, 30);
19671        let mut terminal = Terminal::new(backend).unwrap();
19672
19673        terminal
19674            .draw(|frame| {
19675                let area = frame.area();
19676                crate::ui::cloudtrail::render_events(frame, &app, area);
19677            })
19678            .unwrap();
19679
19680        // The rendering should only show 10 items per page
19681        // This is verified by the pagination logic in render_events
19682        assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
19683    }
19684
19685    #[test]
19686    fn test_cloudtrail_readonly_column_selectable_in_preferences() {
19687        let mut app = App::new_without_client("default".to_string(), None);
19688        app.service_selected = true;
19689        app.current_service = Service::CloudTrailEvents;
19690        app.mode = Mode::ColumnSelector;
19691
19692        // Verify we have 14 columns
19693        assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
19694
19695        // Navigate to Read-only column (index 7)
19696        app.column_selector_index = 7;
19697
19698        // Verify we can select it by checking the index is valid
19699        let max_index = app.get_column_selector_max();
19700        assert!(
19701            app.column_selector_index <= max_index,
19702            "Read-only column index {} should be <= max {}",
19703            app.column_selector_index,
19704            max_index
19705        );
19706
19707        // Verify the column exists at this index
19708        let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
19709        let col = CloudTrailEventColumn::from_id(col_id);
19710        assert!(col.is_some(), "Column should exist at index 7");
19711        assert_eq!(
19712            col.unwrap().default_name(),
19713            "Read-only",
19714            "Column at index 7 should be Read-only"
19715        );
19716    }
19717
19718    #[test]
19719    fn test_cloudtrail_navigate_to_readonly_column() {
19720        let mut app = App::new_without_client("default".to_string(), None);
19721        app.service_selected = true;
19722        app.current_service = Service::CloudTrailEvents;
19723        app.mode = Mode::ColumnSelector;
19724        app.column_selector_index = 1; // Start at first column
19725
19726        // Check column IDs are initialized
19727        println!(
19728            "cloudtrail_event_column_ids.len() = {}",
19729            app.cloudtrail_event_column_ids.len()
19730        );
19731        assert_eq!(
19732            app.cloudtrail_event_column_ids.len(),
19733            14,
19734            "Should have 14 columns"
19735        );
19736
19737        // Check blank row index
19738        let column_count = app.get_column_count();
19739        println!("Column count from get_column_count(): {}", column_count);
19740        assert_eq!(column_count, 14, "Column count should be 14");
19741
19742        // The blank row should be at index 15 (14 columns + 1)
19743        assert!(!app.is_blank_row_index(7), "Index 7 should NOT be blank");
19744        assert!(app.is_blank_row_index(15), "Index 15 should be blank");
19745
19746        // Navigate down to Read-only column (index 7)
19747        for _ in 0..6 {
19748            app.handle_action(Action::NextItem);
19749        }
19750
19751        assert_eq!(
19752            app.column_selector_index, 7,
19753            "Should navigate to Read-only column at index 7"
19754        );
19755
19756        // Verify it's the Read-only column
19757        let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
19758        let col = CloudTrailEventColumn::from_id(col_id).unwrap();
19759        assert_eq!(col.default_name(), "Read-only");
19760    }
19761
19762    #[test]
19763    fn test_cloudtrail_navigate_beyond_loaded_items() {
19764        let mut app = App::new_without_client("default".to_string(), None);
19765        app.service_selected = true;
19766        app.current_service = Service::CloudTrailEvents;
19767
19768        // Load only 50 items
19769        app.cloudtrail_state.table.items = (0..50)
19770            .map(|i| CloudTrailEvent {
19771                event_name: format!("Event{}", i),
19772                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19773                username: "user".to_string(),
19774                event_source: "s3.amazonaws.com".to_string(),
19775                resource_type: "Bucket".to_string(),
19776                resource_name: "bucket".to_string(),
19777                read_only: "false".to_string(),
19778                aws_region: "us-east-1".to_string(),
19779                event_id: "id".to_string(),
19780                access_key_id: "key".to_string(),
19781                source_ip_address: "1.2.3.4".to_string(),
19782                error_code: "".to_string(),
19783                request_id: "req".to_string(),
19784                event_type: "AwsApiCall".to_string(),
19785                cloud_trail_event_json: "{}".to_string(),
19786            })
19787            .collect();
19788
19789        // Page size is 50 by default, so we have 1 page of data
19790        // Try to navigate to page 10 (beyond loaded data)
19791        let initial_selected = app.cloudtrail_state.table.selected;
19792        app.page_input = "10".to_string();
19793        app.go_to_page(10);
19794
19795        // Should do nothing (stay at initial position)
19796        assert_eq!(
19797            app.cloudtrail_state.table.selected, initial_selected,
19798            "Should ignore navigation to page 10 when only 1 page is loaded"
19799        );
19800
19801        // But page 2 should work (loaded + 1)
19802        app.go_to_page(2);
19803        let page_size = app.cloudtrail_state.table.page_size.value();
19804        assert_eq!(
19805            app.cloudtrail_state.table.selected, page_size,
19806            "Should allow navigation to page 2 (loaded + 1)"
19807        );
19808    }
19809
19810    #[test]
19811    fn test_cloudtrail_page_change_resets_expansion() {
19812        let mut app = App::new_without_client("default".to_string(), None);
19813        app.service_selected = true;
19814        app.current_service = Service::CloudTrailEvents;
19815
19816        // Create 100 items (2 pages with page size 50)
19817        app.cloudtrail_state.table.items = (0..100)
19818            .map(|i| CloudTrailEvent {
19819                event_name: format!("Event{}", i),
19820                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19821                username: "user".to_string(),
19822                event_source: "s3.amazonaws.com".to_string(),
19823                resource_type: "Bucket".to_string(),
19824                resource_name: "bucket".to_string(),
19825                read_only: "false".to_string(),
19826                aws_region: "us-east-1".to_string(),
19827                event_id: "id".to_string(),
19828                access_key_id: "key".to_string(),
19829                source_ip_address: "1.2.3.4".to_string(),
19830                error_code: "".to_string(),
19831                request_id: "req".to_string(),
19832                event_type: "AwsApiCall".to_string(),
19833                cloud_trail_event_json: "{}".to_string(),
19834            })
19835            .collect();
19836
19837        // Expand an item on page 1
19838        app.cloudtrail_state.table.expanded_item = Some(5);
19839        assert_eq!(app.cloudtrail_state.table.expanded_item, Some(5));
19840
19841        // Navigate to page 2
19842        app.go_to_page(2);
19843
19844        // Expansion should be reset
19845        assert_eq!(
19846            app.cloudtrail_state.table.expanded_item, None,
19847            "Page change should reset expanded_item"
19848        );
19849    }
19850
19851    #[test]
19852    fn test_cloudtrail_pagination_right_left_arrow_works_when_filter_focused() {
19853        // Regression: Left/Right arrows map to PageUp/PageDown in FilterInput mode.
19854        // page_down_filter_input called handle_page_down which only acts when
19855        // input_focus == Pagination. When filter is focused, it was a no-op.
19856        let mut app = App::new_without_client("default".to_string(), None);
19857        app.service_selected = true;
19858        app.current_service = Service::CloudTrailEvents;
19859        app.mode = Mode::FilterInput;
19860        // Focus is on the FILTER text input (not pagination)
19861        app.cloudtrail_state.input_focus = InputFocus::Filter;
19862        app.cloudtrail_state.table.page_size = PageSize::Ten;
19863
19864        app.cloudtrail_state.table.items = (0..25)
19865            .map(|i| crate::cloudtrail::CloudTrailEvent {
19866                event_name: format!("Event{}", i),
19867                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19868                username: "user".to_string(),
19869                event_source: "s3.amazonaws.com".to_string(),
19870                resource_type: "Bucket".to_string(),
19871                resource_name: "bucket".to_string(),
19872                read_only: "false".to_string(),
19873                aws_region: "us-east-1".to_string(),
19874                event_id: format!("id-{}", i),
19875                access_key_id: "key".to_string(),
19876                source_ip_address: "1.2.3.4".to_string(),
19877                error_code: String::new(),
19878                request_id: "req".to_string(),
19879                event_type: "AwsApiCall".to_string(),
19880                cloud_trail_event_json: "{}".to_string(),
19881            })
19882            .collect();
19883
19884        assert_eq!(app.cloudtrail_state.table.selected, 0, "starts on page 1");
19885
19886        // Right arrow (PageDown) must go to page 2 even when filter is focused
19887        app.handle_action(Action::PageDown);
19888        assert_eq!(
19889            app.cloudtrail_state.table.selected, 10,
19890            "Right arrow must advance to page 2 even when filter text box is focused"
19891        );
19892
19893        // Left arrow (PageUp) must go back to page 1
19894        app.handle_action(Action::PageUp);
19895        assert_eq!(
19896            app.cloudtrail_state.table.selected, 0,
19897            "Left arrow must go back to page 1 even when filter text box is focused"
19898        );
19899    }
19900
19901    #[test]
19902    fn test_cloudtrail_yank_in_event_detail_copies_json() {
19903        // y key in event detail view must copy cloud_trail_event_json to clipboard.
19904        let mut app = test_app();
19905        app.current_service = Service::CloudTrailEvents;
19906        app.service_selected = true;
19907        app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19908            event_name: "PutObject".to_string(),
19909            event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19910            username: "alice".to_string(),
19911            event_source: "s3.amazonaws.com".to_string(),
19912            resource_type: "Bucket".to_string(),
19913            resource_name: "my-bucket".to_string(),
19914            read_only: "false".to_string(),
19915            aws_region: "us-east-1".to_string(),
19916            event_id: "evt-001".to_string(),
19917            access_key_id: "AKIA".to_string(),
19918            source_ip_address: "1.2.3.4".to_string(),
19919            error_code: String::new(),
19920            request_id: "req-001".to_string(),
19921            event_type: "AwsApiCall".to_string(),
19922            cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19923        });
19924
19925        // yank must not panic — we can't assert clipboard contents in tests
19926        // but verify the function is reachable
19927        app.handle_action(Action::Yank);
19928        // event must still be open after yank (yank is non-destructive)
19929        assert!(
19930            app.cloudtrail_state.current_event.is_some(),
19931            "Event must remain open after yank"
19932        );
19933    }
19934
19935    #[test]
19936    fn test_cloudtrail_filter_reduces_visible_events() {
19937        // Regression: render_events was collecting all items without applying
19938        // cloudtrail_state.table.filter — search had no visible effect.
19939        // We test the filter application directly via filtered_events helper
19940        // (the render path is tested via UI tests; here we test the data path).
19941        let mut app = test_app();
19942        app.current_service = Service::CloudTrailEvents;
19943        app.service_selected = true;
19944
19945        let items: Vec<CloudTrailEvent> = vec![
19946            CloudTrailEvent {
19947                event_name: "PutObject".to_string(),
19948                event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19949                username: "alice".to_string(),
19950                event_source: "s3.amazonaws.com".to_string(),
19951                resource_type: "Bucket".to_string(),
19952                resource_name: "my-bucket".to_string(),
19953                read_only: "false".to_string(),
19954                aws_region: "us-east-1".to_string(),
19955                event_id: "evt-001".to_string(),
19956                access_key_id: "AKIA".to_string(),
19957                source_ip_address: "1.2.3.4".to_string(),
19958                error_code: String::new(),
19959                request_id: "req-001".to_string(),
19960                event_type: "AwsApiCall".to_string(),
19961                cloud_trail_event_json: "{}".to_string(),
19962            },
19963            CloudTrailEvent {
19964                event_name: "GetObject".to_string(),
19965                event_time: "2024-01-01 11:00:00 (UTC)".to_string(),
19966                username: "bob".to_string(),
19967                event_source: "s3.amazonaws.com".to_string(),
19968                resource_type: "Object".to_string(),
19969                resource_name: "my-key".to_string(),
19970                read_only: "true".to_string(),
19971                aws_region: "us-east-1".to_string(),
19972                event_id: "evt-002".to_string(),
19973                access_key_id: "AKIA2".to_string(),
19974                source_ip_address: "5.6.7.8".to_string(),
19975                error_code: String::new(),
19976                request_id: "req-002".to_string(),
19977                event_type: "AwsApiCall".to_string(),
19978                cloud_trail_event_json: "{}".to_string(),
19979            },
19980        ];
19981        app.cloudtrail_state.table.items = items;
19982
19983        // No filter — both events visible
19984        let q = app.cloudtrail_state.table.filter.to_lowercase();
19985        let filtered: Vec<_> = app
19986            .cloudtrail_state
19987            .table
19988            .items
19989            .iter()
19990            .filter(|e| {
19991                q.is_empty()
19992                    || e.event_name.to_lowercase().contains(&q)
19993                    || e.username.to_lowercase().contains(&q)
19994            })
19995            .collect();
19996        assert_eq!(
19997            filtered.len(),
19998            2,
19999            "No filter: both events should be visible"
20000        );
20001
20002        // Filter by event name
20003        app.cloudtrail_state.table.filter = "PutObject".to_string();
20004        let q = app.cloudtrail_state.table.filter.to_lowercase();
20005        let filtered: Vec<_> = app
20006            .cloudtrail_state
20007            .table
20008            .items
20009            .iter()
20010            .filter(|e| {
20011                q.is_empty()
20012                    || e.event_name.to_lowercase().contains(&q)
20013                    || e.username.to_lowercase().contains(&q)
20014            })
20015            .collect();
20016        assert_eq!(
20017            filtered.len(),
20018            1,
20019            "Filter 'PutObject' should show only 1 event"
20020        );
20021        assert_eq!(filtered[0].event_name, "PutObject");
20022
20023        // Filter by username
20024        app.cloudtrail_state.table.filter = "bob".to_string();
20025        let q = app.cloudtrail_state.table.filter.to_lowercase();
20026        let filtered: Vec<_> = app
20027            .cloudtrail_state
20028            .table
20029            .items
20030            .iter()
20031            .filter(|e| {
20032                q.is_empty()
20033                    || e.event_name.to_lowercase().contains(&q)
20034                    || e.username.to_lowercase().contains(&q)
20035            })
20036            .collect();
20037        assert_eq!(filtered.len(), 1, "Filter 'bob' should show only 1 event");
20038        assert_eq!(filtered[0].username, "bob");
20039    }
20040
20041    #[test]
20042    fn test_lambda_application_resources_have_columns() {
20043        let app = test_app();
20044
20045        // Verify lambda resource columns are initialized correctly
20046        assert_eq!(app.lambda_resource_visible_column_ids.len(), 4);
20047        assert_eq!(app.lambda_resource_column_ids.len(), 4);
20048
20049        // Verify they contain the expected Lambda ResourceColumn IDs
20050        assert!(app
20051            .lambda_resource_visible_column_ids
20052            .contains(&"column.lambda.resource.logical_id"));
20053        assert!(app
20054            .lambda_resource_column_ids
20055            .contains(&"column.lambda.resource.logical_id"));
20056    }
20057
20058    #[test]
20059    fn test_lambda_functions_list_right_expands_left_collapses() {
20060        use crate::lambda::Function;
20061        let mut app = test_app();
20062        app.mode = Mode::Normal;
20063        app.current_service = Service::LambdaFunctions;
20064        app.service_selected = true;
20065        app.lambda_state.table.items = vec![Function {
20066            name: "fn1".to_string(),
20067            arn: "arn::fn1".to_string(),
20068            application: None,
20069            description: String::new(),
20070            package_type: String::new(),
20071            runtime: "python3.12".to_string(),
20072            architecture: String::new(),
20073            code_size: 0,
20074            code_sha256: String::new(),
20075            memory_mb: 128,
20076            timeout_seconds: 3,
20077            last_modified: String::new(),
20078            layers: vec![],
20079        }];
20080        app.lambda_state.table.selected = 0;
20081
20082        // Right arrow should expand
20083        app.handle_action(Action::NextPane);
20084        assert_eq!(app.lambda_state.table.expanded_item, Some(0));
20085
20086        // Right arrow again should NOT collapse (stays expanded)
20087        app.handle_action(Action::NextPane);
20088        assert_eq!(app.lambda_state.table.expanded_item, Some(0));
20089
20090        // Left arrow should collapse
20091        app.handle_action(Action::CollapseRow);
20092        assert_eq!(app.lambda_state.table.expanded_item, None);
20093    }
20094
20095    #[test]
20096    fn test_lambda_applications_list_right_expands_left_collapses() {
20097        use crate::lambda::Application;
20098        let mut app = test_app();
20099        app.mode = Mode::Normal;
20100        app.current_service = Service::LambdaApplications;
20101        app.service_selected = true;
20102        app.lambda_application_state.table.items = vec![Application {
20103            name: "my-app".to_string(),
20104            arn: String::new(),
20105            description: String::new(),
20106            status: "ACTIVE".to_string(),
20107            last_modified: String::new(),
20108        }];
20109        app.lambda_application_state.table.selected = 0;
20110
20111        // Right arrow should expand
20112        app.handle_action(Action::NextPane);
20113        assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
20114
20115        // Right arrow again should NOT collapse (stays expanded)
20116        app.handle_action(Action::NextPane);
20117        assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
20118
20119        // Left arrow should collapse
20120        app.handle_action(Action::CollapseRow);
20121        assert_eq!(app.lambda_application_state.table.expanded_item, None);
20122    }
20123
20124    #[test]
20125    fn test_lambda_application_enter_on_deployments_tab_does_not_switch_to_overview() {
20126        use crate::lambda::Application;
20127        let mut app = test_app();
20128        app.mode = Mode::Normal;
20129        app.current_service = Service::LambdaApplications;
20130        app.service_selected = true;
20131        app.lambda_application_state.table.items = vec![Application {
20132            name: "my-app".to_string(),
20133            arn: String::new(),
20134            description: String::new(),
20135            status: "ACTIVE".to_string(),
20136            last_modified: String::new(),
20137        }];
20138
20139        // Select the application (Enter on list)
20140        app.handle_action(Action::Select);
20141        assert!(app.lambda_application_state.current_application.is_some());
20142
20143        // Switch to Deployments tab
20144        use crate::ui::lambda::ApplicationDetailTab as LambdaApplicationDetailTab;
20145        app.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Deployments;
20146        assert_eq!(
20147            app.lambda_application_state.detail_tab,
20148            LambdaApplicationDetailTab::Deployments
20149        );
20150
20151        // Enter on Deployments tab must NOT switch back to Overview
20152        app.handle_action(Action::Select);
20153        assert_eq!(
20154            app.lambda_application_state.detail_tab,
20155            LambdaApplicationDetailTab::Deployments
20156        );
20157    }
20158
20159    #[test]
20160    fn test_lambda_applications_pagination_focus_left_right_jump_pages() {
20161        use crate::lambda::Application;
20162        let mut app = test_app();
20163        app.current_service = Service::LambdaApplications;
20164        app.service_selected = true;
20165        app.mode = Mode::FilterInput;
20166        app.lambda_application_state.input_focus = InputFocus::Pagination;
20167
20168        // 100 apps, page size 50 — 2 pages
20169        app.lambda_application_state.table.items = (0..100)
20170            .map(|i| Application {
20171                name: format!("app{:03}", i),
20172                arn: String::new(),
20173                description: String::new(),
20174                status: "ACTIVE".to_string(),
20175                last_modified: String::new(),
20176            })
20177            .collect();
20178        app.lambda_application_state.table.selected = 0;
20179
20180        // Right arrow (PageDown) must jump to page 2
20181        app.handle_action(Action::PageDown);
20182        assert_eq!(
20183            app.lambda_application_state.table.selected, 50,
20184            "Right arrow with pagination focus should jump to page 2"
20185        );
20186
20187        // Left arrow (PageUp) must jump back to page 1
20188        app.handle_action(Action::PageUp);
20189        assert_eq!(
20190            app.lambda_application_state.table.selected, 0,
20191            "Left arrow with pagination focus should jump back to page 1"
20192        );
20193
20194        // Up/Down arrows must NOT move table selection when pagination is focused
20195        app.handle_action(Action::NextItem);
20196        assert_eq!(
20197            app.lambda_application_state.table.selected, 0,
20198            "Down arrow must not move table when pagination is focused"
20199        );
20200        app.lambda_application_state.table.selected = 5;
20201        app.handle_action(Action::PrevItem);
20202        assert_eq!(
20203            app.lambda_application_state.table.selected, 5,
20204            "Up arrow must not move table when pagination is focused"
20205        );
20206    }
20207
20208    #[test]
20209    fn test_cloudwatch_alarms_enter_drills_down() {
20210        let mut app = test_app();
20211        app.mode = Mode::Normal;
20212        app.current_service = Service::CloudWatchAlarms;
20213        app.service_selected = true;
20214        app.view_mode = ViewMode::List;
20215
20216        // Add test alarm
20217        app.alarms_state.table.items = vec![Alarm {
20218            name: "test-alarm".to_string(),
20219            state: "ALARM".to_string(),
20220            state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
20221            description: "Test alarm".to_string(),
20222            metric_name: "CPUUtilization".to_string(),
20223            namespace: "AWS/EC2".to_string(),
20224            statistic: "Average".to_string(),
20225            period: 300,
20226            comparison_operator: "GreaterThanThreshold".to_string(),
20227            threshold: 80.0,
20228            actions_enabled: true,
20229            state_reason: "Threshold crossed".to_string(),
20230            resource: "".to_string(),
20231            dimensions: "".to_string(),
20232            expression: "".to_string(),
20233            alarm_type: "MetricAlarm".to_string(),
20234            cross_account: "".to_string(),
20235            ..Default::default()
20236        }];
20237
20238        assert!(app.alarms_state.current_alarm.is_none());
20239        assert_eq!(app.view_mode, ViewMode::List);
20240        assert!(!app.alarms_state.metrics_loading);
20241
20242        // Press Enter - should drill into alarm and trigger metrics loading
20243        app.handle_action(Action::Select);
20244
20245        assert_eq!(
20246            app.alarms_state.current_alarm,
20247            Some("test-alarm".to_string())
20248        );
20249        assert_eq!(app.view_mode, ViewMode::Detail);
20250        assert!(
20251            app.alarms_state.metrics_loading,
20252            "Should trigger metrics loading"
20253        );
20254    }
20255
20256    #[test]
20257    fn test_cloudwatch_alarms_metric_data_renders() {
20258        let mut app = test_app();
20259        app.mode = Mode::Normal;
20260        app.current_service = Service::CloudWatchAlarms;
20261        app.service_selected = true;
20262        app.view_mode = ViewMode::Detail;
20263
20264        // Add test alarm
20265        app.alarms_state.table.items = vec![Alarm {
20266            name: "test-alarm".to_string(),
20267            state: "ALARM".to_string(),
20268            state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
20269            description: "Test alarm".to_string(),
20270            metric_name: "CPUUtilization".to_string(),
20271            namespace: "AWS/EC2".to_string(),
20272            statistic: "Average".to_string(),
20273            period: 300,
20274            comparison_operator: "GreaterThanThreshold".to_string(),
20275            threshold: 80.0,
20276            actions_enabled: true,
20277            state_reason: "Threshold crossed".to_string(),
20278            resource: "".to_string(),
20279            dimensions: "".to_string(),
20280            expression: "".to_string(),
20281            alarm_type: "MetricAlarm".to_string(),
20282            cross_account: "".to_string(),
20283            ..Default::default()
20284        }];
20285
20286        app.alarms_state.current_alarm = Some("test-alarm".to_string());
20287
20288        // Add metric data
20289        app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0), (3000, 70.0)];
20290        app.alarms_state.metrics_loading = false;
20291
20292        // Verify metric data is present and will be rendered
20293        assert!(!app.alarms_state.metric_data.is_empty());
20294        assert_eq!(app.alarms_state.metric_data.len(), 3);
20295        assert!(!app.alarms_state.metrics_loading);
20296    }
20297
20298    #[test]
20299    fn test_cloudwatch_alarms_back_clears_metrics() {
20300        let mut app = test_app();
20301        app.mode = Mode::Normal;
20302        app.current_service = Service::CloudWatchAlarms;
20303        app.service_selected = true;
20304        app.view_mode = ViewMode::Detail;
20305
20306        // Set up alarm detail view with metrics
20307        app.alarms_state.current_alarm = Some("test-alarm".to_string());
20308        app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
20309
20310        assert!(!app.alarms_state.metric_data.is_empty());
20311
20312        // Go back
20313        app.handle_action(Action::GoBack);
20314
20315        // Should clear current alarm and metrics
20316        assert!(app.alarms_state.current_alarm.is_none());
20317        assert!(app.alarms_state.metric_data.is_empty());
20318        assert_eq!(app.view_mode, ViewMode::List);
20319    }
20320
20321    #[test]
20322    fn test_cloudwatch_alarms_refresh_reloads_metrics() {
20323        let mut app = test_app();
20324        app.mode = Mode::Normal;
20325        app.current_service = Service::CloudWatchAlarms;
20326        app.service_selected = true;
20327        app.view_mode = ViewMode::Detail;
20328
20329        // Set up alarm detail view with old metrics
20330        app.alarms_state.current_alarm = Some("test-alarm".to_string());
20331        app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
20332        app.alarms_state.metrics_loading = false;
20333
20334        assert!(!app.alarms_state.metric_data.is_empty());
20335        assert!(!app.alarms_state.metrics_loading);
20336
20337        // Refresh with Ctrl+R
20338        app.handle_action(Action::Refresh);
20339
20340        // Should keep old metrics visible until new data arrives
20341        assert!(!app.alarms_state.metric_data.is_empty());
20342        assert!(app.alarms_state.metrics_loading);
20343    }
20344
20345    #[test]
20346    fn test_cloudwatch_alarms_right_arrow_expands() {
20347        let mut app = test_app();
20348        app.mode = Mode::Normal;
20349        app.current_service = Service::CloudWatchAlarms;
20350        app.service_selected = true;
20351        app.view_mode = ViewMode::List;
20352
20353        // Add test alarm
20354        app.alarms_state.table.items = vec![Alarm {
20355            name: "test-alarm".to_string(),
20356            state: "ALARM".to_string(),
20357            state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
20358            description: "Test alarm".to_string(),
20359            metric_name: "CPUUtilization".to_string(),
20360            namespace: "AWS/EC2".to_string(),
20361            statistic: "Average".to_string(),
20362            period: 300,
20363            comparison_operator: "GreaterThanThreshold".to_string(),
20364            threshold: 80.0,
20365            actions_enabled: true,
20366            state_reason: "Threshold crossed".to_string(),
20367            resource: "".to_string(),
20368            dimensions: "".to_string(),
20369            expression: "".to_string(),
20370            alarm_type: "MetricAlarm".to_string(),
20371            cross_account: "".to_string(),
20372            ..Default::default()
20373        }];
20374
20375        assert!(!app.alarms_state.table.is_expanded());
20376
20377        // Right arrow should expand
20378        app.handle_action(Action::NextPane);
20379        assert!(app.alarms_state.table.is_expanded());
20380
20381        // Left arrow should collapse
20382        app.handle_action(Action::PrevPane);
20383        assert!(!app.alarms_state.table.is_expanded());
20384    }
20385
20386    #[test]
20387    fn test_cloudwatch_alarms_tab_switches_tabs() {
20388        let mut app = test_app();
20389        app.mode = Mode::Normal;
20390        app.current_service = Service::CloudWatchAlarms;
20391        app.service_selected = true;
20392
20393        // Start on All Alarms tab
20394        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
20395
20396        // Tab switches to In Alarm
20397        app.handle_action(Action::NextDetailTab);
20398        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
20399
20400        // Tab again switches back to All Alarms
20401        app.handle_action(Action::NextDetailTab);
20402        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
20403
20404        // Shift+Tab switches to In Alarm
20405        app.handle_action(Action::PrevDetailTab);
20406        assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
20407    }
20408
20409    #[test]
20410    fn test_cloudwatch_alarms_pagination_changes_table_contents() {
20411        // Regression: alarms render was passing all items to render_table without
20412        // slicing to the current page, so pagination only moved selection but
20413        // never changed what was displayed.
20414        let mut app = test_app();
20415        app.current_service = Service::CloudWatchAlarms;
20416        app.service_selected = true;
20417        app.mode = Mode::FilterInput;
20418        app.alarms_state.input_focus = InputFocus::Pagination;
20419        app.alarms_state.table.page_size = crate::common::PageSize::Ten;
20420
20421        // Create 25 alarms
20422        app.alarms_state.table.items = (0..25)
20423            .map(|i| crate::cw::Alarm {
20424                name: format!("alarm-{:02}", i),
20425                state: "OK".to_string(),
20426                state_updated_timestamp: String::new(),
20427                description: String::new(),
20428                metric_name: String::new(),
20429                namespace: String::new(),
20430                statistic: String::new(),
20431                period: 60,
20432                comparison_operator: String::new(),
20433                threshold: 0.0,
20434                actions_enabled: false,
20435                state_reason: String::new(),
20436                resource: String::new(),
20437                dimensions: String::new(),
20438                expression: String::new(),
20439                alarm_type: String::new(),
20440                cross_account: String::new(),
20441                ..Default::default()
20442            })
20443            .collect();
20444
20445        assert_eq!(app.alarms_state.table.selected, 0, "starts on page 1");
20446
20447        // Right arrow (PageDown) in FilterInput+Pagination must advance to page 2
20448        app.handle_action(Action::PageDown);
20449        assert_eq!(
20450            app.alarms_state.table.selected, 10,
20451            "Right arrow must advance selected to page 2"
20452        );
20453
20454        // Left arrow (PageUp) must go back
20455        app.handle_action(Action::PageUp);
20456        assert_eq!(
20457            app.alarms_state.table.selected, 0,
20458            "Left arrow must go back"
20459        );
20460
20461        // go_to_page must also work correctly
20462        app.go_to_page(3);
20463        assert_eq!(
20464            app.alarms_state.table.selected, 20,
20465            "go_to_page(3) must set selected to start of page 3"
20466        );
20467    }
20468
20469    #[test]
20470    fn test_cloudwatch_alarms_num_p_page_jump() {
20471        // <num>p must jump to that page: digits accumulate in page_input,
20472        // then p (OpenColumnSelector) applies the page jump.
20473        let mut app = test_app();
20474        app.current_service = Service::CloudWatchAlarms;
20475        app.service_selected = true;
20476        app.mode = Mode::Normal;
20477        app.alarms_state.table.page_size = crate::common::PageSize::Ten;
20478        app.alarms_state.table.items = (0..25)
20479            .map(|i| crate::cw::Alarm {
20480                name: format!("alarm-{:02}", i),
20481                state: "OK".to_string(),
20482                state_updated_timestamp: String::new(),
20483                description: String::new(),
20484                metric_name: String::new(),
20485                namespace: String::new(),
20486                statistic: String::new(),
20487                period: 60,
20488                comparison_operator: String::new(),
20489                threshold: 0.0,
20490                actions_enabled: false,
20491                state_reason: String::new(),
20492                resource: String::new(),
20493                dimensions: String::new(),
20494                expression: String::new(),
20495                alarm_type: String::new(),
20496                cross_account: String::new(),
20497                ..Default::default()
20498            })
20499            .collect();
20500
20501        // Type '2' then 'p' → jump to page 2
20502        app.handle_action(Action::FilterInput('2'));
20503        assert_eq!(app.page_input, "2");
20504        app.handle_action(Action::OpenColumnSelector);
20505        assert_eq!(
20506            app.alarms_state.table.selected, 10,
20507            "2p must jump to page 2 (selected=10)"
20508        );
20509        assert_eq!(app.page_input, "", "page_input must be cleared after jump");
20510        // mode stays Normal (not ColumnSelector) since page_input was consumed
20511        assert_ne!(
20512            app.mode,
20513            Mode::ColumnSelector,
20514            "mode must not open column selector when page_input was set"
20515        );
20516    }
20517
20518    #[test]
20519    fn test_cloudwatch_alarms_console_url_list_view() {
20520        let url = crate::cw::alarms::console_url("us-east-1", None);
20521        assert!(url.contains("alarmsV2"), "must contain alarmsV2");
20522        assert!(
20523            !url.contains("table") && !url.contains("card"),
20524            "must not embed view_mode"
20525        );
20526    }
20527
20528    #[test]
20529    fn test_cloudwatch_alarms_console_url_alarm_detail() {
20530        let url = crate::cw::alarms::console_url("us-east-1", Some("My-Alarm"));
20531        assert!(
20532            url.contains("alarmsV2:alarm/My-Alarm"),
20533            "must link directly to alarm, got: {url}"
20534        );
20535    }
20536
20537    #[test]
20538    fn test_cloudwatch_alarms_console_url_from_list_selected_alarm() {
20539        // Regression: ^o from list view with expanded row was returning the list URL
20540        // because current_alarm was None. Should use the selected alarm's name.
20541        let mut app = test_app();
20542        app.current_service = Service::CloudWatchAlarms;
20543        app.service_selected = true;
20544        app.alarms_state.current_alarm = None; // list view, not drilled in
20545        app.alarms_state.table.items = vec![
20546            crate::cw::Alarm {
20547                name: "alarm-0".to_string(),
20548                ..Default::default()
20549            },
20550            crate::cw::Alarm {
20551                name: "alarm-1".to_string(),
20552                ..Default::default()
20553            },
20554        ];
20555        app.alarms_state.table.selected = 1; // second alarm selected
20556
20557        let url = app.get_console_url();
20558        assert!(
20559            url.contains("alarmsV2:alarm/alarm-1"),
20560            "^o from list view must link to selected alarm, got: {url}"
20561        );
20562    }
20563
20564    #[test]
20565    fn test_cloudwatch_alarms_metric_math_alarm_does_not_error() {
20566        // Regression: alarms with empty statistic (metric math expressions) caused
20567        // InvalidParameterValueException when trying to call GetMetricStatistics.
20568        // The fix: skip the API call when statistic is empty.
20569        let alarm = crate::cw::Alarm {
20570            name: "math-alarm".to_string(),
20571            state: "OK".to_string(),
20572            state_updated_timestamp: String::new(),
20573            description: String::new(),
20574            metric_name: String::new(), // empty for metric math
20575            namespace: String::new(),   // empty for metric math
20576            statistic: String::new(),   // empty for metric math
20577            period: 60,
20578            comparison_operator: "GreaterThanOrEqualToThreshold".to_string(),
20579            threshold: 1.0,
20580            actions_enabled: true,
20581            state_reason: String::new(),
20582            resource: String::new(),
20583            dimensions: String::new(),
20584            expression: "SUM([m1, m2])".to_string(),
20585            alarm_type: "expression".to_string(),
20586            cross_account: String::new(),
20587            ..Default::default()
20588        };
20589        // The fix: when statistic is empty, we should NOT call get_metric_statistics
20590        assert!(
20591            alarm.statistic.is_empty(),
20592            "Metric math alarm must have empty statistic"
20593        );
20594        assert!(
20595            alarm.metric_name.is_empty(),
20596            "Metric math alarm must have empty metric_name"
20597        );
20598        // Verify the guard condition: statistic.is_empty() || metric_name.is_empty()
20599        // This is the condition checked in main.rs before calling get_metric_statistics
20600        let should_skip = alarm.statistic.is_empty() || alarm.metric_name.is_empty();
20601        assert!(
20602            should_skip,
20603            "Should skip get_metric_statistics for metric math alarms"
20604        );
20605    }
20606
20607    #[test]
20608    fn test_column_toggle_prevents_hiding_last_column() {
20609        let mut app = test_app();
20610        app.mode = Mode::Normal;
20611        app.current_service = Service::CloudWatchAlarms;
20612        app.service_selected = true;
20613
20614        // Start with only one visible column
20615        app.cw_alarm_visible_column_ids = vec!["column.cw.alarm.name"];
20616
20617        // Open column selector
20618        app.handle_action(Action::OpenColumnSelector);
20619        assert_eq!(app.mode, Mode::ColumnSelector);
20620
20621        // Select the only visible column (index 1)
20622        app.column_selector_index = 1;
20623
20624        // Try to toggle it off - should NOT remove it
20625        app.handle_action(Action::ToggleColumn);
20626
20627        // Should still have one column visible
20628        assert_eq!(app.cw_alarm_visible_column_ids.len(), 1);
20629        assert_eq!(app.cw_alarm_visible_column_ids[0], "column.cw.alarm.name");
20630    }
20631}
20632
20633#[cfg(test)]
20634mod insights_log_group_dropdown_tests {
20635    use super::*;
20636    use test_helpers::*;
20637
20638    fn setup_insights_with_log_groups(app: &mut App) {
20639        app.current_service = Service::CloudWatchInsights;
20640        app.service_selected = true;
20641        app.mode = Mode::InsightsInput;
20642        app.insights_state.insights.insights_focus = InsightsFocus::LogGroupSearch;
20643
20644        // Pre-populate log groups
20645        app.log_groups_state.log_groups.items = vec![
20646            rusticity_core::LogGroup {
20647                name: "/aws/lambda/my-function".to_string(),
20648                stored_bytes: None,
20649                retention_days: None,
20650                log_class: None,
20651                arn: None,
20652                log_group_arn: None,
20653                deletion_protection_enabled: None,
20654                creation_time: None,
20655            },
20656            rusticity_core::LogGroup {
20657                name: "/aws/containerinsights/my-cluster/application".to_string(),
20658                stored_bytes: None,
20659                retention_days: None,
20660                log_class: None,
20661                arn: None,
20662                log_group_arn: None,
20663                deletion_protection_enabled: None,
20664                creation_time: None,
20665            },
20666        ];
20667    }
20668
20669    #[test]
20670    fn test_typing_in_log_group_search_shows_dropdown() {
20671        let mut app = test_app();
20672        setup_insights_with_log_groups(&mut app);
20673
20674        // Type 'a' 'w' to search
20675        app.handle_action(Action::FilterInput('a'));
20676        app.handle_action(Action::FilterInput('w'));
20677
20678        assert!(
20679            app.insights_state.insights.show_dropdown,
20680            "Dropdown must show when log group search has matches"
20681        );
20682        assert!(
20683            !app.insights_state.insights.log_group_matches.is_empty(),
20684            "Must have matches for 'aw'"
20685        );
20686    }
20687
20688    #[test]
20689    fn test_space_toggles_highlighted_log_group_in_dropdown() {
20690        // Regression: Space in InsightsInput with LogGroupSearch focus must
20691        // toggle the highlighted dropdown entry, not type a space.
20692        let mut app = test_app();
20693        setup_insights_with_log_groups(&mut app);
20694
20695        // Type to show dropdown
20696        app.handle_action(Action::FilterInput('a'));
20697        app.handle_action(Action::FilterInput('w'));
20698
20699        assert!(app.insights_state.insights.show_dropdown);
20700        assert!(app.insights_state.insights.selected_log_groups.is_empty());
20701
20702        // Space must toggle the first/highlighted entry
20703        app.handle_action(Action::ToggleFilterCheckbox);
20704
20705        assert!(
20706            !app.insights_state.insights.selected_log_groups.is_empty(),
20707            "Space must add the highlighted log group to selected_log_groups"
20708        );
20709    }
20710
20711    #[test]
20712    fn test_space_key_dispatches_toggle_not_filter_input_in_insights_mode() {
20713        use crate::keymap::handle_key;
20714        use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
20715
20716        let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
20717        let action = handle_key(key, Mode::InsightsInput);
20718        assert_eq!(
20719            action,
20720            Some(Action::ToggleFilterCheckbox),
20721            "Space in InsightsInput must dispatch ToggleFilterCheckbox, not FilterInput(' ')"
20722        );
20723    }
20724
20725    #[test]
20726    fn test_down_up_arrows_navigate_dropdown() {
20727        let mut app = test_app();
20728        setup_insights_with_log_groups(&mut app);
20729        app.handle_action(Action::FilterInput('a'));
20730        app.handle_action(Action::FilterInput('w'));
20731
20732        assert_eq!(app.insights_state.insights.dropdown_selected, 0);
20733
20734        app.handle_action(Action::NextItem);
20735        assert!(
20736            app.insights_state.insights.dropdown_selected >= 1
20737                || app.insights_state.insights.log_group_matches.len() == 1,
20738            "Down arrow must advance dropdown selection"
20739        );
20740    }
20741
20742    #[test]
20743    fn test_space_in_normal_mode_toggles_dropdown_entry_not_open_space_menu() {
20744        // Regression: in Normal mode with Insights dropdown showing, Space was
20745        // opening the space menu instead of toggling the log group entry.
20746        let mut app = test_app();
20747        setup_insights_with_log_groups(&mut app);
20748        // Enter InsightsInput, type, show dropdown
20749        app.handle_action(Action::StartFilter);
20750        assert_eq!(app.mode, Mode::InsightsInput);
20751        app.handle_action(Action::FilterInput('a'));
20752        app.handle_action(Action::FilterInput('w'));
20753        assert!(app.insights_state.insights.show_dropdown);
20754
20755        // Switch back to Normal mode (simulate pressing Esc or Tab away)
20756        app.mode = Mode::Normal;
20757
20758        // Space must toggle entry, not open space menu
20759        app.handle_action(Action::OpenSpaceMenu); // Space maps to OpenSpaceMenu in Normal
20760        assert_ne!(
20761            app.mode,
20762            Mode::SpaceMenu,
20763            "Space must NOT open space menu when Insights dropdown is showing"
20764        );
20765        assert!(
20766            !app.insights_state.insights.selected_log_groups.is_empty(),
20767            "Space must toggle the highlighted log group"
20768        );
20769    }
20770
20771    #[test]
20772    fn test_enter_selects_highlighted_entry_and_closes_dropdown() {
20773        // Regression: Enter was closing the dropdown without selecting the entry.
20774        let mut app = test_app();
20775        setup_insights_with_log_groups(&mut app);
20776        app.handle_action(Action::StartFilter);
20777        app.handle_action(Action::FilterInput('a'));
20778        app.handle_action(Action::FilterInput('w'));
20779        assert!(app.insights_state.insights.show_dropdown);
20780
20781        // Enter must select highlighted entry AND close dropdown
20782        app.handle_action(Action::ApplyFilter);
20783
20784        assert!(
20785            !app.insights_state.insights.show_dropdown,
20786            "Dropdown must close after Enter"
20787        );
20788        assert!(
20789            !app.insights_state.insights.selected_log_groups.is_empty(),
20790            "Enter must select the highlighted log group before closing"
20791        );
20792    }
20793}