Skip to main content

rusticity_term/ui/
lambda.rs

1use crate::app::App;
2use crate::common::{
3    filter_by_fields, format_bytes, format_duration_seconds, format_memory_mb,
4    render_pagination_text, ColumnId, CyclicEnum, InputFocus, SortDirection,
5};
6use crate::keymap::Mode;
7use crate::lambda::{
8    format_architecture, format_runtime, parse_layer_arn, Alias, AliasColumn,
9    Application as LambdaApplication, ApplicationColumn, Deployment, DeploymentColumn,
10    Function as LambdaFunction, FunctionColumn as LambdaColumn, Layer, LayerColumn, Resource,
11    ResourceColumn, Version, VersionColumn,
12};
13use crate::table::{TableState, TableWindow};
14use crate::ui::filter::{render_simple_filter, SimpleFilterConfig};
15use crate::ui::monitoring::MonitoringState;
16use crate::ui::table::{
17    expanded_from_columns, plain_expanded_content, render_table, Column as TableColumn, TableConfig,
18};
19use crate::ui::{
20    calculate_dynamic_height, format_expansion_text, format_title, labeled_field,
21    render_fields_with_dynamic_columns, render_tabs, rounded_block, section_header, vertical,
22};
23use ratatui::{prelude::*, widgets::*};
24
25pub const FILTER_CONTROLS: [InputFocus; 2] = [InputFocus::Filter, InputFocus::Pagination];
26
27pub struct State {
28    pub table: TableState<LambdaFunction>,
29    pub current_function: Option<String>,
30    pub current_version: Option<String>,
31    pub current_alias: Option<String>,
32    pub detail_tab: DetailTab,
33    pub version_detail_tab: VersionDetailTab,
34    pub function_visible_column_ids: Vec<ColumnId>,
35    pub function_column_ids: Vec<ColumnId>,
36    pub version_table: TableState<Version>,
37    pub version_visible_column_ids: Vec<String>,
38    pub version_column_ids: Vec<String>,
39    pub alias_table: TableState<Alias>,
40    pub alias_visible_column_ids: Vec<String>,
41    pub alias_column_ids: Vec<String>,
42    pub layer_visible_column_ids: Vec<String>,
43    pub layer_column_ids: Vec<String>,
44    pub input_focus: InputFocus,
45    pub version_input_focus: InputFocus,
46    pub alias_input_focus: InputFocus,
47    pub layer_selected: usize,
48    pub layer_expanded: Option<usize>,
49    pub monitoring_scroll: usize,
50    pub metric_data_invocations: Vec<(i64, f64)>,
51    pub metric_data_duration_min: Vec<(i64, f64)>,
52    pub metric_data_duration_avg: Vec<(i64, f64)>,
53    pub metric_data_duration_max: Vec<(i64, f64)>,
54    pub metric_data_errors: Vec<(i64, f64)>,
55    pub metric_data_success_rate: Vec<(i64, f64)>,
56    pub metric_data_throttles: Vec<(i64, f64)>,
57    pub metric_data_concurrent_executions: Vec<(i64, f64)>,
58    pub metric_data_recursive_invocations_dropped: Vec<(i64, f64)>,
59    pub metric_data_async_event_age_min: Vec<(i64, f64)>,
60    pub metric_data_async_event_age_avg: Vec<(i64, f64)>,
61    pub metric_data_async_event_age_max: Vec<(i64, f64)>,
62    pub metric_data_async_events_received: Vec<(i64, f64)>,
63    pub metric_data_async_events_dropped: Vec<(i64, f64)>,
64    pub metric_data_destination_delivery_failures: Vec<(i64, f64)>,
65    pub metric_data_dead_letter_errors: Vec<(i64, f64)>,
66    pub metric_data_iterator_age: Vec<(i64, f64)>,
67    pub metrics_loading: bool,
68}
69
70impl Default for State {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl State {
77    pub fn new() -> Self {
78        Self {
79            table: TableState::new(),
80            current_function: None,
81            current_version: None,
82            current_alias: None,
83            detail_tab: DetailTab::Code,
84            version_detail_tab: VersionDetailTab::Code,
85            function_visible_column_ids: LambdaColumn::visible(),
86            function_column_ids: LambdaColumn::ids(),
87            version_table: TableState::new(),
88            version_visible_column_ids: VersionColumn::all()
89                .iter()
90                .map(|c| c.name().to_string())
91                .collect(),
92            version_column_ids: VersionColumn::all()
93                .iter()
94                .map(|c| c.name().to_string())
95                .collect(),
96            alias_table: TableState::new(),
97            alias_visible_column_ids: AliasColumn::all()
98                .iter()
99                .map(|c| c.name().to_string())
100                .collect(),
101            alias_column_ids: AliasColumn::all()
102                .iter()
103                .map(|c| c.name().to_string())
104                .collect(),
105            layer_visible_column_ids: LayerColumn::all()
106                .iter()
107                .map(|c| c.name().to_string())
108                .collect(),
109            layer_column_ids: LayerColumn::all()
110                .iter()
111                .map(|c| c.name().to_string())
112                .collect(),
113            input_focus: InputFocus::Filter,
114            version_input_focus: InputFocus::Filter,
115            alias_input_focus: InputFocus::Filter,
116            layer_selected: 0,
117            layer_expanded: None,
118            monitoring_scroll: 0,
119            metric_data_invocations: Vec::new(),
120            metric_data_duration_min: Vec::new(),
121            metric_data_duration_avg: Vec::new(),
122            metric_data_duration_max: Vec::new(),
123            metric_data_errors: Vec::new(),
124            metric_data_success_rate: Vec::new(),
125            metric_data_throttles: Vec::new(),
126            metric_data_concurrent_executions: Vec::new(),
127            metric_data_recursive_invocations_dropped: Vec::new(),
128            metric_data_async_event_age_min: Vec::new(),
129            metric_data_async_event_age_avg: Vec::new(),
130            metric_data_async_event_age_max: Vec::new(),
131            metric_data_async_events_received: Vec::new(),
132            metric_data_async_events_dropped: Vec::new(),
133            metric_data_destination_delivery_failures: Vec::new(),
134            metric_data_dead_letter_errors: Vec::new(),
135            metric_data_iterator_age: Vec::new(),
136            metrics_loading: false,
137        }
138    }
139}
140
141impl MonitoringState for State {
142    fn is_metrics_loading(&self) -> bool {
143        self.metrics_loading
144    }
145
146    fn set_metrics_loading(&mut self, loading: bool) {
147        self.metrics_loading = loading;
148    }
149
150    fn monitoring_scroll(&self) -> usize {
151        self.monitoring_scroll
152    }
153
154    fn set_monitoring_scroll(&mut self, scroll: usize) {
155        self.monitoring_scroll = scroll;
156    }
157
158    fn clear_metrics(&mut self) {
159        self.metric_data_invocations.clear();
160        self.metric_data_duration_min.clear();
161        self.metric_data_duration_avg.clear();
162        self.metric_data_duration_max.clear();
163        self.metric_data_errors.clear();
164        self.metric_data_success_rate.clear();
165        self.metric_data_throttles.clear();
166        self.metric_data_concurrent_executions.clear();
167        self.metric_data_recursive_invocations_dropped.clear();
168        self.metric_data_async_event_age_min.clear();
169        self.metric_data_async_event_age_avg.clear();
170        self.metric_data_async_event_age_max.clear();
171        self.metric_data_async_events_received.clear();
172        self.metric_data_async_events_dropped.clear();
173        self.metric_data_destination_delivery_failures.clear();
174        self.metric_data_dead_letter_errors.clear();
175        self.metric_data_iterator_age.clear();
176    }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq)]
180pub enum DetailTab {
181    Code,
182    Monitor,
183    Configuration,
184    Aliases,
185    Versions,
186}
187
188impl CyclicEnum for DetailTab {
189    const ALL: &'static [Self] = &[
190        Self::Code,
191        Self::Monitor,
192        Self::Configuration,
193        Self::Aliases,
194        Self::Versions,
195    ];
196}
197
198impl DetailTab {
199    pub const VERSION_TABS: &'static [Self] = &[Self::Code, Self::Monitor, Self::Configuration];
200
201    pub fn name(&self) -> &'static str {
202        match self {
203            DetailTab::Code => "Code",
204            DetailTab::Monitor => "Monitor",
205            DetailTab::Configuration => "Configuration",
206            DetailTab::Aliases => "Aliases",
207            DetailTab::Versions => "Versions",
208        }
209    }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq)]
213pub enum VersionDetailTab {
214    Code,
215    Monitor,
216    Configuration,
217}
218
219impl CyclicEnum for VersionDetailTab {
220    const ALL: &'static [Self] = &[Self::Code, Self::Monitor, Self::Configuration];
221}
222
223impl VersionDetailTab {
224    pub fn name(&self) -> &'static str {
225        match self {
226            VersionDetailTab::Code => "Code",
227            VersionDetailTab::Monitor => "Monitor",
228            VersionDetailTab::Configuration => "Configuration",
229        }
230    }
231
232    pub fn to_detail_tab(&self) -> DetailTab {
233        match self {
234            VersionDetailTab::Code => DetailTab::Code,
235            VersionDetailTab::Monitor => DetailTab::Monitor,
236            VersionDetailTab::Configuration => DetailTab::Configuration,
237        }
238    }
239
240    pub fn from_detail_tab(tab: DetailTab) -> Self {
241        match tab {
242            DetailTab::Code => VersionDetailTab::Code,
243            DetailTab::Monitor => VersionDetailTab::Monitor,
244            _ => VersionDetailTab::Configuration,
245        }
246    }
247}
248
249pub struct ApplicationState {
250    pub table: TableState<LambdaApplication>,
251    pub input_focus: InputFocus,
252    pub current_application: Option<String>,
253    pub detail_tab: ApplicationDetailTab,
254    pub deployments: TableState<Deployment>,
255    pub deployment_input_focus: InputFocus,
256    pub resources: TableState<Resource>,
257    pub resource_input_focus: InputFocus,
258}
259
260#[derive(Debug, Clone, Copy, PartialEq)]
261pub enum ApplicationDetailTab {
262    Overview,
263    Deployments,
264}
265
266impl CyclicEnum for ApplicationDetailTab {
267    const ALL: &'static [Self] = &[Self::Overview, Self::Deployments];
268}
269
270impl ApplicationDetailTab {
271    pub fn name(&self) -> &'static str {
272        match self {
273            Self::Overview => "Overview",
274            Self::Deployments => "Deployments",
275        }
276    }
277}
278
279impl Default for ApplicationState {
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285impl ApplicationState {
286    pub fn new() -> Self {
287        Self {
288            table: TableState::new(),
289            input_focus: InputFocus::Filter,
290            current_application: None,
291            detail_tab: ApplicationDetailTab::Overview,
292            deployments: TableState::new(),
293            deployment_input_focus: InputFocus::Filter,
294            resources: TableState::new(),
295            resource_input_focus: InputFocus::Filter,
296        }
297    }
298}
299
300pub fn render_functions(frame: &mut Frame, app: &App, area: Rect) {
301    frame.render_widget(Clear, area);
302
303    if app.lambda_state.current_alias.is_some() {
304        render_alias_detail(frame, app, area);
305        return;
306    }
307
308    if app.lambda_state.current_version.is_some() {
309        render_version_detail(frame, app, area);
310        return;
311    }
312
313    if app.lambda_state.current_function.is_some() {
314        render_detail(frame, app, area);
315        return;
316    }
317
318    let chunks = vertical(
319        [
320            Constraint::Length(3), // Filter
321            Constraint::Min(0),    // Table
322        ],
323        area,
324    );
325
326    // Filter
327    let view = app.lambda_state.table.prepare_view(
328        app.lambda_state
329            .table
330            .items
331            .iter()
332            .filter(|f| {
333                app.lambda_state.table.filter.is_empty()
334                    || f.name
335                        .to_lowercase()
336                        .contains(&app.lambda_state.table.filter.to_lowercase())
337                    || f.description
338                        .to_lowercase()
339                        .contains(&app.lambda_state.table.filter.to_lowercase())
340                    || f.runtime
341                        .to_lowercase()
342                        .contains(&app.lambda_state.table.filter.to_lowercase())
343            })
344            .collect(),
345        TableWindow::SelectedPage,
346    );
347    let pagination = render_pagination_text(view.current_page, view.total_pages);
348
349    render_simple_filter(
350        frame,
351        chunks[0],
352        SimpleFilterConfig {
353            filter_text: &app.lambda_state.table.filter,
354            placeholder: "Filter by attributes or search by keyword",
355            pagination: &pagination,
356            mode: app.mode,
357            is_input_focused: app.lambda_state.input_focus == InputFocus::Filter,
358            is_pagination_focused: app.lambda_state.input_focus == InputFocus::Pagination,
359        },
360    );
361
362    // Table
363
364    let title = format_title(&format!("Lambda functions ({})", view.total_items));
365
366    let mut columns: Vec<Box<dyn TableColumn<LambdaFunction>>> = vec![];
367    for col_id in &app.lambda_state.function_visible_column_ids {
368        if let Some(column) = LambdaColumn::from_id(col_id) {
369            columns.push(Box::new(column));
370        }
371    }
372
373    let config = TableConfig {
374        items: view.items,
375        selected_index: view.selected_index,
376        expanded_index: view.expanded_index,
377        columns: &columns,
378        sort_column: "Last modified",
379        sort_direction: SortDirection::Desc,
380        title,
381        area: chunks[1],
382        get_expanded_content: Some(Box::new(|func: &LambdaFunction| {
383            expanded_from_columns(&columns, func)
384        })),
385        is_active: app.mode != Mode::FilterInput,
386    };
387
388    render_table(frame, config);
389}
390
391pub fn render_detail(frame: &mut Frame, app: &App, area: Rect) {
392    frame.render_widget(Clear, area);
393
394    // Build overview lines first to calculate height
395    let overview_lines = if let Some(func_name) = &app.lambda_state.current_function {
396        if let Some(func) = app
397            .lambda_state
398            .table
399            .items
400            .iter()
401            .find(|f| f.name == *func_name)
402        {
403            vec![
404                labeled_field(
405                    "Description",
406                    if func.description.is_empty() {
407                        "-"
408                    } else {
409                        &func.description
410                    },
411                ),
412                labeled_field("Last modified", &func.last_modified),
413                labeled_field("Function ARN", &func.arn),
414                labeled_field("Application", func.application.as_deref().unwrap_or("-")),
415            ]
416        } else {
417            vec![]
418        }
419    } else {
420        vec![]
421    };
422
423    let overview_height = if overview_lines.is_empty() {
424        0
425    } else {
426        calculate_dynamic_height(&overview_lines, area.width.saturating_sub(4)) + 2
427    };
428
429    let chunks = vertical(
430        [
431            Constraint::Length(overview_height),
432            Constraint::Length(1), // Tabs
433            Constraint::Min(0),    // Content
434        ],
435        area,
436    );
437
438    // Function overview
439    if !overview_lines.is_empty() {
440        let overview_block = Block::default()
441            .title(format_title("Function overview"))
442            .borders(Borders::ALL)
443            .border_type(BorderType::Rounded)
444            .border_style(Style::default());
445
446        let overview_inner = overview_block.inner(chunks[0]);
447        frame.render_widget(overview_block, chunks[0]);
448        render_fields_with_dynamic_columns(frame, overview_inner, overview_lines);
449    }
450
451    // Tabs
452    let tabs: Vec<(&str, DetailTab)> = DetailTab::ALL
453        .iter()
454        .map(|tab| (tab.name(), *tab))
455        .collect();
456
457    render_tabs(frame, chunks[1], &tabs, &app.lambda_state.detail_tab);
458
459    // Content area
460    if app.lambda_state.detail_tab == DetailTab::Code {
461        // Show Code properties
462        if let Some(func_name) = &app.lambda_state.current_function {
463            if let Some(func) = app
464                .lambda_state
465                .table
466                .items
467                .iter()
468                .find(|f| f.name == *func_name)
469            {
470                // Build lines first to calculate heights
471                let code_lines = vec![
472                    labeled_field("Package size", format_bytes(func.code_size)),
473                    labeled_field("SHA256 hash", &func.code_sha256),
474                    labeled_field("Last modified", &func.last_modified),
475                    section_header(
476                        "Encryption with AWS KMS customer managed KMS key",
477                        chunks[2].width.saturating_sub(2),
478                    ),
479                    Line::from(Span::styled(
480                        "To edit customer managed key encryption, you must upload a new .zip deployment package.",
481                        Style::default().fg(Color::DarkGray),
482                    )),
483                    labeled_field("AWS KMS key ARN", ""),
484                    labeled_field("Key alias", ""),
485                    labeled_field("Status", ""),
486                ];
487
488                let runtime_lines = vec![
489                    labeled_field("Runtime", format_runtime(&func.runtime)),
490                    labeled_field("Handler", ""),
491                    labeled_field("Architecture", format_architecture(&func.architecture)),
492                    section_header(
493                        "Runtime management configuration",
494                        chunks[2].width.saturating_sub(2),
495                    ),
496                    labeled_field("Runtime version ARN", ""),
497                    labeled_field("Update runtime version", "Auto"),
498                ];
499
500                let chunks_content = Layout::default()
501                    .direction(Direction::Vertical)
502                    .constraints([
503                        Constraint::Length(
504                            calculate_dynamic_height(
505                                &code_lines,
506                                chunks[2].width.saturating_sub(4),
507                            ) + 2,
508                        ),
509                        Constraint::Length(
510                            calculate_dynamic_height(
511                                &runtime_lines,
512                                chunks[2].width.saturating_sub(4),
513                            ) + 2,
514                        ),
515                        Constraint::Min(0), // Layers
516                    ])
517                    .split(chunks[2]);
518
519                // Code properties section
520                let code_block = Block::default()
521                    .title(format_title("Code properties"))
522                    .borders(Borders::ALL)
523                    .border_type(BorderType::Rounded);
524
525                let code_inner = code_block.inner(chunks_content[0]);
526                frame.render_widget(code_block, chunks_content[0]);
527
528                render_fields_with_dynamic_columns(frame, code_inner, code_lines);
529
530                // Runtime settings section
531                let runtime_block = Block::default()
532                    .title(format_title("Runtime settings"))
533                    .borders(Borders::ALL)
534                    .border_type(BorderType::Rounded);
535
536                let runtime_inner = runtime_block.inner(chunks_content[1]);
537                frame.render_widget(runtime_block, chunks_content[1]);
538
539                render_fields_with_dynamic_columns(frame, runtime_inner, runtime_lines);
540
541                // Layers section
542                let layer_refs: Vec<&Layer> = func.layers.iter().collect();
543                let title = format_title(&format!("Layers ({})", layer_refs.len()));
544
545                let columns: Vec<Box<dyn TableColumn<Layer>>> = vec![
546                    Box::new(LayerColumn::MergeOrder),
547                    Box::new(LayerColumn::Name),
548                    Box::new(LayerColumn::LayerVersion),
549                    Box::new(LayerColumn::CompatibleRuntimes),
550                    Box::new(LayerColumn::CompatibleArchitectures),
551                    Box::new(LayerColumn::VersionArn),
552                ];
553
554                let config = TableConfig {
555                    items: layer_refs,
556                    selected_index: app.lambda_state.layer_selected,
557                    expanded_index: app.lambda_state.layer_expanded,
558                    columns: &columns,
559                    sort_column: "",
560                    sort_direction: SortDirection::Asc,
561                    title,
562                    area: chunks_content[2],
563                    get_expanded_content: Some(Box::new(|layer: &Layer| {
564                        format_expansion_text(&[
565                            ("Merge order", layer.merge_order.clone()),
566                            ("Name", layer.name.clone()),
567                            ("Layer version", layer.layer_version.clone()),
568                            ("Compatible runtimes", layer.compatible_runtimes.clone()),
569                            (
570                                "Compatible architectures",
571                                layer.compatible_architectures.clone(),
572                            ),
573                            ("Version ARN", layer.version_arn.clone()),
574                        ])
575                    })),
576                    is_active: app.lambda_state.detail_tab == DetailTab::Code,
577                };
578
579                render_table(frame, config);
580            }
581        }
582    } else if app.lambda_state.detail_tab == DetailTab::Monitor {
583        render_lambda_monitoring_charts(frame, app, chunks[2]);
584    } else if app.lambda_state.detail_tab == DetailTab::Configuration {
585        // Configuration tab
586        if let Some(func_name) = &app.lambda_state.current_function {
587            if let Some(func) = app
588                .lambda_state
589                .table
590                .items
591                .iter()
592                .find(|f| f.name == *func_name)
593            {
594                let config_lines = vec![
595                    labeled_field("Description", &func.description),
596                    labeled_field("Revision", &func.last_modified),
597                    labeled_field("Memory", format_memory_mb(func.memory_mb)),
598                    labeled_field("Ephemeral storage", format_memory_mb(512)),
599                    labeled_field("Timeout", format_duration_seconds(func.timeout_seconds)),
600                    labeled_field("SnapStart", "None"),
601                ];
602
603                let config_chunks = vertical(
604                    [
605                        Constraint::Length(
606                            calculate_dynamic_height(
607                                &config_lines,
608                                chunks[2].width.saturating_sub(4),
609                            ) + 2,
610                        ),
611                        Constraint::Min(0),
612                    ],
613                    chunks[2],
614                );
615
616                let config_block = Block::default()
617                    .title(format_title("General configuration"))
618                    .borders(Borders::ALL)
619                    .border_type(BorderType::Rounded)
620                    .border_style(Style::default());
621
622                let config_inner = config_block.inner(config_chunks[0]);
623                frame.render_widget(config_block, config_chunks[0]);
624
625                render_fields_with_dynamic_columns(frame, config_inner, config_lines);
626            }
627        }
628    } else if app.lambda_state.detail_tab == DetailTab::Versions {
629        // Versions tab
630        let version_chunks = vertical(
631            [
632                Constraint::Length(3), // Filter
633                Constraint::Min(0),    // Table
634            ],
635            chunks[2],
636        );
637
638        // Filter
639        let view = app.lambda_state.version_table.prepare_view(
640            app.lambda_state
641                .version_table
642                .items
643                .iter()
644                .filter(|v| {
645                    app.lambda_state.version_table.filter.is_empty()
646                        || v.version
647                            .to_lowercase()
648                            .contains(&app.lambda_state.version_table.filter.to_lowercase())
649                        || v.aliases
650                            .to_lowercase()
651                            .contains(&app.lambda_state.version_table.filter.to_lowercase())
652                        || v.description
653                            .to_lowercase()
654                            .contains(&app.lambda_state.version_table.filter.to_lowercase())
655                })
656                .collect(),
657            TableWindow::SelectedPage,
658        );
659        let pagination = render_pagination_text(view.current_page, view.total_pages);
660
661        render_simple_filter(
662            frame,
663            version_chunks[0],
664            SimpleFilterConfig {
665                filter_text: &app.lambda_state.version_table.filter,
666                placeholder: "Filter by attributes or search by keyword",
667                pagination: &pagination,
668                mode: app.mode,
669                is_input_focused: app.lambda_state.version_input_focus == InputFocus::Filter,
670                is_pagination_focused: app.lambda_state.version_input_focus
671                    == InputFocus::Pagination,
672            },
673        );
674
675        // Table
676
677        let title = format_title(&format!("Versions ({})", view.total_items));
678
679        let mut columns: Vec<Box<dyn TableColumn<Version>>> = vec![];
680        for col_name in &app.lambda_state.version_visible_column_ids {
681            let column = match col_name.as_str() {
682                "Version" => Some(VersionColumn::Version),
683                "Aliases" => Some(VersionColumn::Aliases),
684                "Description" => Some(VersionColumn::Description),
685                "Last modified" => Some(VersionColumn::LastModified),
686                "Architecture" => Some(VersionColumn::Architecture),
687                _ => None,
688            };
689            if let Some(c) = column {
690                columns.push(c.to_column());
691            }
692        }
693
694        let config = TableConfig {
695            items: view.items,
696            selected_index: view.selected_index,
697            expanded_index: view.expanded_index,
698            columns: &columns,
699            sort_column: "Version",
700            sort_direction: SortDirection::Desc,
701            title,
702            area: version_chunks[1],
703            get_expanded_content: Some(Box::new(|ver: &Version| {
704                expanded_from_columns(&columns, ver)
705            })),
706            is_active: app.mode != Mode::FilterInput,
707        };
708
709        render_table(frame, config);
710    } else if app.lambda_state.detail_tab == DetailTab::Aliases {
711        // Aliases tab
712        let alias_chunks = vertical(
713            [
714                Constraint::Length(3), // Filter
715                Constraint::Min(0),    // Table
716            ],
717            chunks[2],
718        );
719
720        // Filter
721        let view = app.lambda_state.alias_table.prepare_view(
722            app.lambda_state
723                .alias_table
724                .items
725                .iter()
726                .filter(|a| {
727                    app.lambda_state.alias_table.filter.is_empty()
728                        || a.name
729                            .to_lowercase()
730                            .contains(&app.lambda_state.alias_table.filter.to_lowercase())
731                        || a.versions
732                            .to_lowercase()
733                            .contains(&app.lambda_state.alias_table.filter.to_lowercase())
734                        || a.description
735                            .to_lowercase()
736                            .contains(&app.lambda_state.alias_table.filter.to_lowercase())
737                })
738                .collect(),
739            TableWindow::SelectedPage,
740        );
741        let pagination = render_pagination_text(view.current_page, view.total_pages);
742
743        render_simple_filter(
744            frame,
745            alias_chunks[0],
746            SimpleFilterConfig {
747                filter_text: &app.lambda_state.alias_table.filter,
748                placeholder: "Filter by attributes or search by keyword",
749                pagination: &pagination,
750                mode: app.mode,
751                is_input_focused: app.lambda_state.alias_input_focus == InputFocus::Filter,
752                is_pagination_focused: app.lambda_state.alias_input_focus == InputFocus::Pagination,
753            },
754        );
755
756        // Table
757
758        let title = format_title(&format!("Aliases ({})", view.total_items));
759
760        let mut columns: Vec<Box<dyn TableColumn<Alias>>> = vec![];
761        for col_name in &app.lambda_state.alias_visible_column_ids {
762            let column = match col_name.as_str() {
763                "Name" => Some(AliasColumn::Name),
764                "Versions" => Some(AliasColumn::Versions),
765                "Description" => Some(AliasColumn::Description),
766                _ => None,
767            };
768            if let Some(c) = column {
769                columns.push(c.to_column());
770            }
771        }
772
773        let config = TableConfig {
774            items: view.items,
775            selected_index: view.selected_index,
776            expanded_index: view.expanded_index,
777            columns: &columns,
778            sort_column: "Name",
779            sort_direction: SortDirection::Asc,
780            title,
781            area: alias_chunks[1],
782            get_expanded_content: Some(Box::new(|alias: &Alias| {
783                expanded_from_columns(&columns, alias)
784            })),
785            is_active: app.mode != Mode::FilterInput,
786        };
787
788        render_table(frame, config);
789    } else {
790        // Placeholder for other tabs
791        let content = Paragraph::new(format!(
792            "{} tab content (coming soon)",
793            app.lambda_state.detail_tab.name()
794        ))
795        .block(rounded_block());
796        frame.render_widget(content, chunks[2]);
797    }
798}
799
800pub fn render_alias_detail(frame: &mut Frame, app: &App, area: Rect) {
801    frame.render_widget(Clear, area);
802
803    // Build overview lines first to calculate height
804    let mut overview_lines = vec![];
805    if let Some(func_name) = &app.lambda_state.current_function {
806        if let Some(func) = app
807            .lambda_state
808            .table
809            .items
810            .iter()
811            .find(|f| f.name == *func_name)
812        {
813            if let Some(alias_name) = &app.lambda_state.current_alias {
814                if let Some(alias) = app
815                    .lambda_state
816                    .alias_table
817                    .items
818                    .iter()
819                    .find(|a| a.name == *alias_name)
820                {
821                    overview_lines.push(labeled_field("Description", &alias.description));
822
823                    // Parse versions
824                    let versions_parts: Vec<&str> =
825                        alias.versions.split(',').map(|s| s.trim()).collect();
826                    if let Some(first_version) = versions_parts.first() {
827                        overview_lines.push(labeled_field("Version", *first_version));
828                    }
829                    if versions_parts.len() > 1 {
830                        if let Some(second_version) = versions_parts.get(1) {
831                            overview_lines
832                                .push(labeled_field("Additional version", *second_version));
833                        }
834                    }
835
836                    overview_lines.push(labeled_field("Function ARN", &func.arn));
837
838                    if let Some(app) = &func.application {
839                        overview_lines.push(labeled_field("Application", app));
840                    }
841
842                    overview_lines.push(labeled_field("Function URL", "-"));
843                }
844            }
845        }
846    }
847
848    // Build config lines to calculate height
849    let mut config_lines = vec![];
850    if let Some(_func_name) = &app.lambda_state.current_function {
851        if let Some(alias_name) = &app.lambda_state.current_alias {
852            if let Some(alias) = app
853                .lambda_state
854                .alias_table
855                .items
856                .iter()
857                .find(|a| a.name == *alias_name)
858            {
859                config_lines.push(labeled_field("Name", &alias.name));
860                config_lines.push(labeled_field("Description", &alias.description));
861
862                // Parse versions
863                let versions_parts: Vec<&str> =
864                    alias.versions.split(',').map(|s| s.trim()).collect();
865                if let Some(first_version) = versions_parts.first() {
866                    config_lines.push(labeled_field("Version", *first_version));
867                }
868                if versions_parts.len() > 1 {
869                    if let Some(second_version) = versions_parts.get(1) {
870                        config_lines.push(labeled_field("Additional version", *second_version));
871                    }
872                }
873            }
874        }
875    }
876
877    let config_height = if config_lines.is_empty() {
878        0
879    } else {
880        calculate_dynamic_height(&config_lines, area.width.saturating_sub(4)) + 2
881    };
882
883    let overview_height =
884        calculate_dynamic_height(&overview_lines, area.width.saturating_sub(4)) + 2;
885
886    let chunks = vertical(
887        [
888            Constraint::Length(overview_height),
889            Constraint::Length(config_height),
890            Constraint::Min(0), // Empty space
891        ],
892        area,
893    );
894
895    // Function overview
896    if let Some(func_name) = &app.lambda_state.current_function {
897        if let Some(_func) = app
898            .lambda_state
899            .table
900            .items
901            .iter()
902            .find(|f| f.name == *func_name)
903        {
904            if let Some(alias_name) = &app.lambda_state.current_alias {
905                if let Some(_alias) = app
906                    .lambda_state
907                    .alias_table
908                    .items
909                    .iter()
910                    .find(|a| a.name == *alias_name)
911                {
912                    let overview_block = Block::default()
913                        .title(format_title("Function overview"))
914                        .borders(Borders::ALL)
915                        .border_type(BorderType::Rounded)
916                        .border_style(Style::default());
917
918                    let overview_inner = overview_block.inner(chunks[0]);
919                    frame.render_widget(overview_block, chunks[0]);
920
921                    render_fields_with_dynamic_columns(frame, overview_inner, overview_lines);
922                }
923            }
924        }
925    }
926
927    // General configuration
928    if !config_lines.is_empty() {
929        let config_block = Block::default()
930            .title(format_title("General configuration"))
931            .borders(Borders::ALL)
932            .border_type(BorderType::Rounded);
933
934        let config_inner = config_block.inner(chunks[1]);
935        frame.render_widget(config_block, chunks[1]);
936        render_fields_with_dynamic_columns(frame, config_inner, config_lines);
937    }
938}
939
940pub fn render_version_detail(frame: &mut Frame, app: &App, area: Rect) {
941    frame.render_widget(Clear, area);
942
943    // Build overview lines first to calculate height
944    let mut overview_lines = vec![];
945    if let Some(func_name) = &app.lambda_state.current_function {
946        if let Some(func) = app
947            .lambda_state
948            .table
949            .items
950            .iter()
951            .find(|f| f.name == *func_name)
952        {
953            if let Some(version_num) = &app.lambda_state.current_version {
954                let version_arn = format!("{}:{}", func.arn, version_num);
955
956                overview_lines.push(labeled_field("Name", &func.name));
957
958                if let Some(app) = &func.application {
959                    overview_lines.push(labeled_field("Application", app));
960                }
961
962                overview_lines.extend(vec![
963                    labeled_field("ARN", version_arn),
964                    labeled_field("Version", version_num),
965                ]);
966            }
967        }
968    }
969
970    let overview_height = if overview_lines.is_empty() {
971        0
972    } else {
973        calculate_dynamic_height(&overview_lines, area.width.saturating_sub(4)) + 2
974    };
975
976    let chunks = vertical(
977        [
978            Constraint::Length(overview_height),
979            Constraint::Length(1), // Tabs (Code, Configuration only)
980            Constraint::Min(0),    // Content
981        ],
982        area,
983    );
984
985    // Function overview
986    if !overview_lines.is_empty() {
987        let overview_block = Block::default()
988            .title(format_title("Function overview"))
989            .borders(Borders::ALL)
990            .border_type(BorderType::Rounded)
991            .border_style(Style::default());
992
993        let overview_inner = overview_block.inner(chunks[0]);
994        frame.render_widget(overview_block, chunks[0]);
995        render_fields_with_dynamic_columns(frame, overview_inner, overview_lines);
996    }
997
998    // Tabs - only Code, Monitor, and Configuration
999    let tabs: Vec<(&str, VersionDetailTab)> = VersionDetailTab::ALL
1000        .iter()
1001        .map(|tab| (tab.name(), *tab))
1002        .collect();
1003
1004    render_tabs(
1005        frame,
1006        chunks[1],
1007        &tabs,
1008        &app.lambda_state.version_detail_tab,
1009    );
1010
1011    // Content area - reuse same rendering as function detail
1012    if app.lambda_state.detail_tab == DetailTab::Code {
1013        if let Some(func_name) = &app.lambda_state.current_function {
1014            if let Some(func) = app
1015                .lambda_state
1016                .table
1017                .items
1018                .iter()
1019                .find(|f| f.name == *func_name)
1020            {
1021                // Build lines first to calculate heights
1022                let code_lines = vec![
1023                    labeled_field("Package size", format_bytes(func.code_size)),
1024                    labeled_field("SHA256 hash", &func.code_sha256),
1025                    labeled_field("Last modified", &func.last_modified),
1026                ];
1027
1028                let runtime_lines = vec![
1029                    labeled_field("Runtime", format_runtime(&func.runtime)),
1030                    labeled_field("Handler", ""),
1031                    labeled_field("Architecture", format_architecture(&func.architecture)),
1032                ];
1033
1034                let chunks_content = Layout::default()
1035                    .direction(Direction::Vertical)
1036                    .constraints([
1037                        Constraint::Length(
1038                            calculate_dynamic_height(
1039                                &code_lines,
1040                                chunks[2].width.saturating_sub(4),
1041                            ) + 2,
1042                        ),
1043                        Constraint::Length(
1044                            calculate_dynamic_height(
1045                                &runtime_lines,
1046                                chunks[2].width.saturating_sub(4),
1047                            ) + 2,
1048                        ),
1049                        Constraint::Min(0),
1050                    ])
1051                    .split(chunks[2]);
1052
1053                // Code properties section
1054                let code_block = Block::default()
1055                    .title(format_title("Code properties"))
1056                    .borders(Borders::ALL)
1057                    .border_type(BorderType::Rounded);
1058
1059                let code_inner = code_block.inner(chunks_content[0]);
1060                frame.render_widget(code_block, chunks_content[0]);
1061
1062                render_fields_with_dynamic_columns(frame, code_inner, code_lines);
1063
1064                // Runtime settings section
1065                let runtime_block = Block::default()
1066                    .title(format_title("Runtime settings"))
1067                    .borders(Borders::ALL)
1068                    .border_type(BorderType::Rounded);
1069
1070                let runtime_inner = runtime_block.inner(chunks_content[1]);
1071                frame.render_widget(runtime_block, chunks_content[1]);
1072
1073                render_fields_with_dynamic_columns(frame, runtime_inner, runtime_lines);
1074
1075                // Layers section (empty table)
1076                let layers: Vec<Layer> = vec![];
1077                let layer_refs: Vec<&Layer> = layers.iter().collect();
1078                let title = format_title(&format!("Layers ({})", layer_refs.len()));
1079
1080                let columns: Vec<Box<dyn TableColumn<Layer>>> = vec![
1081                    Box::new(LayerColumn::MergeOrder),
1082                    Box::new(LayerColumn::Name),
1083                    Box::new(LayerColumn::LayerVersion),
1084                    Box::new(LayerColumn::CompatibleRuntimes),
1085                    Box::new(LayerColumn::CompatibleArchitectures),
1086                    Box::new(LayerColumn::VersionArn),
1087                ];
1088
1089                let config = TableConfig {
1090                    items: layer_refs,
1091                    selected_index: 0,
1092                    expanded_index: None,
1093                    columns: &columns,
1094                    sort_column: "",
1095                    sort_direction: SortDirection::Asc,
1096                    title,
1097                    area: chunks_content[2],
1098                    get_expanded_content: Some(Box::new(|layer: &Layer| {
1099                        format_expansion_text(&[
1100                            ("Merge order", layer.merge_order.clone()),
1101                            ("Name", layer.name.clone()),
1102                            ("Layer version", layer.layer_version.clone()),
1103                            ("Compatible runtimes", layer.compatible_runtimes.clone()),
1104                            (
1105                                "Compatible architectures",
1106                                layer.compatible_architectures.clone(),
1107                            ),
1108                            ("Version ARN", layer.version_arn.clone()),
1109                        ])
1110                    })),
1111                    is_active: app.lambda_state.detail_tab == DetailTab::Code,
1112                };
1113
1114                render_table(frame, config);
1115            }
1116        }
1117    } else if app.lambda_state.detail_tab == DetailTab::Monitor {
1118        // Monitor tab - render same charts as function detail
1119        if app.lambda_state.metrics_loading {
1120            let loading_block = Block::default()
1121                .title(format_title("Monitor"))
1122                .borders(Borders::ALL)
1123                .border_type(BorderType::Rounded);
1124            let loading_text = Paragraph::new("Loading metrics...")
1125                .block(loading_block)
1126                .alignment(ratatui::layout::Alignment::Center);
1127            frame.render_widget(loading_text, chunks[2]);
1128            return;
1129        }
1130
1131        // Reuse the same monitoring rendering logic
1132        render_lambda_monitoring_charts(frame, app, chunks[2]);
1133    } else if app.lambda_state.detail_tab == DetailTab::Configuration {
1134        if let Some(func_name) = &app.lambda_state.current_function {
1135            if let Some(func) = app
1136                .lambda_state
1137                .table
1138                .items
1139                .iter()
1140                .find(|f| f.name == *func_name)
1141            {
1142                if let Some(version_num) = &app.lambda_state.current_version {
1143                    // Version Configuration: show config + aliases for this version
1144                    let config_lines = vec![
1145                        labeled_field("Description", &func.description),
1146                        labeled_field("Memory", format_memory_mb(func.memory_mb)),
1147                        labeled_field("Timeout", format_duration_seconds(func.timeout_seconds)),
1148                    ];
1149
1150                    let chunks_content = Layout::default()
1151                        .direction(Direction::Vertical)
1152                        .constraints([
1153                            Constraint::Length(
1154                                calculate_dynamic_height(
1155                                    &config_lines,
1156                                    chunks[2].width.saturating_sub(4),
1157                                ) + 2,
1158                            ),
1159                            Constraint::Length(3), // Filter
1160                            Constraint::Min(0),    // Aliases table
1161                        ])
1162                        .split(chunks[2]);
1163
1164                    let config_block = Block::default()
1165                        .title(format_title("General configuration"))
1166                        .borders(Borders::ALL)
1167                        .border_type(BorderType::Rounded)
1168                        .border_style(Style::default());
1169
1170                    let config_inner = config_block.inner(chunks_content[0]);
1171                    frame.render_widget(config_block, chunks_content[0]);
1172
1173                    render_fields_with_dynamic_columns(frame, config_inner, config_lines);
1174
1175                    // Filter for aliases
1176                    let view = app.lambda_state.alias_table.prepare_view(
1177                        app.lambda_state
1178                            .alias_table
1179                            .items
1180                            .iter()
1181                            .filter(|a| {
1182                                a.versions.contains(version_num)
1183                                    && (app.lambda_state.alias_table.filter.is_empty()
1184                                        || a.name.to_lowercase().contains(
1185                                            &app.lambda_state.alias_table.filter.to_lowercase(),
1186                                        )
1187                                        || a.versions.to_lowercase().contains(
1188                                            &app.lambda_state.alias_table.filter.to_lowercase(),
1189                                        )
1190                                        || a.description.to_lowercase().contains(
1191                                            &app.lambda_state.alias_table.filter.to_lowercase(),
1192                                        ))
1193                            })
1194                            .collect(),
1195                        TableWindow::SelectedPage,
1196                    );
1197                    let pagination = render_pagination_text(view.current_page, view.total_pages);
1198
1199                    render_simple_filter(
1200                        frame,
1201                        chunks_content[1],
1202                        SimpleFilterConfig {
1203                            filter_text: &app.lambda_state.alias_table.filter,
1204                            placeholder: "Filter by attributes or search by keyword",
1205                            pagination: &pagination,
1206                            mode: app.mode,
1207                            is_input_focused: app.lambda_state.alias_input_focus
1208                                == InputFocus::Filter,
1209                            is_pagination_focused: app.lambda_state.alias_input_focus
1210                                == InputFocus::Pagination,
1211                        },
1212                    );
1213
1214                    // Aliases table - filter to show only aliases pointing to this version
1215
1216                    let title = format_title(&format!("Aliases ({})", view.total_items));
1217
1218                    let mut columns: Vec<Box<dyn TableColumn<Alias>>> = vec![];
1219                    for col_name in &app.lambda_state.alias_visible_column_ids {
1220                        let column = match col_name.as_str() {
1221                            "Name" => Some(AliasColumn::Name),
1222                            "Versions" => Some(AliasColumn::Versions),
1223                            "Description" => Some(AliasColumn::Description),
1224                            _ => None,
1225                        };
1226                        if let Some(c) = column {
1227                            columns.push(c.to_column());
1228                        }
1229                    }
1230
1231                    let config = TableConfig {
1232                        items: view.items,
1233                        selected_index: view.selected_index,
1234                        expanded_index: view.expanded_index,
1235                        columns: &columns,
1236                        sort_column: "Name",
1237                        sort_direction: SortDirection::Asc,
1238                        title,
1239                        area: chunks_content[2],
1240                        get_expanded_content: Some(Box::new(|alias: &Alias| {
1241                            expanded_from_columns(&columns, alias)
1242                        })),
1243                        is_active: app.mode != Mode::FilterInput,
1244                    };
1245
1246                    render_table(frame, config);
1247                }
1248            }
1249        }
1250    }
1251}
1252
1253pub fn render_applications(frame: &mut Frame, app: &App, area: Rect) {
1254    frame.render_widget(Clear, area);
1255
1256    if app.lambda_application_state.current_application.is_some() {
1257        render_application_detail(frame, app, area);
1258        return;
1259    }
1260
1261    let chunks = Layout::default()
1262        .direction(Direction::Vertical)
1263        .constraints([Constraint::Length(3), Constraint::Min(0)])
1264        .split(area);
1265
1266    // Filter with pagination
1267    let view = app
1268        .lambda_application_state
1269        .table
1270        .prepare_view(filtered_lambda_applications(app), TableWindow::SelectedPage);
1271    let pagination = render_pagination_text(view.current_page, view.total_pages);
1272
1273    render_simple_filter(
1274        frame,
1275        chunks[0],
1276        SimpleFilterConfig {
1277            filter_text: &app.lambda_application_state.table.filter,
1278            placeholder: "Filter by attributes or search by keyword",
1279            pagination: &pagination,
1280            mode: app.mode,
1281            is_input_focused: app.lambda_application_state.input_focus == InputFocus::Filter,
1282            is_pagination_focused: app.lambda_application_state.input_focus
1283                == InputFocus::Pagination,
1284        },
1285    );
1286
1287    // Table
1288    let title = format_title(&format!("Applications ({})", view.total_items));
1289
1290    let mut columns: Vec<Box<dyn TableColumn<LambdaApplication>>> = vec![];
1291    for col_id in &app.lambda_application_visible_column_ids {
1292        if let Some(column) = ApplicationColumn::from_id(col_id) {
1293            columns.push(Box::new(column));
1294        }
1295    }
1296
1297    let config = TableConfig {
1298        items: view.items,
1299        selected_index: view.selected_index,
1300        expanded_index: view.expanded_index,
1301        columns: &columns,
1302        sort_column: "Last modified",
1303        sort_direction: SortDirection::Desc,
1304        title,
1305        area: chunks[1],
1306        get_expanded_content: Some(Box::new(|app: &LambdaApplication| {
1307            expanded_from_columns(&columns, app)
1308        })),
1309        is_active: app.mode != Mode::FilterInput,
1310    };
1311
1312    render_table(frame, config);
1313}
1314
1315// Lambda-specific helper functions
1316pub fn filtered_lambda_functions(app: &App) -> Vec<&LambdaFunction> {
1317    filter_by_fields(
1318        &app.lambda_state.table.items,
1319        &app.lambda_state.table.filter,
1320        |f| vec![&f.name, &f.description, &f.runtime],
1321    )
1322}
1323
1324pub fn filtered_lambda_applications(app: &App) -> Vec<&LambdaApplication> {
1325    filter_by_fields(
1326        &app.lambda_application_state.table.items,
1327        &app.lambda_application_state.table.filter,
1328        |a| vec![&a.name, &a.description, &a.status],
1329    )
1330}
1331
1332pub async fn load_lambda_functions(app: &mut App) -> anyhow::Result<()> {
1333    let functions = app.lambda_client.list_functions().await?;
1334
1335    let mut functions: Vec<LambdaFunction> = functions
1336        .into_iter()
1337        .map(|f| LambdaFunction {
1338            name: f.name,
1339            arn: f.arn,
1340            application: f.application,
1341            description: f.description,
1342            package_type: f.package_type,
1343            runtime: f.runtime,
1344            architecture: f.architecture,
1345            code_size: f.code_size,
1346            code_sha256: f.code_sha256,
1347            memory_mb: f.memory_mb,
1348            timeout_seconds: f.timeout_seconds,
1349            last_modified: f.last_modified,
1350            layers: f
1351                .layers
1352                .into_iter()
1353                .enumerate()
1354                .map(|(i, l)| {
1355                    let (name, version) = parse_layer_arn(&l.arn);
1356                    Layer {
1357                        merge_order: (i + 1).to_string(),
1358                        name,
1359                        layer_version: version,
1360                        compatible_runtimes: "-".to_string(),
1361                        compatible_architectures: "-".to_string(),
1362                        version_arn: l.arn,
1363                    }
1364                })
1365                .collect(),
1366        })
1367        .collect();
1368
1369    // Sort by last_modified DESC
1370    functions.sort_by(|a, b| b.last_modified.cmp(&a.last_modified));
1371
1372    app.lambda_state.table.items = functions;
1373
1374    Ok(())
1375}
1376
1377pub async fn load_lambda_applications(app: &mut App) -> anyhow::Result<()> {
1378    let applications = app.lambda_client.list_applications().await?;
1379    let mut applications: Vec<LambdaApplication> = applications
1380        .into_iter()
1381        .map(|a| LambdaApplication {
1382            name: a.name,
1383            arn: a.arn,
1384            description: a.description,
1385            status: a.status,
1386            last_modified: a.last_modified,
1387        })
1388        .collect();
1389    applications.sort_by(|a, b| b.last_modified.cmp(&a.last_modified));
1390    app.lambda_application_state.table.items = applications;
1391    Ok(())
1392}
1393
1394pub async fn load_lambda_versions(app: &mut App, function_name: &str) -> anyhow::Result<()> {
1395    let versions = app.lambda_client.list_versions(function_name).await?;
1396    let mut versions: Vec<Version> = versions
1397        .into_iter()
1398        .map(|v| Version {
1399            version: v.version,
1400            aliases: v.aliases,
1401            description: v.description,
1402            last_modified: v.last_modified,
1403            architecture: v.architecture,
1404        })
1405        .collect();
1406
1407    // Sort by version DESC (numeric sort)
1408    versions.sort_by(|a, b| {
1409        let a_num = a.version.parse::<i32>().unwrap_or(0);
1410        let b_num = b.version.parse::<i32>().unwrap_or(0);
1411        b_num.cmp(&a_num)
1412    });
1413
1414    app.lambda_state.version_table.items = versions;
1415    Ok(())
1416}
1417
1418pub async fn load_lambda_aliases(app: &mut App, function_name: &str) -> anyhow::Result<()> {
1419    let aliases = app.lambda_client.list_aliases(function_name).await?;
1420    let mut aliases: Vec<Alias> = aliases
1421        .into_iter()
1422        .map(|a| Alias {
1423            name: a.name,
1424            versions: a.versions,
1425            description: a.description,
1426        })
1427        .collect();
1428
1429    // Sort by name ASC
1430    aliases.sort_by(|a, b| a.name.cmp(&b.name));
1431
1432    app.lambda_state.alias_table.items = aliases;
1433    Ok(())
1434}
1435
1436pub async fn load_lambda_metrics(
1437    app: &mut App,
1438    function_name: &str,
1439    version: Option<&str>,
1440) -> anyhow::Result<()> {
1441    use rusticity_core::lambda::Statistic;
1442
1443    // Build resource string if version is provided (e.g., "function_name:1")
1444    let resource = version.map(|v| format!("{}:{}", function_name, v));
1445    let resource_ref = resource.as_deref();
1446
1447    let invocations = app
1448        .lambda_client
1449        .get_invocations_metric(function_name, resource_ref)
1450        .await?;
1451    app.lambda_state.metric_data_invocations = invocations.clone();
1452
1453    let duration_min = app
1454        .lambda_client
1455        .get_duration_metric(function_name, Statistic::Minimum)
1456        .await?;
1457    app.lambda_state.metric_data_duration_min = duration_min;
1458
1459    let duration_avg = app
1460        .lambda_client
1461        .get_duration_metric(function_name, Statistic::Average)
1462        .await?;
1463    app.lambda_state.metric_data_duration_avg = duration_avg;
1464
1465    let duration_max = app
1466        .lambda_client
1467        .get_duration_metric(function_name, Statistic::Maximum)
1468        .await?;
1469    app.lambda_state.metric_data_duration_max = duration_max;
1470
1471    let errors = app.lambda_client.get_errors_metric(function_name).await?;
1472    app.lambda_state.metric_data_errors = errors.clone();
1473
1474    let mut success_rate = Vec::new();
1475    for (timestamp, error_count) in &errors {
1476        if let Some((_, invocation_count)) = invocations.iter().find(|(ts, _)| ts == timestamp) {
1477            let max_val = error_count.max(*invocation_count);
1478            if max_val > 0.0 {
1479                let rate = 100.0 - 100.0 * error_count / max_val;
1480                success_rate.push((*timestamp, rate));
1481            }
1482        }
1483    }
1484    app.lambda_state.metric_data_success_rate = success_rate;
1485
1486    let throttles = app
1487        .lambda_client
1488        .get_throttles_metric(function_name)
1489        .await?;
1490    app.lambda_state.metric_data_throttles = throttles;
1491
1492    let concurrent_executions = app
1493        .lambda_client
1494        .get_concurrent_executions_metric(function_name)
1495        .await?;
1496    app.lambda_state.metric_data_concurrent_executions = concurrent_executions;
1497
1498    let recursive_invocations_dropped = app
1499        .lambda_client
1500        .get_recursive_invocations_dropped_metric(function_name)
1501        .await?;
1502    app.lambda_state.metric_data_recursive_invocations_dropped = recursive_invocations_dropped;
1503
1504    let async_event_age_min = app
1505        .lambda_client
1506        .get_async_event_age_metric(function_name, Statistic::Minimum)
1507        .await?;
1508    app.lambda_state.metric_data_async_event_age_min = async_event_age_min;
1509
1510    let async_event_age_avg = app
1511        .lambda_client
1512        .get_async_event_age_metric(function_name, Statistic::Average)
1513        .await?;
1514    app.lambda_state.metric_data_async_event_age_avg = async_event_age_avg;
1515
1516    let async_event_age_max = app
1517        .lambda_client
1518        .get_async_event_age_metric(function_name, Statistic::Maximum)
1519        .await?;
1520    app.lambda_state.metric_data_async_event_age_max = async_event_age_max;
1521
1522    let async_events_received = app
1523        .lambda_client
1524        .get_async_events_received_metric(function_name)
1525        .await?;
1526    app.lambda_state.metric_data_async_events_received = async_events_received;
1527
1528    let async_events_dropped = app
1529        .lambda_client
1530        .get_async_events_dropped_metric(function_name)
1531        .await?;
1532    app.lambda_state.metric_data_async_events_dropped = async_events_dropped;
1533
1534    let destination_delivery_failures = app
1535        .lambda_client
1536        .get_destination_delivery_failures_metric(function_name)
1537        .await?;
1538    app.lambda_state.metric_data_destination_delivery_failures = destination_delivery_failures;
1539
1540    let dead_letter_errors = app
1541        .lambda_client
1542        .get_dead_letter_errors_metric(function_name)
1543        .await?;
1544    app.lambda_state.metric_data_dead_letter_errors = dead_letter_errors;
1545
1546    let iterator_age = app
1547        .lambda_client
1548        .get_iterator_age_metric(function_name)
1549        .await?;
1550    app.lambda_state.metric_data_iterator_age = iterator_age;
1551
1552    Ok(())
1553}
1554
1555pub fn render_application_detail(frame: &mut Frame, app: &App, area: Rect) {
1556    frame.render_widget(Clear, area);
1557
1558    let chunks = vertical(
1559        [
1560            Constraint::Length(1), // Application name
1561            Constraint::Length(1), // Tabs
1562            Constraint::Min(0),    // Content
1563        ],
1564        area,
1565    );
1566
1567    // Application name
1568    if let Some(app_name) = &app.lambda_application_state.current_application {
1569        frame.render_widget(Paragraph::new(app_name.as_str()), chunks[0]);
1570    }
1571
1572    // Tabs
1573    let tabs: Vec<(&str, ApplicationDetailTab)> = ApplicationDetailTab::ALL
1574        .iter()
1575        .map(|tab| (tab.name(), *tab))
1576        .collect();
1577    render_tabs(
1578        frame,
1579        chunks[1],
1580        &tabs,
1581        &app.lambda_application_state.detail_tab,
1582    );
1583
1584    // Content
1585    if app.lambda_application_state.detail_tab == ApplicationDetailTab::Overview {
1586        let chunks_content = vertical(
1587            [
1588                Constraint::Length(3), // Filter
1589                Constraint::Min(0),    // Table
1590            ],
1591            chunks[2],
1592        );
1593
1594        // Filter with pagination
1595        let view = app.lambda_application_state.resources.prepare_view(
1596            app.lambda_application_state
1597                .resources
1598                .items
1599                .iter()
1600                .collect(),
1601            TableWindow::SelectedPage,
1602        );
1603        let pagination = render_pagination_text(view.current_page, view.total_pages);
1604
1605        render_simple_filter(
1606            frame,
1607            chunks_content[0],
1608            SimpleFilterConfig {
1609                filter_text: &app.lambda_application_state.resources.filter,
1610                placeholder: "Filter by attributes or search by keyword",
1611                pagination: &pagination,
1612                mode: app.mode,
1613                is_input_focused: app.lambda_application_state.resource_input_focus
1614                    == InputFocus::Filter,
1615                is_pagination_focused: app.lambda_application_state.resource_input_focus
1616                    == InputFocus::Pagination,
1617            },
1618        );
1619
1620        // Resources table
1621        let title = format!(
1622            " Resources ({}) ",
1623            app.lambda_application_state.resources.items.len()
1624        );
1625
1626        let columns: Vec<Box<dyn TableColumn<Resource>>> = app
1627            .lambda_resource_visible_column_ids
1628            .iter()
1629            .filter_map(|col_id| {
1630                ResourceColumn::from_id(col_id)
1631                    .map(|col| Box::new(col) as Box<dyn TableColumn<Resource>>)
1632            })
1633            .collect();
1634        let config = TableConfig {
1635            items: view.items,
1636            selected_index: view.selected_index,
1637            expanded_index: view.expanded_index,
1638            columns: &columns,
1639            sort_column: "Logical ID",
1640            sort_direction: SortDirection::Asc,
1641            title,
1642            area: chunks_content[1],
1643            get_expanded_content: Some(Box::new(|res: &Resource| {
1644                plain_expanded_content(format!(
1645                    "Logical ID: {}\nPhysical ID: {}\nType: {}\nLast modified: {}",
1646                    res.logical_id, res.physical_id, res.resource_type, res.last_modified
1647                ))
1648            })),
1649            is_active: true,
1650        };
1651
1652        render_table(frame, config);
1653    } else if app.lambda_application_state.detail_tab == ApplicationDetailTab::Deployments {
1654        let chunks_content = vertical(
1655            [
1656                Constraint::Length(3), // Filter
1657                Constraint::Min(0),    // Table
1658            ],
1659            chunks[2],
1660        );
1661
1662        // Filter with pagination
1663        let view = app.lambda_application_state.deployments.prepare_view(
1664            app.lambda_application_state
1665                .deployments
1666                .items
1667                .iter()
1668                .collect(),
1669            TableWindow::SelectedPage,
1670        );
1671        let pagination = render_pagination_text(view.current_page, view.total_pages);
1672
1673        render_simple_filter(
1674            frame,
1675            chunks_content[0],
1676            SimpleFilterConfig {
1677                filter_text: &app.lambda_application_state.deployments.filter,
1678                placeholder: "Filter by attributes or search by keyword",
1679                pagination: &pagination,
1680                mode: app.mode,
1681                is_input_focused: app.lambda_application_state.deployment_input_focus
1682                    == InputFocus::Filter,
1683                is_pagination_focused: app.lambda_application_state.deployment_input_focus
1684                    == InputFocus::Pagination,
1685            },
1686        );
1687
1688        // Table
1689        let title = format!(
1690            " Deployment history ({}) ",
1691            app.lambda_application_state.deployments.items.len()
1692        );
1693
1694        let columns: Vec<Box<dyn TableColumn<Deployment>>> = app
1695            .lambda_deployment_visible_column_ids
1696            .iter()
1697            .filter_map(|id| {
1698                DeploymentColumn::from_id(id)
1699                    .map(|column| Box::new(column) as Box<dyn TableColumn<Deployment>>)
1700            })
1701            .collect();
1702
1703        let config = TableConfig {
1704            items: view.items,
1705            selected_index: view.selected_index,
1706            expanded_index: view.expanded_index,
1707            columns: &columns,
1708            sort_column: "",
1709            sort_direction: SortDirection::Asc,
1710            title,
1711            area: chunks_content[1],
1712            get_expanded_content: Some(Box::new(|dep: &Deployment| {
1713                plain_expanded_content(format!(
1714                    "Deployment: {}\nResource type: {}\nLast updated: {}\nStatus: {}",
1715                    dep.deployment_id, dep.resource_type, dep.last_updated, dep.status
1716                ))
1717            })),
1718            is_active: true,
1719        };
1720
1721        render_table(frame, config);
1722    }
1723}
1724
1725fn render_lambda_monitoring_charts(frame: &mut Frame, app: &App, area: Rect) {
1726    use crate::ui::monitoring::{
1727        render_monitoring_tab, DualAxisChart, MetricChart, MultiDatasetChart,
1728    };
1729
1730    // Calculate all labels (same logic as inline version)
1731    let invocations_sum: f64 = app
1732        .lambda_state
1733        .metric_data_invocations
1734        .iter()
1735        .map(|(_, v)| v)
1736        .sum();
1737    let invocations_label = format!("Invocations [sum: {:.0}]", invocations_sum);
1738
1739    let duration_min: f64 = app
1740        .lambda_state
1741        .metric_data_duration_min
1742        .iter()
1743        .map(|(_, v)| v)
1744        .fold(f64::INFINITY, |a, &b| a.min(b));
1745    let duration_avg: f64 = if !app.lambda_state.metric_data_duration_avg.is_empty() {
1746        app.lambda_state
1747            .metric_data_duration_avg
1748            .iter()
1749            .map(|(_, v)| v)
1750            .sum::<f64>()
1751            / app.lambda_state.metric_data_duration_avg.len() as f64
1752    } else {
1753        0.0
1754    };
1755    let duration_max: f64 = app
1756        .lambda_state
1757        .metric_data_duration_max
1758        .iter()
1759        .map(|(_, v)| v)
1760        .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1761    let duration_label = format!(
1762        "Minimum [{:.0}], Average [{:.0}], Maximum [{:.0}]",
1763        if duration_min.is_finite() {
1764            duration_min
1765        } else {
1766            0.0
1767        },
1768        duration_avg,
1769        if duration_max.is_finite() {
1770            duration_max
1771        } else {
1772            0.0
1773        }
1774    );
1775
1776    let async_event_age_min: f64 = app
1777        .lambda_state
1778        .metric_data_async_event_age_min
1779        .iter()
1780        .map(|(_, v)| v)
1781        .fold(f64::INFINITY, |a, &b| a.min(b));
1782    let async_event_age_avg: f64 = if !app.lambda_state.metric_data_async_event_age_avg.is_empty() {
1783        app.lambda_state
1784            .metric_data_async_event_age_avg
1785            .iter()
1786            .map(|(_, v)| v)
1787            .sum::<f64>()
1788            / app.lambda_state.metric_data_async_event_age_avg.len() as f64
1789    } else {
1790        0.0
1791    };
1792    let async_event_age_max: f64 = app
1793        .lambda_state
1794        .metric_data_async_event_age_max
1795        .iter()
1796        .map(|(_, v)| v)
1797        .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1798    let async_event_age_label = format!(
1799        "Minimum [{:.0}], Average [{:.0}], Maximum [{:.0}]",
1800        if async_event_age_min.is_finite() {
1801            async_event_age_min
1802        } else {
1803            0.0
1804        },
1805        async_event_age_avg,
1806        if async_event_age_max.is_finite() {
1807            async_event_age_max
1808        } else {
1809            0.0
1810        }
1811    );
1812
1813    let async_events_received_sum: f64 = app
1814        .lambda_state
1815        .metric_data_async_events_received
1816        .iter()
1817        .map(|(_, v)| v)
1818        .sum();
1819    let async_events_dropped_sum: f64 = app
1820        .lambda_state
1821        .metric_data_async_events_dropped
1822        .iter()
1823        .map(|(_, v)| v)
1824        .sum();
1825    let async_events_label = format!(
1826        "Received [sum: {:.0}], Dropped [sum: {:.0}]",
1827        async_events_received_sum, async_events_dropped_sum
1828    );
1829
1830    let destination_delivery_failures_sum: f64 = app
1831        .lambda_state
1832        .metric_data_destination_delivery_failures
1833        .iter()
1834        .map(|(_, v)| v)
1835        .sum();
1836    let dead_letter_errors_sum: f64 = app
1837        .lambda_state
1838        .metric_data_dead_letter_errors
1839        .iter()
1840        .map(|(_, v)| v)
1841        .sum();
1842    let async_delivery_failures_label = format!(
1843        "Destination delivery failures [sum: {:.0}], Dead letter queue failures [sum: {:.0}]",
1844        destination_delivery_failures_sum, dead_letter_errors_sum
1845    );
1846
1847    let iterator_age_max: f64 = app
1848        .lambda_state
1849        .metric_data_iterator_age
1850        .iter()
1851        .map(|(_, v)| v)
1852        .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1853    let iterator_age_label = format!(
1854        "Maximum [{}]",
1855        if iterator_age_max.is_finite() {
1856            format!("{:.0}", iterator_age_max)
1857        } else {
1858            "--".to_string()
1859        }
1860    );
1861
1862    let error_max: f64 = app
1863        .lambda_state
1864        .metric_data_errors
1865        .iter()
1866        .map(|(_, v)| v)
1867        .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1868    let success_rate_min: f64 = app
1869        .lambda_state
1870        .metric_data_success_rate
1871        .iter()
1872        .map(|(_, v)| v)
1873        .fold(f64::INFINITY, |a, &b| a.min(b));
1874    let error_label = format!(
1875        "Errors [max: {:.0}] and Success rate [min: {:.0}%]",
1876        if error_max.is_finite() {
1877            error_max
1878        } else {
1879            0.0
1880        },
1881        if success_rate_min.is_finite() {
1882            success_rate_min
1883        } else {
1884            0.0
1885        }
1886    );
1887
1888    let throttles_max: f64 = app
1889        .lambda_state
1890        .metric_data_throttles
1891        .iter()
1892        .map(|(_, v)| v)
1893        .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1894    let throttles_label = format!(
1895        "Throttles [max: {:.0}]",
1896        if throttles_max.is_finite() {
1897            throttles_max
1898        } else {
1899            0.0
1900        }
1901    );
1902
1903    let concurrent_max: f64 = app
1904        .lambda_state
1905        .metric_data_concurrent_executions
1906        .iter()
1907        .map(|(_, v)| v)
1908        .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1909    let concurrent_label = format!(
1910        "Concurrent executions [max: {}]",
1911        if concurrent_max.is_finite() {
1912            format!("{:.0}", concurrent_max)
1913        } else {
1914            "--".to_string()
1915        }
1916    );
1917
1918    let recursive_sum: f64 = app
1919        .lambda_state
1920        .metric_data_recursive_invocations_dropped
1921        .iter()
1922        .map(|(_, v)| v)
1923        .sum();
1924    let recursive_label = format!(
1925        "Dropped [sum: {}]",
1926        if recursive_sum > 0.0 {
1927            format!("{:.0}", recursive_sum)
1928        } else {
1929            "--".to_string()
1930        }
1931    );
1932
1933    render_monitoring_tab(
1934        frame,
1935        area,
1936        &[MetricChart {
1937            title: "Invocations",
1938            data: &app.lambda_state.metric_data_invocations,
1939            y_axis_label: "Count",
1940            x_axis_label: Some(invocations_label),
1941            ..Default::default()
1942        }],
1943        &[MultiDatasetChart {
1944            title: "Duration",
1945            datasets: vec![
1946                ("Minimum", &app.lambda_state.metric_data_duration_min),
1947                ("Average", &app.lambda_state.metric_data_duration_avg),
1948                ("Maximum", &app.lambda_state.metric_data_duration_max),
1949            ],
1950            y_axis_label: "Milliseconds",
1951            y_axis_step: 1000,
1952            x_axis_label: Some(duration_label),
1953        }],
1954        &[DualAxisChart {
1955            title: "Error count and success rate",
1956            left_dataset: ("Errors", &app.lambda_state.metric_data_errors),
1957            right_dataset: ("Success rate", &app.lambda_state.metric_data_success_rate),
1958            left_y_label: "Count",
1959            right_y_label: "%",
1960            x_axis_label: Some(error_label),
1961        }],
1962        &[
1963            MetricChart {
1964                title: "Throttles",
1965                data: &app.lambda_state.metric_data_throttles,
1966                y_axis_label: "Count",
1967                x_axis_label: Some(throttles_label),
1968                ..Default::default()
1969            },
1970            MetricChart {
1971                title: "Total concurrent executions",
1972                data: &app.lambda_state.metric_data_concurrent_executions,
1973                y_axis_label: "Count",
1974                x_axis_label: Some(concurrent_label),
1975                ..Default::default()
1976            },
1977            MetricChart {
1978                title: "Recursive invocations",
1979                data: &app.lambda_state.metric_data_recursive_invocations_dropped,
1980                y_axis_label: "Count",
1981                x_axis_label: Some(recursive_label),
1982                ..Default::default()
1983            },
1984            MetricChart {
1985                title: "Async event age",
1986                data: &app.lambda_state.metric_data_async_event_age_avg,
1987                y_axis_label: "Milliseconds",
1988                x_axis_label: Some(async_event_age_label),
1989                ..Default::default()
1990            },
1991            MetricChart {
1992                title: "Async events",
1993                data: &app.lambda_state.metric_data_async_events_received,
1994                y_axis_label: "Count",
1995                x_axis_label: Some(async_events_label),
1996                ..Default::default()
1997            },
1998            MetricChart {
1999                title: "Async delivery failures",
2000                data: &app.lambda_state.metric_data_destination_delivery_failures,
2001                y_axis_label: "Count",
2002                x_axis_label: Some(async_delivery_failures_label),
2003                ..Default::default()
2004            },
2005            MetricChart {
2006                title: "Iterator age",
2007                data: &app.lambda_state.metric_data_iterator_age,
2008                y_axis_label: "Milliseconds",
2009                x_axis_label: Some(iterator_age_label),
2010                ..Default::default()
2011            },
2012        ],
2013        app.lambda_state.monitoring_scroll,
2014    );
2015}
2016
2017#[cfg(test)]
2018mod tests {
2019    use super::*;
2020
2021    #[test]
2022    fn test_detail_tab_monitoring_in_all() {
2023        let tabs = DetailTab::ALL;
2024        assert_eq!(tabs.len(), 5);
2025        assert_eq!(tabs[0], DetailTab::Code);
2026        assert_eq!(tabs[1], DetailTab::Monitor);
2027        assert_eq!(tabs[2], DetailTab::Configuration);
2028        assert_eq!(tabs[3], DetailTab::Aliases);
2029        assert_eq!(tabs[4], DetailTab::Versions);
2030    }
2031
2032    #[test]
2033    fn test_detail_tab_monitoring_name() {
2034        assert_eq!(DetailTab::Monitor.name(), "Monitor");
2035    }
2036
2037    #[test]
2038    fn test_detail_tab_monitoring_navigation() {
2039        let tab = DetailTab::Code;
2040        assert_eq!(tab.next(), DetailTab::Monitor);
2041
2042        let tab = DetailTab::Monitor;
2043        assert_eq!(tab.next(), DetailTab::Configuration);
2044        assert_eq!(tab.prev(), DetailTab::Code);
2045    }
2046
2047    #[test]
2048    fn test_state_monitoring_fields_initialized() {
2049        let state = State::new();
2050        assert_eq!(state.monitoring_scroll, 0);
2051        assert!(state.metric_data_invocations.is_empty());
2052        assert!(state.metric_data_duration_min.is_empty());
2053        assert!(state.metric_data_duration_avg.is_empty());
2054        assert!(state.metric_data_duration_max.is_empty());
2055        assert!(state.metric_data_errors.is_empty());
2056        assert!(state.metric_data_success_rate.is_empty());
2057        assert!(state.metric_data_throttles.is_empty());
2058        assert!(state.metric_data_concurrent_executions.is_empty());
2059        assert!(state.metric_data_recursive_invocations_dropped.is_empty());
2060    }
2061
2062    #[test]
2063    fn test_state_monitoring_scroll() {
2064        let mut state = State::new();
2065        assert_eq!(state.monitoring_scroll, 0);
2066
2067        state.monitoring_scroll = 1;
2068        assert_eq!(state.monitoring_scroll, 1);
2069
2070        state.monitoring_scroll = 2;
2071        assert_eq!(state.monitoring_scroll, 2);
2072    }
2073
2074    #[test]
2075    fn test_state_metric_data() {
2076        let mut state = State::new();
2077        state.metric_data_invocations = vec![(1700000000, 10.0), (1700000060, 15.0)];
2078        state.metric_data_duration_min = vec![(1700000000, 100.0), (1700000060, 150.0)];
2079        state.metric_data_duration_avg = vec![(1700000000, 200.0), (1700000060, 250.0)];
2080        state.metric_data_duration_max = vec![(1700000000, 300.0), (1700000060, 350.0)];
2081        state.metric_data_errors = vec![(1700000000, 1.0), (1700000060, 2.0)];
2082        state.metric_data_success_rate = vec![(1700000000, 90.0), (1700000060, 85.0)];
2083        state.metric_data_throttles = vec![(1700000000, 0.0), (1700000060, 1.0)];
2084        state.metric_data_concurrent_executions = vec![(1700000000, 5.0), (1700000060, 10.0)];
2085        state.metric_data_recursive_invocations_dropped =
2086            vec![(1700000000, 0.0), (1700000060, 0.0)];
2087
2088        assert_eq!(state.metric_data_invocations.len(), 2);
2089        assert_eq!(state.metric_data_duration_min.len(), 2);
2090        assert_eq!(state.metric_data_duration_avg.len(), 2);
2091        assert_eq!(state.metric_data_duration_max.len(), 2);
2092        assert_eq!(state.metric_data_errors.len(), 2);
2093        assert_eq!(state.metric_data_success_rate.len(), 2);
2094        assert_eq!(state.metric_data_throttles.len(), 2);
2095        assert_eq!(state.metric_data_concurrent_executions.len(), 2);
2096        assert_eq!(state.metric_data_recursive_invocations_dropped.len(), 2);
2097    }
2098
2099    #[test]
2100    fn test_invocations_sum_calculation() {
2101        let data = [(1700000000, 10.0), (1700000060, 15.0), (1700000120, 5.0)];
2102        let sum: f64 = data.iter().map(|(_, v)| v).sum();
2103        assert_eq!(sum, 30.0);
2104    }
2105
2106    #[test]
2107    fn test_invocations_label_format() {
2108        let sum = 1234.5;
2109        let label = format!("Invocations [sum: {:.0}]", sum);
2110        assert_eq!(label, "Invocations [sum: 1234]");
2111    }
2112
2113    #[test]
2114    fn test_invocations_sum_empty() {
2115        let data: Vec<(i64, f64)> = vec![];
2116        let sum: f64 = data.iter().map(|(_, v)| v).sum();
2117        assert_eq!(sum, 0.0);
2118    }
2119
2120    #[test]
2121    fn test_duration_label_formatting() {
2122        let min = 100.5;
2123        let avg = 250.7;
2124        let max = 450.2;
2125        let label = format!(
2126            "Minimum [{:.0}], Average [{:.0}], Maximum [{:.0}]",
2127            min, avg, max
2128        );
2129        assert_eq!(label, "Minimum [100], Average [251], Maximum [450]");
2130    }
2131
2132    #[test]
2133    fn test_duration_min_with_infinity() {
2134        let data: Vec<(i64, f64)> = vec![];
2135        let min: f64 = data
2136            .iter()
2137            .map(|(_, v)| v)
2138            .fold(f64::INFINITY, |a, &b| a.min(b));
2139        assert!(min.is_infinite());
2140        let result = if min.is_finite() { min } else { 0.0 };
2141        assert_eq!(result, 0.0);
2142    }
2143
2144    #[test]
2145    fn test_duration_max_with_neg_infinity() {
2146        let data: Vec<(i64, f64)> = vec![];
2147        let max: f64 = data
2148            .iter()
2149            .map(|(_, v)| v)
2150            .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
2151        assert!(max.is_infinite());
2152        let result = if max.is_finite() { max } else { 0.0 };
2153        assert_eq!(result, 0.0);
2154    }
2155
2156    #[test]
2157    fn test_duration_avg_empty_data() {
2158        let data: Vec<(i64, f64)> = vec![];
2159        let avg: f64 = if !data.is_empty() {
2160            data.iter().map(|(_, v)| v).sum::<f64>() / data.len() as f64
2161        } else {
2162            0.0
2163        };
2164        assert_eq!(avg, 0.0);
2165    }
2166
2167    #[test]
2168    fn test_duration_metrics_with_data() {
2169        let min_data = [(1700000000, 100.0), (1700000060, 90.0), (1700000120, 110.0)];
2170        let avg_data = [
2171            (1700000000, 200.0),
2172            (1700000060, 210.0),
2173            (1700000120, 190.0),
2174        ];
2175        let max_data = [
2176            (1700000000, 300.0),
2177            (1700000060, 320.0),
2178            (1700000120, 310.0),
2179        ];
2180
2181        let min: f64 = min_data
2182            .iter()
2183            .map(|(_, v)| v)
2184            .fold(f64::INFINITY, |a, &b| a.min(b));
2185        let avg: f64 = avg_data.iter().map(|(_, v)| v).sum::<f64>() / avg_data.len() as f64;
2186        let max: f64 = max_data
2187            .iter()
2188            .map(|(_, v)| v)
2189            .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
2190
2191        assert_eq!(min, 90.0);
2192        assert_eq!(avg, 200.0);
2193        assert_eq!(max, 320.0);
2194    }
2195
2196    #[test]
2197    fn test_success_rate_calculation() {
2198        let errors: f64 = 5.0;
2199        let invocations: f64 = 100.0;
2200        let max_val = errors.max(invocations);
2201        let success_rate = 100.0 - 100.0 * errors / max_val;
2202        assert_eq!(success_rate, 95.0);
2203    }
2204
2205    #[test]
2206    fn test_success_rate_with_zero_invocations() {
2207        let errors: f64 = 0.0;
2208        let invocations: f64 = 0.0;
2209        let max_val = errors.max(invocations);
2210        assert_eq!(max_val, 0.0);
2211    }
2212
2213    #[test]
2214    fn test_error_label_format() {
2215        let error_max = 10.0;
2216        let success_rate_min = 85.5;
2217        let label = format!(
2218            "Errors [max: {:.0}] and Success rate [min: {:.0}%]",
2219            error_max, success_rate_min
2220        );
2221        assert_eq!(label, "Errors [max: 10] and Success rate [min: 86%]");
2222    }
2223
2224    #[test]
2225    fn test_load_lambda_metrics_builds_resource_string() {
2226        // Test that version parameter creates correct resource format
2227        let function_name = "test-function";
2228        let version = Some("1");
2229        let resource = version.map(|v| format!("{}:{}", function_name, v));
2230        assert_eq!(resource, Some("test-function:1".to_string()));
2231
2232        // Test without version
2233        let version: Option<&str> = None;
2234        let resource = version.map(|v| format!("{}:{}", function_name, v));
2235        assert_eq!(resource, None);
2236    }
2237
2238    #[test]
2239    fn test_detail_tab_next_version_tab() {
2240        assert_eq!(VersionDetailTab::Code.next(), VersionDetailTab::Monitor);
2241        assert_eq!(
2242            VersionDetailTab::Monitor.next(),
2243            VersionDetailTab::Configuration
2244        );
2245        assert_eq!(
2246            VersionDetailTab::Configuration.next(),
2247            VersionDetailTab::Code
2248        );
2249    }
2250
2251    #[test]
2252    fn test_detail_tab_prev_version_tab() {
2253        assert_eq!(
2254            VersionDetailTab::Code.prev(),
2255            VersionDetailTab::Configuration
2256        );
2257        assert_eq!(
2258            VersionDetailTab::Configuration.prev(),
2259            VersionDetailTab::Monitor
2260        );
2261        assert_eq!(VersionDetailTab::Monitor.prev(), VersionDetailTab::Code);
2262    }
2263
2264    #[test]
2265    fn test_rounded_block_helper_usage() {
2266        use crate::ui::titled_block;
2267        use ratatui::prelude::Rect;
2268        // Verify rounded_block helper creates proper block structure
2269        let block = titled_block("Function overview");
2270        let area = Rect::new(0, 0, 100, 20);
2271        let inner = block.inner(area);
2272        assert_eq!(inner.width, 98); // 2 less for borders
2273        assert_eq!(inner.height, 18); // 2 less for borders
2274    }
2275
2276    #[test]
2277    fn test_overview_height_uses_dynamic_calculation() {
2278        use crate::ui::labeled_field;
2279        // Verify height is calculated based on column layout, not just field count
2280        let fields = vec![
2281            labeled_field("Name", "test-function"),
2282            labeled_field("Runtime", "python3.11"),
2283            labeled_field("Handler", "lambda_function.lambda_handler"),
2284            labeled_field("Memory", "128 MB"),
2285        ];
2286        let width = 200; // Wide enough for 2 columns
2287        let height = calculate_dynamic_height(&fields, width);
2288        // With 4 fields and wide width, should fit in 2 rows (2 columns)
2289        assert!(height <= 2, "Expected 2 rows or less, got {}", height);
2290    }
2291
2292    #[test]
2293    fn test_code_properties_uses_dynamic_height() {
2294        use crate::ui::labeled_field;
2295        // Verify Code properties and Runtime settings use dynamic height
2296        let code_lines = vec![
2297            labeled_field("Package size", "17.86 MB"),
2298            labeled_field("SHA256 hash", "abc123"),
2299            labeled_field("Last modified", "2025-12-11"),
2300        ];
2301        let width = 200;
2302        let height = calculate_dynamic_height(&code_lines, width);
2303        // With 3 fields and wide width, should pack into 2 rows or less
2304        assert!(
2305            height <= 2,
2306            "Expected 2 rows or less for code properties, got {}",
2307            height
2308        );
2309    }
2310}
2311
2312#[cfg(test)]
2313mod prepared_detail_tests {
2314    use super::*;
2315    use crate::common::PageSize;
2316    use ratatui::{backend::TestBackend, Terminal};
2317
2318    #[test]
2319    fn versions_and_aliases_use_prepared_page_bounds_and_expansion() {
2320        let mut app = App::new_without_client("test".into(), None);
2321        app.mode = Mode::Normal;
2322        app.lambda_state.current_function = Some("fn".into());
2323        app.lambda_state.version_table.page_size = PageSize::Ten;
2324        app.lambda_state.alias_table.page_size = PageSize::Ten;
2325        app.lambda_state.version_table.items = (0..25)
2326            .map(|i| Version {
2327                version: format!("v{i:02}"),
2328                aliases: String::new(),
2329                description: String::new(),
2330                last_modified: String::new(),
2331                architecture: String::new(),
2332            })
2333            .collect();
2334        app.lambda_state.alias_table.items = (0..25)
2335            .map(|i| Alias {
2336                name: format!("alias{i:02}"),
2337                versions: "1".into(),
2338                description: String::new(),
2339            })
2340            .collect();
2341        for (tab, name) in [(DetailTab::Versions, "v"), (DetailTab::Aliases, "alias")] {
2342            app.lambda_state.detail_tab = tab;
2343            app.lambda_state.version_table.selected = 12;
2344            app.lambda_state.alias_table.selected = 12;
2345            app.lambda_state.version_table.expanded_item = Some(11);
2346            app.lambda_state.alias_table.expanded_item = Some(11);
2347            let mut terminal = Terminal::new(TestBackend::new(240, 40)).unwrap();
2348            terminal
2349                .draw(|f| render_functions(f, &app, f.area()))
2350                .unwrap();
2351            let text: String = terminal
2352                .backend()
2353                .buffer()
2354                .content()
2355                .iter()
2356                .map(|c| c.symbol())
2357                .collect();
2358            assert!(text.contains("1 [2] 3"));
2359            assert!(text.contains(&format!("► {name}12")));
2360            assert!(text.contains(&format!("▼ {name}11")));
2361            assert!(!text.contains(&format!("{name}09")));
2362            assert!(!text.contains(&format!("{name}20")));
2363            app.lambda_state.version_table.filter = "absent".into();
2364            app.lambda_state.alias_table.filter = "absent".into();
2365            terminal
2366                .draw(|f| render_functions(f, &app, f.area()))
2367                .unwrap();
2368            app.lambda_state.version_table.filter.clear();
2369            app.lambda_state.alias_table.filter.clear();
2370        }
2371    }
2372}
2373
2374#[cfg(test)]
2375mod prepared_application_tests {
2376    use super::*;
2377    use crate::common::PageSize;
2378    use ratatui::{backend::TestBackend, Terminal};
2379
2380    #[test]
2381    fn application_resources_and_deployments_use_safe_page_relative_rows() {
2382        let mut app = App::new_without_client("test".into(), None);
2383        app.lambda_application_state.current_application = Some("application".into());
2384        app.lambda_application_state.resources.page_size = PageSize::Ten;
2385        app.lambda_application_state.deployments.page_size = PageSize::Ten;
2386        app.lambda_application_state.resources.items = (0..25)
2387            .map(|i| Resource {
2388                logical_id: format!("r{i:02}"),
2389                physical_id: String::new(),
2390                resource_type: String::new(),
2391                last_modified: String::new(),
2392            })
2393            .collect();
2394        app.lambda_application_state.deployments.items = (0..25)
2395            .map(|i| Deployment {
2396                deployment_id: format!("d{i:02}"),
2397                resource_type: "hidden-type".into(),
2398                last_updated: String::new(),
2399                status: String::new(),
2400            })
2401            .collect();
2402        app.lambda_deployment_visible_column_ids = vec![DeploymentColumn::Deployment.id()];
2403        let mut terminal = Terminal::new(TestBackend::new(160, 40)).unwrap();
2404        for (tab, prefix) in [
2405            (ApplicationDetailTab::Overview, "r"),
2406            (ApplicationDetailTab::Deployments, "d"),
2407        ] {
2408            app.lambda_application_state.detail_tab = tab;
2409            app.lambda_application_state.resources.selected = 12;
2410            app.lambda_application_state.deployments.selected = 12;
2411            app.lambda_application_state.resources.expanded_item = Some(11);
2412            app.lambda_application_state.deployments.expanded_item = Some(11);
2413            terminal
2414                .draw(|frame| render_application_detail(frame, &app, frame.area()))
2415                .unwrap();
2416            let text: String = terminal
2417                .backend()
2418                .buffer()
2419                .content()
2420                .iter()
2421                .map(|cell| cell.symbol())
2422                .collect();
2423            assert!(text.contains("1 [2] 3"));
2424            assert!(text.contains(&format!("► {prefix}12")));
2425            assert!(text.contains(&format!("▼ {prefix}11")));
2426            assert!(!text.contains(&format!("{prefix}09")));
2427            assert!(!text.contains(&format!("{prefix}20")));
2428        }
2429        app.lambda_application_state.deployments.expanded_item = None;
2430        terminal
2431            .draw(|frame| render_application_detail(frame, &app, frame.area()))
2432            .unwrap();
2433        let text: String = terminal
2434            .backend()
2435            .buffer()
2436            .content()
2437            .iter()
2438            .map(|cell| cell.symbol())
2439            .collect();
2440        assert!(
2441            !text.contains("hidden-type"),
2442            "deployment column visibility must be honored"
2443        );
2444        app.lambda_application_state.resources.items.clear();
2445        app.lambda_application_state.deployments.items.clear();
2446        for tab in [
2447            ApplicationDetailTab::Overview,
2448            ApplicationDetailTab::Deployments,
2449        ] {
2450            app.lambda_application_state.detail_tab = tab;
2451            terminal
2452                .draw(|frame| render_application_detail(frame, &app, frame.area()))
2453                .unwrap();
2454        }
2455    }
2456}