// SPDX-FileCopyrightText: 2025 The superseedr Contributors
// SPDX-License-Identifier: GPL-3.0-or-later

use crate::app::{
    refresh_torrent_preview_directory_priorities, App, AppCommand, AppMode, BrowserPane,
    BrowserSearchState, ConfigItem, ConfigUiState, DownloadSelectionTarget, FileBrowserMode,
    FileMetadata, FilePriority, SearchMode, TorrentPreviewPayload, AWAITING_MAGNET_METADATA_LABEL,
};
use crate::integrations::control::{ControlFilePriorityOverride, ControlRequest};
use crate::theme::ThemeContext;
use crate::tui::action_style::{footer_key_style, ActionTone};
use crate::tui::app_command::spawn_app_command_sender;
use crate::tui::formatters::{centered_rect, format_bytes, sanitize_text, truncate_with_ellipsis};
use crate::tui::layout::browser::calculate_file_browser_layout;
use crate::tui::screen_context::ScreenContext;
use crate::tui::screens::input_panel::draw_prompt_panel;
use crate::tui::tree::{
    RawNode, TreeAction, TreeFilter, TreeMathHelper, TreeProjection, TreeViewState,
};
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use ratatui::crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::prelude::{Alignment, Frame, Line, Modifier, Span, Style};
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph};
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use tokio::sync::{broadcast, mpsc};

const ASCII_TREE_DIR_ICON: &str = "> ";
const ASCII_TREE_FILE_ICON: &str = "  ";
const ASCII_TREE_ROOT_ICON: &str = "> ";

pub struct DownloadConfirmPayload {
    pub base_path: PathBuf,
    pub container_name_to_use: Option<String>,
    pub file_priorities: HashMap<usize, FilePriority>,
    pub target: DownloadSelectionTarget,
    pub has_preview_files: bool,
}

pub fn draw(
    f: &mut Frame,
    screen: &ScreenContext<'_>,
    state: &TreeViewState,
    data: &[RawNode<FileMetadata>],
    browser_mode: &FileBrowserMode,
) {
    let app_state = screen.ui;
    let ctx = screen.theme;

    let has_preview_content = has_preview_content(
        browser_mode,
        app_state.pending_torrent_path.is_some(),
        !app_state.pending_torrent_link.is_empty(),
        state.cursor_path.as_ref(),
    );
    let existing_priority_only = existing_torrent_priority_only(browser_mode);

    let preview_file_path = match browser_mode {
        FileBrowserMode::DownloadLocSelection { .. } => app_state.pending_torrent_path.as_ref(),
        FileBrowserMode::File(_) => state.cursor_path.as_ref(),
        _ => None,
    };

    let focused_pane = focused_pane(browser_mode);
    let search_panel_active = browser_search_panel_active(app_state.ui.file_browser.search_state);
    let max_area = centered_rect(90, 80, f.area());
    f.render_widget(Clear, max_area);

    let area = calculate_area(f.area(), has_preview_content);
    let layout = calculate_file_browser_layout(
        area,
        has_preview_content,
        search_panel_active,
        &focused_pane,
        existing_priority_only,
    );

    let (files_border_style, preview_border_style) =
        if let FileBrowserMode::DownloadLocSelection { focused_pane, .. } = browser_mode {
            match focused_pane {
                BrowserPane::FileSystem => (
                    ctx.apply(Style::default().fg(ctx.state_selected())),
                    ctx.apply(Style::default().fg(ctx.theme.semantic.surface2)),
                ),
                BrowserPane::TorrentPreview => (
                    ctx.apply(Style::default().fg(ctx.theme.semantic.surface2)),
                    ctx.apply(Style::default().fg(ctx.state_selected())),
                ),
            }
        } else {
            (
                ctx.apply(Style::default().fg(ctx.state_selected())),
                ctx.apply(Style::default().fg(ctx.accent_sapphire())),
            )
        };

    if let Some(preview_area) = layout.preview {
        let preview_filter = if matches!(focused_pane, BrowserPane::TorrentPreview) {
            build_torrent_preview_filter(
                &app_state.ui.file_browser.search_query,
                app_state.ui.file_browser.search_mode,
            )
        } else {
            TreeFilter::default()
        };
        draw_torrent_preview_panel(
            f,
            ctx,
            preview_area,
            TorrentPreviewPanelProps {
                path: preview_file_path.map(|p| p.as_path()),
                browser_mode,
                border_style: preview_border_style,
                current_fs_path: &state.current_path,
                preview_filter,
            },
        );
    }
    if let Some(search_area) = layout.search {
        draw_browser_search_panel(
            f,
            app_state.ui.file_browser.search_mode,
            &focused_pane,
            &app_state.ui.file_browser.search_query,
            search_area,
            ctx,
        );
    }

    let mut footer_spans = Vec::new();
    match browser_mode {
        FileBrowserMode::ConfigPathSelection { .. } | FileBrowserMode::Directory => {
            footer_spans.push(Span::styled(
                "[Arrows/Vim]",
                footer_key_style(ctx, ActionTone::Navigate),
            ));
            footer_spans.push(Span::raw(" Nav | "));
            footer_spans.push(Span::styled(
                "[Backspace]",
                footer_key_style(ctx, ActionTone::Navigate),
            ));
            footer_spans.push(Span::raw(" Up | "));
            footer_spans.push(Span::styled(
                "[Enter]",
                footer_key_style(ctx, ActionTone::Navigate),
            ));
            footer_spans.push(Span::raw(" Down | "));
            footer_spans.push(Span::styled(
                "[Y]",
                footer_key_style(ctx, ActionTone::Confirm),
            ));
            footer_spans.push(Span::raw(" Confirm Selection | "));
        }
        FileBrowserMode::DownloadLocSelection {
            focused_pane,
            use_container,
            ..
        } => {
            let edit_locked = pending_magnet_metadata_editing_locked(browser_mode);
            if !existing_priority_only {
                footer_spans.push(Span::styled(
                    "[Tab]",
                    footer_key_style(ctx, ActionTone::Mode),
                ));
                if search_panel_active {
                    footer_spans.push(Span::raw(" Search Mode | "));
                } else {
                    footer_spans.push(Span::raw(" Switch Pane | "));
                }
            }
            footer_spans.push(Span::styled("[/]", footer_key_style(ctx, ActionTone::Edit)));
            footer_spans.push(Span::raw(" Search | "));

            if existing_priority_only || matches!(focused_pane, BrowserPane::TorrentPreview) {
                footer_spans.push(Span::styled(
                    "[Space/p]",
                    footer_key_style(ctx, ActionTone::Toggle),
                ));
                footer_spans.push(Span::raw(" Priority | "));
                footer_spans.push(Span::styled(
                    "[P]",
                    footer_key_style(ctx, ActionTone::Toggle),
                ));
                footer_spans.push(Span::raw(" Priority All | "));
                footer_spans.push(Span::styled(
                    "[e]",
                    footer_key_style(ctx, ActionTone::Navigate),
                ));
                footer_spans.push(Span::raw(" Expand | "));
                footer_spans.push(Span::styled(
                    "[c]",
                    footer_key_style(ctx, ActionTone::Navigate),
                ));
                footer_spans.push(Span::raw(" Collapse | "));
            }

            if !existing_priority_only && !edit_locked {
                footer_spans.push(Span::styled(
                    "[x]",
                    footer_key_style(ctx, ActionTone::Toggle),
                ));
                footer_spans.push(Span::raw(" Container Folder | "));

                if *use_container {
                    footer_spans.push(Span::styled("[r]", footer_key_style(ctx, ActionTone::Edit)));
                    footer_spans.push(Span::raw(" Rename | "));
                }
            }

            footer_spans.push(Span::styled(
                "[Y]",
                footer_key_style(ctx, ActionTone::Confirm),
            ));
            footer_spans.push(Span::raw(" Confirm"));
        }
        FileBrowserMode::File(_) => {
            footer_spans.push(Span::styled(
                "[Y]",
                footer_key_style(ctx, ActionTone::Confirm),
            ));
            footer_spans.push(Span::raw(" Confirm File | "));
        }
    }
    footer_spans.push(Span::raw(" | "));
    footer_spans.push(Span::styled(
        "[Esc]",
        footer_key_style(ctx, ActionTone::Cancel),
    ));
    footer_spans.push(Span::raw(" Cancel"));

    let footer = Paragraph::new(Line::from(footer_spans))
        .alignment(Alignment::Center)
        .style(ctx.apply(Style::default().fg(ctx.theme.semantic.subtext1)));
    f.render_widget(footer, layout.footer);

    if existing_priority_only {
        return;
    }

    let inner_height = layout.list.height.saturating_sub(2) as usize;
    let list_width = layout.list.width.saturating_sub(2) as usize;
    let filesystem_search_query = if matches!(focused_pane, BrowserPane::FileSystem) {
        app_state.ui.file_browser.search_query.as_str()
    } else {
        ""
    };
    let filter = build_filesystem_filter(
        browser_mode,
        filesystem_search_query,
        app_state.ui.file_browser.search_mode,
    );

    let abs_path = state.current_path.to_string_lossy();
    let item_count = data.len();
    let count_label = if item_count == 0 {
        " (empty)".to_string()
    } else {
        format!(" ({} items)", item_count)
    };
    let left_title = format!(" {}/{} ", abs_path, count_label);
    let right_title = match browser_mode {
        FileBrowserMode::Directory => " Select Directory ".to_string(),
        FileBrowserMode::DownloadLocSelection { .. } => String::new(),
        FileBrowserMode::ConfigPathSelection { .. } => " Select Config Path ".to_string(),
        FileBrowserMode::File(exts) => format!(" Select File [{}] ", exts.join(", ")),
    };

    let visible_items = TreeMathHelper::get_visible_slice(data, state, filter, inner_height);
    let mut list_items = Vec::new();

    if data.is_empty() {
        list_items.push(ListItem::new(Line::from(vec![Span::styled(
            "   (Directory is empty)",
            ctx.apply(Style::default().fg(ctx.theme.semantic.overlay0))
                .italic(),
        )])));
    } else if visible_items.is_empty() {
        list_items.push(ListItem::new(Line::from(vec![Span::styled(
            format!("   (No matching files among {} items)", item_count),
            ctx.apply(Style::default().fg(ctx.theme.semantic.overlay0))
                .italic(),
        )])));
    } else {
        for item in visible_items {
            let is_cursor = item.is_cursor;
            let indent_str = "  ".repeat(item.depth);
            let indent_len = indent_str.len();
            let icon_str = if item.node.is_dir {
                ASCII_TREE_DIR_ICON
            } else {
                ASCII_TREE_FILE_ICON
            };
            let icon_len = ASCII_TREE_DIR_ICON.len();

            let (meta_str, meta_len) = if !item.node.is_dir {
                let datetime: chrono::DateTime<chrono::Local> = item.node.payload.modified.into();
                let size_str = format_bytes(item.node.payload.size);
                let s = format!(" {} ({})", size_str, datetime.format("%b %d %H:%M"));
                (s.clone(), s.len())
            } else {
                (String::new(), 0)
            };

            let fixed_used = indent_len + icon_len + meta_len + 1;
            let available_for_name = list_width.saturating_sub(fixed_used);
            let clean_name: String = item
                .node
                .name
                .chars()
                .map(|c| if c.is_control() { '?' } else { c })
                .collect();
            let display_name = truncate_with_ellipsis(&clean_name, available_for_name);

            let (icon_style, text_style) = if is_cursor {
                (
                    Style::default()
                        .fg(ctx.state_warning())
                        .add_modifier(Modifier::BOLD),
                    Style::default()
                        .fg(ctx.state_warning())
                        .add_modifier(Modifier::BOLD),
                )
            } else {
                let i_style = if item.node.is_dir {
                    ctx.apply(Style::default().fg(ctx.state_info()))
                } else {
                    ctx.apply(Style::default().fg(ctx.theme.semantic.text))
                };
                (
                    i_style,
                    ctx.apply(Style::default().fg(ctx.theme.semantic.text)),
                )
            };

            let mut line_spans = vec![
                Span::raw(indent_str),
                Span::styled(icon_str, icon_style),
                Span::styled(display_name, text_style),
            ];

            if !item.node.is_dir {
                line_spans.push(Span::raw(" "));
                line_spans.push(Span::styled(
                    meta_str,
                    ctx.apply(Style::default().fg(ctx.theme.semantic.surface2))
                        .italic(),
                ));
            }

            list_items.push(ListItem::new(Line::from(line_spans)));
        }
    }

    f.render_widget(
        List::new(list_items)
            .block(
                Block::default()
                    .title_top(
                        Line::from(Span::styled(
                            left_title,
                            Style::default().fg(ctx.state_selected()).bold(),
                        ))
                        .alignment(Alignment::Left),
                    )
                    .title_top(
                        Line::from(Span::styled(
                            right_title,
                            Style::default().fg(ctx.state_selected()).italic(),
                        ))
                        .alignment(Alignment::Right),
                    )
                    .borders(Borders::ALL)
                    .border_style(files_border_style),
            )
            .highlight_symbol("▶ "),
        layout.list,
    );
}

fn existing_torrent_priority_only(browser_mode: &FileBrowserMode) -> bool {
    matches!(
        browser_mode,
        FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::ExistingTorrent { .. },
            ..
        }
    )
}

fn browser_search_panel_active(search_state: BrowserSearchState) -> bool {
    search_state.is_visible()
}

fn draw_browser_search_panel(
    f: &mut Frame,
    search_mode: SearchMode,
    focused_pane: &BrowserPane,
    search_query: &str,
    area: Rect,
    ctx: &ThemeContext,
) {
    let title = match focused_pane {
        BrowserPane::TorrentPreview => " Torrent File Search ".to_string(),
        BrowserPane::FileSystem => " File Browser Search ".to_string(),
    };
    draw_prompt_panel(
        f,
        area,
        title,
        sanitize_text(search_query),
        browser_search_mode_spans(search_mode, ctx),
        ctx,
    );
}

fn browser_search_mode_spans(search_mode: SearchMode, ctx: &ThemeContext) -> Vec<Span<'static>> {
    let (fuzzy_style, regex_style) = match search_mode {
        SearchMode::Fuzzy => (
            ctx.apply(Style::default().fg(ctx.state_selected()).bold()),
            ctx.apply(Style::default().fg(ctx.theme.semantic.overlay0)),
        ),
        SearchMode::Regex => (
            ctx.apply(Style::default().fg(ctx.theme.semantic.overlay0)),
            ctx.apply(Style::default().fg(ctx.state_selected()).bold()),
        ),
    };
    vec![
        Span::raw("  "),
        Span::styled("Fuzzy", fuzzy_style),
        Span::raw(" / "),
        Span::styled("Regex", regex_style),
        Span::raw("  Tab"),
    ]
}

struct TorrentPreviewPanelProps<'a> {
    path: Option<&'a Path>,
    browser_mode: &'a FileBrowserMode,
    border_style: Style,
    current_fs_path: &'a Path,
    preview_filter: TreeFilter<TorrentPreviewPayload>,
}

fn draw_torrent_preview_panel(
    f: &mut Frame,
    ctx: &ThemeContext,
    area: Rect,
    props: TorrentPreviewPanelProps<'_>,
) {
    let TorrentPreviewPanelProps {
        path,
        browser_mode,
        border_style,
        current_fs_path,
        preview_filter,
    } = props;
    let is_narrow = area.width < 50;
    let raw_title = "Torrent Preview";
    let avail_width = area.width.saturating_sub(4) as usize;
    let title = if is_narrow {
        truncate_with_ellipsis("Preview", avail_width)
    } else {
        truncate_with_ellipsis(raw_title, avail_width)
    };

    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(border_style)
        .title(title);

    let inner_area = block.inner(area);
    f.render_widget(block, area);

    if let FileBrowserMode::DownloadLocSelection {
        preview_tree,
        preview_state,
        container_name,
        use_container,
        is_editing_name,
        cursor_pos,
        ..
    } = browser_mode
    {
        let priority_only = existing_torrent_priority_only(browser_mode);
        let header_lines = if *use_container && !priority_only {
            2
        } else {
            1
        };
        let list_height = inner_area.height.saturating_sub(header_lines) as usize;

        let visible_rows = TreeMathHelper::get_visible_slice(
            preview_tree,
            preview_state,
            preview_filter,
            list_height,
        );

        let mut list_items = Vec::new();
        let root_style = Style::default()
            .fg(ctx.state_info())
            .add_modifier(Modifier::BOLD);

        let path_display = if priority_only {
            "Priority editor (location unchanged)".to_string()
        } else if is_narrow {
            current_fs_path
                .file_name()
                .map(|n| n.to_string_lossy().to_string())
                .unwrap_or_else(|| "/".to_string())
        } else {
            current_fs_path.to_string_lossy().to_string()
        };

        list_items.push(ListItem::new(Line::from(vec![
            Span::styled(ASCII_TREE_ROOT_ICON, root_style),
            Span::styled(path_display, root_style),
        ])));

        if *use_container && !priority_only {
            let container_style = if *is_editing_name {
                Style::default()
                    .fg(ctx.accent_sky())
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::default()
                    .fg(ctx.state_selected())
                    .add_modifier(Modifier::BOLD)
            };

            let mut spans = vec![
                Span::raw("  "),
                Span::styled(ASCII_TREE_ROOT_ICON, container_style),
            ];

            if *is_editing_name {
                let split_pos = clamp_to_char_boundary(container_name, *cursor_pos);
                let (before, after) = container_name.split_at(split_pos);
                spans.push(Span::styled(before, container_style));
                spans.push(Span::styled(
                    "█",
                    Style::default()
                        .fg(ctx.accent_sky())
                        .add_modifier(Modifier::SLOW_BLINK),
                ));
                spans.push(Span::styled(after, container_style));
            } else {
                spans.push(Span::styled(container_name.clone(), container_style));
                if !is_narrow {
                    spans.push(Span::styled(
                        " (New)",
                        Style::default()
                            .fg(ctx.theme.semantic.surface2)
                            .add_modifier(Modifier::ITALIC),
                    ));
                }
            }
            list_items.push(ListItem::new(Line::from(spans)));
        }

        let tree_items: Vec<ListItem> = visible_rows
            .iter()
            .map(|item| {
                let is_cursor = item.is_cursor;
                let base_indent_level = if *use_container && !priority_only {
                    2
                } else {
                    1
                };
                let indent_multiplier = if is_narrow { 1 } else { 2 };
                let indent_str = " ".repeat((base_indent_level + item.depth) * indent_multiplier);

                let icon = if item.node.is_dir {
                    ASCII_TREE_DIR_ICON
                } else {
                    ASCII_TREE_FILE_ICON
                };

                let (base_content_style, tag) = match item.node.payload.priority {
                    FilePriority::Skip => (
                        Style::default()
                            .fg(ctx.theme.semantic.surface1)
                            .add_modifier(Modifier::CROSSED_OUT),
                        "[S] ",
                    ),
                    FilePriority::High => (
                        Style::default()
                            .fg(ctx.state_success())
                            .add_modifier(Modifier::BOLD),
                        "[H] ",
                    ),
                    FilePriority::Mixed => (
                        Style::default()
                            .fg(ctx.state_warning())
                            .add_modifier(Modifier::ITALIC),
                        "[*] ",
                    ),
                    FilePriority::Normal => (
                        if item.node.is_dir {
                            ctx.apply(Style::default().fg(ctx.state_info()))
                        } else {
                            ctx.apply(Style::default().fg(ctx.theme.semantic.text))
                        },
                        "",
                    ),
                };
                let final_content_style = if is_cursor {
                    base_content_style
                        .add_modifier(Modifier::BOLD)
                        .add_modifier(Modifier::UNDERLINED)
                } else {
                    base_content_style
                };

                let structure_style = final_content_style
                    .remove_modifier(Modifier::CROSSED_OUT)
                    .remove_modifier(Modifier::UNDERLINED);
                let mut spans = vec![
                    Span::styled(indent_str, structure_style),
                    Span::styled(icon, structure_style),
                    Span::styled(&item.node.name, final_content_style),
                ];

                if !item.node.is_dir {
                    if !is_narrow {
                        spans.push(Span::styled(
                            format!(" ({}) ", format_bytes(item.node.payload.size)),
                            structure_style,
                        ));
                    }
                    if !tag.is_empty() {
                        spans.push(Span::styled(tag, structure_style));
                    }
                }
                ListItem::new(Line::from(spans))
            })
            .collect();

        list_items.extend(tree_items);
        f.render_widget(List::new(list_items), inner_area);
        return;
    }

    if let Some(p) = path {
        let file_bytes = match std::fs::read(p) {
            Ok(b) => b,
            Err(e) => {
                f.render_widget(
                    Paragraph::new(format!("Read Error: {}", e))
                        .style(ctx.apply(Style::default().fg(ctx.state_error()))),
                    inner_area,
                );
                return;
            }
        };

        let torrent = match crate::torrent_file::parser::from_bytes(&file_bytes) {
            Ok(t) => t,
            Err(e) => {
                f.render_widget(
                    Paragraph::new(format!("Invalid Torrent: {}", e))
                        .style(ctx.apply(Style::default().fg(ctx.state_error()))),
                    inner_area,
                );
                return;
            }
        };

        let total_size = torrent.info.total_length();
        let protocol_version = match torrent.info.meta_version {
            Some(2) => {
                if !torrent.info.pieces.is_empty() {
                    "BitTorrent v2 (Hybrid)"
                } else {
                    "BitTorrent v2 (Pure)"
                }
            }
            _ => "BitTorrent v1",
        };
        let info_text = vec![
            Line::from(vec![
                Span::styled(
                    "Name: ",
                    ctx.apply(Style::default().fg(ctx.theme.semantic.subtext0)),
                ),
                Span::raw(&torrent.info.name),
            ]),
            Line::from(vec![
                Span::styled(
                    "Protocol: ",
                    ctx.apply(Style::default().fg(ctx.theme.semantic.subtext0)),
                ),
                Span::styled(
                    protocol_version,
                    Style::default().fg(ctx.state_selected()).bold(),
                ),
            ]),
            Line::from(vec![
                Span::styled(
                    "Size: ",
                    ctx.apply(Style::default().fg(ctx.theme.semantic.subtext0)),
                ),
                Span::raw(format_bytes(total_size as u64)),
            ]),
        ];

        let layout = Layout::vertical([
            Constraint::Length(info_text.len() as u16 + 1),
            Constraint::Min(0),
        ])
        .split(inner_area);
        f.render_widget(
            Paragraph::new(info_text).block(
                Block::default()
                    .borders(Borders::BOTTOM)
                    .border_style(ctx.apply(Style::default().fg(ctx.theme.semantic.border))),
            ),
            layout[0],
        );

        let file_list_payloads: Vec<(Vec<String>, TorrentPreviewPayload)> = torrent
            .file_list()
            .into_iter()
            .map(|(path, size)| {
                (
                    path,
                    TorrentPreviewPayload {
                        file_index: None,
                        size,
                        priority: FilePriority::Normal,
                    },
                )
            })
            .collect();

        let final_nodes = RawNode::from_path_list(None, file_list_payloads);
        let mut temp_state = TreeViewState::default();
        for node in &final_nodes {
            node.expand_all(&mut temp_state);
        }

        let visible_rows = TreeMathHelper::get_visible_slice(
            &final_nodes,
            &temp_state,
            TreeFilter::default(),
            layout[1].height as usize,
        );

        let list_items: Vec<ListItem> = visible_rows
            .iter()
            .map(|item| {
                let indent = if is_narrow {
                    " ".repeat(item.depth)
                } else {
                    "  ".repeat(item.depth)
                };
                let icon = if item.node.is_dir {
                    ASCII_TREE_DIR_ICON
                } else {
                    ASCII_TREE_FILE_ICON
                };
                let style = if item.node.is_dir {
                    ctx.apply(Style::default().fg(ctx.state_info()))
                } else {
                    ctx.apply(Style::default().fg(ctx.theme.semantic.text))
                };
                let mut spans = vec![
                    Span::raw(indent),
                    Span::styled(icon, style),
                    Span::styled(&item.node.name, style),
                ];
                if !item.node.is_dir && !is_narrow {
                    spans.push(Span::styled(
                        format!(" ({})", format_bytes(item.node.payload.size)),
                        ctx.apply(Style::default().fg(ctx.theme.semantic.surface2)),
                    ));
                }
                ListItem::new(Line::from(spans))
            })
            .collect();

        f.render_widget(List::new(list_items), layout[1]);
    }
}

pub async fn handle_event(event: CrosstermEvent, app: &mut App) {
    if !matches!(app.app_state.mode, AppMode::FileBrowser) {
        return;
    }

    if let CrosstermEvent::Key(key) = event {
        if key.kind == KeyEventKind::Press {
            if handle_browser_search_key(key, app) {
                return;
            }

            if handle_browser_download_key(key.code, app).await {
                return;
            }

            let _ = handle_browser_common_key(key.code, app).await;
        }
    }
}

fn handle_browser_search_key(key: KeyEvent, app: &mut App) -> bool {
    let file_browser = &mut app.app_state.ui.file_browser;
    if matches!(key.code, KeyCode::Tab) && browser_search_panel_active(file_browser.search_state) {
        toggle_browser_search_mode(&mut file_browser.search_mode);
        reset_active_browser_search_view(&mut file_browser.browser_mode, &mut file_browser.state);
        app.app_state.ui.needs_redraw = true;
        return true;
    }

    if let Some(action) = map_search_key_to_browser_action(key, file_browser.search_state) {
        let reset_view = browser_action_resets_search_view(action);
        let reduced = reduce_browser_action(
            action,
            &mut file_browser.search_state,
            &mut file_browser.search_query,
            &mut file_browser.search_mode,
        );
        if reset_view {
            reset_active_browser_search_view(
                &mut file_browser.browser_mode,
                &mut file_browser.state,
            );
        }
        if reduced.redraw {
            app.app_state.ui.needs_redraw = true;
        }
        return true;
    }
    false
}

async fn handle_browser_download_key(key_code: KeyCode, app: &mut App) -> bool {
    let consumed_download_input = {
        let browser_mode = &mut app.app_state.ui.file_browser.browser_mode;
        if let Some(action) = map_download_key_to_action(key_code, browser_mode) {
            reduce_browser_download_action(action, browser_mode).consumed
        } else {
            false
        }
    };
    if consumed_download_input {
        return true;
    }

    if !matches!(
        app.app_state.ui.file_browser.browser_mode,
        FileBrowserMode::DownloadLocSelection { .. }
    ) {
        return false;
    }

    if preview_search_should_start(key_code, &app.app_state.ui.file_browser.browser_mode) {
        start_browser_search(
            &mut app.app_state.ui.file_browser.search_state,
            &mut app.app_state.ui.file_browser.search_query,
            &mut app.app_state.ui.file_browser.search_mode,
        );
        reset_active_browser_search_view(
            &mut app.app_state.ui.file_browser.browser_mode,
            &mut app.app_state.ui.file_browser.state,
        );
        app.app_state.ui.needs_redraw = true;
        return true;
    }

    if key_code == KeyCode::Esc {
        let reduced = {
            let file_browser = &app.app_state.ui.file_browser;
            reduce_browser_dialog_action(
                BrowserDialogAction::CancelDownloadSelection,
                &file_browser.state,
                &file_browser.browser_mode,
                !app.app_state.pending_torrent_link.is_empty(),
            )
        };
        execute_browser_dialog_effects(app, reduced.effects).await;
        return true;
    }

    let screen_area = app.app_state.screen_area;
    let search_state = app.app_state.ui.file_browser.search_state;
    let search_query = app.app_state.ui.file_browser.search_query.clone();
    let search_panel_active = browser_search_panel_active(search_state);
    let search_mode = app.app_state.ui.file_browser.search_mode;
    let consumed_preview_input = {
        let browser_mode = &mut app.app_state.ui.file_browser.browser_mode;
        if let FileBrowserMode::DownloadLocSelection {
            use_container,
            focused_pane,
            preview_tree,
            preview_state,
            target,
            ..
        } = browser_mode
        {
            if matches!(focused_pane, BrowserPane::TorrentPreview) {
                let preview_only =
                    matches!(target, DownloadSelectionTarget::ExistingTorrent { .. });
                let list_height = calculate_preview_list_height(
                    screen_area,
                    search_panel_active,
                    focused_pane,
                    *use_container,
                    preview_only,
                );
                reduce_browser_preview_action(
                    map_preview_key_to_action(key_code),
                    preview_state,
                    preview_tree,
                    build_torrent_preview_filter(&search_query, search_mode),
                    list_height,
                )
                .consumed
            } else {
                false
            }
        } else {
            false
        }
    };
    if consumed_preview_input {
        return true;
    }

    false
}

fn preview_search_should_start(key_code: KeyCode, browser_mode: &FileBrowserMode) -> bool {
    matches!(key_code, KeyCode::Char('/'))
        && matches!(
            browser_mode,
            FileBrowserMode::DownloadLocSelection {
                focused_pane: BrowserPane::TorrentPreview,
                is_editing_name: false,
                ..
            }
        )
}

fn start_browser_search(
    search_state: &mut BrowserSearchState,
    search_query: &mut String,
    search_mode: &mut SearchMode,
) {
    *search_state = BrowserSearchState::Editing;
    search_query.clear();
    *search_mode = SearchMode::Regex;
}

fn clear_browser_search(search_state: &mut BrowserSearchState, search_query: &mut String) {
    *search_state = BrowserSearchState::Closed;
    search_query.clear();
}

fn toggle_browser_search_mode(search_mode: &mut SearchMode) {
    *search_mode = match *search_mode {
        SearchMode::Fuzzy => SearchMode::Regex,
        SearchMode::Regex => SearchMode::Fuzzy,
    };
}

fn browser_action_resets_search_view(action: BrowserAction) -> bool {
    matches!(
        action,
        BrowserAction::Backspace | BrowserAction::ToggleSearchMode | BrowserAction::Char(_)
    )
}

fn reset_active_browser_search_view(
    browser_mode: &mut FileBrowserMode,
    filesystem_state: &mut TreeViewState,
) {
    match browser_mode {
        FileBrowserMode::DownloadLocSelection {
            focused_pane: BrowserPane::TorrentPreview,
            preview_state,
            ..
        } => {
            preview_state.top_most_offset = 0;
        }
        _ => {
            filesystem_state.top_most_offset = 0;
        }
    }
}

async fn handle_browser_common_key(key_code: KeyCode, app: &mut App) -> bool {
    let list_height = {
        let file_browser = &app.app_state.ui.file_browser;
        let has_preview = has_preview_content(
            &file_browser.browser_mode,
            app.app_state.pending_torrent_path.is_some(),
            !app.app_state.pending_torrent_link.is_empty(),
            file_browser.state.cursor_path.as_ref(),
        );
        let pane = focused_pane(&file_browser.browser_mode);
        let search_panel_active = browser_search_panel_active(file_browser.search_state);
        calculate_list_height(
            app.app_state.screen_area,
            has_preview,
            search_panel_active,
            &pane,
        )
    };

    let consumed_filesystem = {
        let file_browser = &mut app.app_state.ui.file_browser;
        handle_filesystem_navigation(
            key_code,
            BrowserFilesystemNavContext {
                state: &mut file_browser.state,
                data: &file_browser.data,
                browser_mode: &file_browser.browser_mode,
                search_state: &mut file_browser.search_state,
                search_query: &mut file_browser.search_query,
                search_mode: &mut file_browser.search_mode,
                list_height,
                app_command_tx: &app.app_command_tx,
                shutdown_tx: &app.shutdown_tx,
                browser_generation: file_browser.browser_generation,
            },
        )
    };
    if consumed_filesystem {
        return true;
    }

    let dialog_action = match key_code {
        KeyCode::Char('Y') => Some(BrowserDialogAction::ConfirmSelection),
        KeyCode::Esc => Some(BrowserDialogAction::Escape),
        _ => None,
    };
    let Some(dialog_action) = dialog_action else {
        return false;
    };

    let reduced = {
        let file_browser = &app.app_state.ui.file_browser;
        if matches!(dialog_action, BrowserDialogAction::ConfirmSelection)
            && matches!(file_browser.browser_mode, FileBrowserMode::File(_))
            && !filesystem_cursor_visible(
                &file_browser.data,
                &file_browser.state,
                &file_browser.browser_mode,
                &file_browser.search_query,
                file_browser.search_mode,
                list_height,
            )
        {
            return true;
        }
        reduce_browser_dialog_action(
            dialog_action,
            &file_browser.state,
            &file_browser.browser_mode,
            !app.app_state.pending_torrent_link.is_empty(),
        )
    };
    execute_browser_dialog_effects(app, reduced.effects).await;
    true
}

pub enum ConfirmDecision {
    ToConfig(ConfigUiState),
    Download(DownloadConfirmPayload),
    File(PathBuf),
    None,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserAction {
    Esc,
    Enter,
    Backspace,
    ToggleSearchMode,
    Char(char),
    Noop,
}

pub struct BrowserReduceResult {
    pub redraw: bool,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserFsAction {
    StartSearch,
    Move(TreeAction),
    EnterDir,
    GoParent,
}

pub enum BrowserFsEffect {
    FetchFileTree {
        path: PathBuf,
        browser_mode: FileBrowserMode,
        highlight_path: Option<PathBuf>,
    },
}

pub struct BrowserFsReduceResult {
    pub consumed: bool,
    pub effects: Vec<BrowserFsEffect>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserDialogAction {
    ConfirmSelection,
    Escape,
    CancelDownloadSelection,
}

pub enum BrowserDialogEffect {
    ExecuteConfirmDecision(ConfirmDecision),
    ToConfig(ConfigUiState),
    CleanupPendingLink,
    ToNormalAndClearPending,
    ClearSearch,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserTransition {
    ToNormal,
    ToConfig,
    Close,
}

pub struct BrowserDialogReduceResult {
    pub effects: Vec<BrowserDialogEffect>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserDownloadEditAction {
    Commit,
    Cancel,
    MoveLeft,
    MoveRight,
    Backspace,
    Delete,
    Insert(char),
    Noop,
}

pub struct BrowserDownloadEditReduceResult {
    pub consumed: bool,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserDownloadShortcutAction {
    ToggleUseContainer,
    StartRename,
    TogglePane,
}

pub struct BrowserDownloadShortcutReduceResult {
    pub consumed: bool,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserDownloadAction {
    Edit(BrowserDownloadEditAction),
    Shortcut(BrowserDownloadShortcutAction),
}

pub struct BrowserDownloadReduceResult {
    pub consumed: bool,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BrowserPreviewAction {
    ConfirmSelection,
    Navigate(TreeAction),
    CyclePriority,
    CycleAllPriorities,
    ExpandAll,
    CollapseAll,
    Ignore,
}

pub struct BrowserPreviewReduceResult {
    pub consumed: bool,
}

pub struct BrowserFilesystemNavContext<'a> {
    pub state: &'a mut TreeViewState,
    pub data: &'a [RawNode<FileMetadata>],
    pub browser_mode: &'a FileBrowserMode,
    pub search_state: &'a mut BrowserSearchState,
    pub search_query: &'a mut String,
    pub search_mode: &'a mut SearchMode,
    pub list_height: usize,
    pub app_command_tx: &'a mpsc::Sender<AppCommand>,
    pub shutdown_tx: &'a broadcast::Sender<()>,
    pub browser_generation: u64,
}

pub struct BrowserFilesystemReduceContext<'a> {
    pub state: &'a mut TreeViewState,
    pub data: &'a [RawNode<FileMetadata>],
    pub browser_mode: &'a FileBrowserMode,
    pub search_state: &'a mut BrowserSearchState,
    pub search_query: &'a mut String,
    pub search_mode: &'a mut SearchMode,
    pub list_height: usize,
}

fn map_search_key_to_browser_action(
    key: KeyEvent,
    search_state: BrowserSearchState,
) -> Option<BrowserAction> {
    if !search_state.is_editing() {
        return if search_state.is_visible() && matches!(key.code, KeyCode::Esc) {
            Some(BrowserAction::Esc)
        } else {
            None
        };
    }

    Some(match key.code {
        KeyCode::Esc => BrowserAction::Esc,
        KeyCode::Enter => BrowserAction::Enter,
        KeyCode::Tab => BrowserAction::ToggleSearchMode,
        KeyCode::Backspace => BrowserAction::Backspace,
        KeyCode::Char(c) => BrowserAction::Char(c),
        _ => BrowserAction::Noop,
    })
}

pub fn reduce_browser_action(
    action: BrowserAction,
    search_state: &mut BrowserSearchState,
    search_query: &mut String,
    search_mode: &mut SearchMode,
) -> BrowserReduceResult {
    match action {
        BrowserAction::Esc => {
            clear_browser_search(search_state, search_query);
        }
        BrowserAction::Enter => {
            *search_state = if search_query.is_empty() {
                BrowserSearchState::Closed
            } else {
                BrowserSearchState::Applied
            };
        }
        BrowserAction::Backspace => {
            search_query.pop();
        }
        BrowserAction::ToggleSearchMode => {
            toggle_browser_search_mode(search_mode);
        }
        BrowserAction::Char(c) => {
            search_query.push(c);
        }
        BrowserAction::Noop => {}
    }

    BrowserReduceResult { redraw: true }
}

fn map_filesystem_key_to_action(key_code: KeyCode) -> Option<BrowserFsAction> {
    match key_code {
        KeyCode::Char('/') => Some(BrowserFsAction::StartSearch),
        KeyCode::Up | KeyCode::Char('k') => Some(BrowserFsAction::Move(TreeAction::Up)),
        KeyCode::Down | KeyCode::Char('j') => Some(BrowserFsAction::Move(TreeAction::Down)),
        KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => Some(BrowserFsAction::EnterDir),
        KeyCode::Backspace | KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('u') => {
            Some(BrowserFsAction::GoParent)
        }
        _ => None,
    }
}

pub fn reduce_filesystem_navigation_action(
    action: BrowserFsAction,
    ctx: BrowserFilesystemReduceContext<'_>,
) -> BrowserFsReduceResult {
    let filter = build_filesystem_filter(ctx.browser_mode, ctx.search_query, *ctx.search_mode);
    let mut result = BrowserFsReduceResult {
        consumed: true,
        effects: Vec::new(),
    };

    match action {
        BrowserFsAction::StartSearch => {
            start_browser_search(ctx.search_state, ctx.search_query, ctx.search_mode);
            ctx.state.top_most_offset = 0;
        }
        BrowserFsAction::Move(tree_action) => {
            TreeMathHelper::apply_action(ctx.state, ctx.data, tree_action, filter, ctx.list_height);
        }
        BrowserFsAction::EnterDir => {
            if let Some(path) = ctx.state.cursor_path.clone() {
                if path.is_dir() {
                    clear_browser_search(ctx.search_state, ctx.search_query);
                    result.effects.push(BrowserFsEffect::FetchFileTree {
                        path,
                        browser_mode: ctx.browser_mode.clone(),
                        highlight_path: None,
                    });
                }
            }
        }
        BrowserFsAction::GoParent => {
            let child_to_highlight = ctx.state.current_path.clone();
            if let Some(parent) = ctx.state.current_path.parent() {
                result.effects.push(BrowserFsEffect::FetchFileTree {
                    path: parent.to_path_buf(),
                    browser_mode: ctx.browser_mode.clone(),
                    highlight_path: Some(child_to_highlight),
                });
            }
        }
    }

    result
}

fn map_download_name_edit_key_to_action(key_code: KeyCode) -> BrowserDownloadEditAction {
    match key_code {
        KeyCode::Enter => BrowserDownloadEditAction::Commit,
        KeyCode::Esc => BrowserDownloadEditAction::Cancel,
        KeyCode::Left => BrowserDownloadEditAction::MoveLeft,
        KeyCode::Right => BrowserDownloadEditAction::MoveRight,
        KeyCode::Backspace => BrowserDownloadEditAction::Backspace,
        KeyCode::Delete => BrowserDownloadEditAction::Delete,
        KeyCode::Char(c) => BrowserDownloadEditAction::Insert(c),
        _ => BrowserDownloadEditAction::Noop,
    }
}

fn pending_magnet_metadata_editing_locked(browser_mode: &FileBrowserMode) -> bool {
    matches!(
        browser_mode,
        FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            preview_tree,
            container_name,
            ..
        } if preview_tree.is_empty() && container_name == AWAITING_MAGNET_METADATA_LABEL
    )
}

pub fn map_download_key_to_action(
    key_code: KeyCode,
    browser_mode: &FileBrowserMode,
) -> Option<BrowserDownloadAction> {
    let edit_locked = pending_magnet_metadata_editing_locked(browser_mode);
    if let FileBrowserMode::DownloadLocSelection {
        is_editing_name,
        use_container,
        target,
        ..
    } = browser_mode
    {
        if edit_locked {
            if key_code == KeyCode::Tab {
                return Some(BrowserDownloadAction::Shortcut(
                    BrowserDownloadShortcutAction::TogglePane,
                ));
            }
            return None;
        }

        if *is_editing_name {
            return Some(BrowserDownloadAction::Edit(
                map_download_name_edit_key_to_action(key_code),
            ));
        }

        if matches!(target, DownloadSelectionTarget::ExistingTorrent { .. }) {
            return None;
        }

        if let Some(action) = map_download_shortcut_key_to_action(key_code, *use_container) {
            return Some(BrowserDownloadAction::Shortcut(action));
        }
    }
    None
}

pub fn reduce_download_name_edit_action(
    action: BrowserDownloadEditAction,
    container_name: &mut String,
    is_editing_name: &mut bool,
    cursor_pos: &mut usize,
    original_name_backup: &str,
) -> BrowserDownloadEditReduceResult {
    *cursor_pos = clamp_to_char_boundary(container_name, *cursor_pos);

    match action {
        BrowserDownloadEditAction::Commit => {
            *is_editing_name = false;
        }
        BrowserDownloadEditAction::Cancel => {
            *container_name = original_name_backup.to_string();
            *is_editing_name = false;
            *cursor_pos = container_name.len();
        }
        BrowserDownloadEditAction::MoveLeft => {
            *cursor_pos = previous_char_boundary(container_name, *cursor_pos);
        }
        BrowserDownloadEditAction::MoveRight => {
            *cursor_pos = next_char_boundary(container_name, *cursor_pos);
        }
        BrowserDownloadEditAction::Backspace => {
            if *cursor_pos > 0 {
                let previous = previous_char_boundary(container_name, *cursor_pos);
                container_name.drain(previous..*cursor_pos);
                *cursor_pos = previous;
            }
        }
        BrowserDownloadEditAction::Delete => {
            if *cursor_pos < container_name.len() {
                let next = next_char_boundary(container_name, *cursor_pos);
                container_name.drain(*cursor_pos..next);
            }
        }
        BrowserDownloadEditAction::Insert(c) => {
            container_name.insert(*cursor_pos, c);
            *cursor_pos += c.len_utf8();
        }
        BrowserDownloadEditAction::Noop => {}
    }

    BrowserDownloadEditReduceResult { consumed: true }
}

fn clamp_to_char_boundary(value: &str, cursor_pos: usize) -> usize {
    let mut pos = cursor_pos.min(value.len());
    while pos > 0 && !value.is_char_boundary(pos) {
        pos -= 1;
    }
    pos
}

fn previous_char_boundary(value: &str, cursor_pos: usize) -> usize {
    let pos = clamp_to_char_boundary(value, cursor_pos);
    if pos == 0 {
        return 0;
    }
    value[..pos]
        .char_indices()
        .last()
        .map(|(idx, _)| idx)
        .unwrap_or(0)
}

fn next_char_boundary(value: &str, cursor_pos: usize) -> usize {
    let pos = clamp_to_char_boundary(value, cursor_pos);
    if pos >= value.len() {
        return value.len();
    }
    value[pos..]
        .char_indices()
        .nth(1)
        .map(|(idx, _)| pos + idx)
        .unwrap_or(value.len())
}

fn filesystem_cursor_visible(
    data: &[RawNode<FileMetadata>],
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
    search_query: &str,
    search_mode: SearchMode,
    list_height: usize,
) -> bool {
    let filter = build_filesystem_filter(browser_mode, search_query, search_mode);
    let projection = TreeProjection::new(data, state, filter, list_height);
    projection.cursor_index(state).is_some()
}

fn map_download_shortcut_key_to_action(
    key_code: KeyCode,
    use_container: bool,
) -> Option<BrowserDownloadShortcutAction> {
    match key_code {
        KeyCode::Char('x') => Some(BrowserDownloadShortcutAction::ToggleUseContainer),
        KeyCode::Char('r') if use_container => Some(BrowserDownloadShortcutAction::StartRename),
        KeyCode::Tab => Some(BrowserDownloadShortcutAction::TogglePane),
        _ => None,
    }
}

pub fn reduce_download_shortcut_action(
    action: BrowserDownloadShortcutAction,
    container_name: &str,
    use_container: &mut bool,
    is_editing_name: &mut bool,
    focused_pane: &mut BrowserPane,
    cursor_pos: &mut usize,
    original_name_backup: &mut String,
) -> BrowserDownloadShortcutReduceResult {
    match action {
        BrowserDownloadShortcutAction::ToggleUseContainer => {
            *use_container = !*use_container;
        }
        BrowserDownloadShortcutAction::StartRename => {
            *is_editing_name = true;
            *original_name_backup = container_name.to_string();
            *cursor_pos = container_name.len();
            *focused_pane = BrowserPane::TorrentPreview;
        }
        BrowserDownloadShortcutAction::TogglePane => {
            *focused_pane = match focused_pane {
                BrowserPane::FileSystem => BrowserPane::TorrentPreview,
                BrowserPane::TorrentPreview => BrowserPane::FileSystem,
            };
        }
    }

    BrowserDownloadShortcutReduceResult { consumed: true }
}

pub fn reduce_browser_download_action(
    action: BrowserDownloadAction,
    browser_mode: &mut FileBrowserMode,
) -> BrowserDownloadReduceResult {
    let edit_locked = pending_magnet_metadata_editing_locked(browser_mode);
    if let FileBrowserMode::DownloadLocSelection {
        container_name,
        use_container,
        is_editing_name,
        focused_pane,
        cursor_pos,
        original_name_backup,
        ..
    } = browser_mode
    {
        if edit_locked
            && !matches!(
                action,
                BrowserDownloadAction::Shortcut(BrowserDownloadShortcutAction::TogglePane)
            )
        {
            return BrowserDownloadReduceResult { consumed: false };
        }

        let consumed = match action {
            BrowserDownloadAction::Edit(edit_action) => {
                reduce_download_name_edit_action(
                    edit_action,
                    container_name,
                    is_editing_name,
                    cursor_pos,
                    original_name_backup,
                )
                .consumed
            }
            BrowserDownloadAction::Shortcut(shortcut_action) => {
                reduce_download_shortcut_action(
                    shortcut_action,
                    container_name,
                    use_container,
                    is_editing_name,
                    focused_pane,
                    cursor_pos,
                    original_name_backup,
                )
                .consumed
            }
        };

        return BrowserDownloadReduceResult { consumed };
    }

    BrowserDownloadReduceResult { consumed: false }
}

pub fn has_preview_content(
    browser_mode: &FileBrowserMode,
    pending_torrent_path: bool,
    pending_torrent_link: bool,
    cursor_path: Option<&std::path::PathBuf>,
) -> bool {
    match browser_mode {
        FileBrowserMode::DownloadLocSelection {
            target,
            preview_tree,
            ..
        } => {
            pending_torrent_path
                || pending_torrent_link
                || !preview_tree.is_empty()
                || matches!(target, DownloadSelectionTarget::ExistingTorrent { .. })
        }
        FileBrowserMode::File(_) => {
            cursor_path.is_some_and(|p| p.extension().is_some_and(|ext| ext == "torrent"))
        }
        _ => false,
    }
}

pub fn focused_pane(browser_mode: &FileBrowserMode) -> BrowserPane {
    if let FileBrowserMode::DownloadLocSelection { focused_pane, .. } = browser_mode {
        focused_pane.clone()
    } else {
        BrowserPane::FileSystem
    }
}

pub fn calculate_area(screen: Rect, has_preview_content: bool) -> Rect {
    if has_preview_content {
        if screen.width < 60 {
            screen
        } else {
            centered_rect(90, 80, screen)
        }
    } else if screen.width < 40 {
        screen
    } else {
        centered_rect(75, 80, screen)
    }
}

pub fn calculate_list_height(
    screen: Rect,
    has_preview_content: bool,
    is_searching: bool,
    focused_pane: &BrowserPane,
) -> usize {
    let area = calculate_area(screen, has_preview_content);
    let layout =
        calculate_file_browser_layout(area, has_preview_content, is_searching, focused_pane, false);
    layout.list.height.saturating_sub(2) as usize
}

pub fn calculate_preview_list_height(
    screen: Rect,
    is_searching: bool,
    focused_pane: &BrowserPane,
    use_container: bool,
    preview_only: bool,
) -> Option<usize> {
    let area = if screen.width < 60 {
        screen
    } else {
        centered_rect(90, 80, screen)
    };
    let layout =
        calculate_file_browser_layout(area, true, is_searching, focused_pane, preview_only);
    layout.preview.map(|preview_rect| {
        let inner_height = preview_rect.height.saturating_sub(2);
        let header_rows = if use_container && !preview_only { 2 } else { 1 };
        inner_height.saturating_sub(header_rows) as usize
    })
}

pub fn map_preview_key_to_action(key_code: KeyCode) -> BrowserPreviewAction {
    match key_code {
        KeyCode::Char('Y') => BrowserPreviewAction::ConfirmSelection,
        KeyCode::Up | KeyCode::Char('k') => BrowserPreviewAction::Navigate(TreeAction::Up),
        KeyCode::Down | KeyCode::Char('j') => BrowserPreviewAction::Navigate(TreeAction::Down),
        KeyCode::Left | KeyCode::Char('h') => BrowserPreviewAction::Navigate(TreeAction::Left),
        KeyCode::Right | KeyCode::Char('l') => BrowserPreviewAction::Navigate(TreeAction::Right),
        KeyCode::Char('P') => BrowserPreviewAction::CycleAllPriorities,
        KeyCode::Char(' ') | KeyCode::Char('p') => BrowserPreviewAction::CyclePriority,
        KeyCode::Char('e') => BrowserPreviewAction::ExpandAll,
        KeyCode::Char('c') => BrowserPreviewAction::CollapseAll,
        _ => BrowserPreviewAction::Ignore,
    }
}

pub fn reduce_browser_preview_action(
    action: BrowserPreviewAction,
    preview_state: &mut TreeViewState,
    preview_tree: &mut [RawNode<TorrentPreviewPayload>],
    filter: TreeFilter<TorrentPreviewPayload>,
    list_height: Option<usize>,
) -> BrowserPreviewReduceResult {
    match action {
        BrowserPreviewAction::ConfirmSelection => BrowserPreviewReduceResult { consumed: false },
        BrowserPreviewAction::Navigate(tree_action) => {
            if let Some(height) = list_height {
                TreeMathHelper::apply_action(
                    preview_state,
                    preview_tree,
                    tree_action,
                    filter,
                    height,
                );
            }
            BrowserPreviewReduceResult { consumed: true }
        }
        BrowserPreviewAction::CyclePriority => {
            if let Some(height) = list_height {
                let cursor_visible = {
                    let projection =
                        TreeProjection::new(preview_tree, preview_state, filter, height);
                    projection.cursor_index(preview_state).is_some()
                };
                if cursor_visible {
                    if let Some(target) = &preview_state.cursor_path {
                        apply_priority_cycle(preview_tree, target);
                    }
                }
            }
            BrowserPreviewReduceResult { consumed: true }
        }
        BrowserPreviewAction::CycleAllPriorities => {
            if let Some(_height) = list_height {
                apply_priority_cycle_to_all(preview_tree);
            }
            BrowserPreviewReduceResult { consumed: true }
        }
        BrowserPreviewAction::ExpandAll => {
            expand_preview_tree(preview_state, preview_tree);
            BrowserPreviewReduceResult { consumed: true }
        }
        BrowserPreviewAction::CollapseAll => {
            collapse_preview_tree(preview_state, preview_tree);
            BrowserPreviewReduceResult { consumed: true }
        }
        BrowserPreviewAction::Ignore => BrowserPreviewReduceResult { consumed: true },
    }
}

pub fn build_filesystem_filter(
    browser_mode: &FileBrowserMode,
    search_query: &str,
    search_mode: SearchMode,
) -> TreeFilter<FileMetadata> {
    if search_query.trim().is_empty() {
        return match browser_mode {
            FileBrowserMode::File(extensions) => {
                let exts = extensions.clone();
                TreeFilter::new("", move |node| filesystem_node_allowed(node, &exts))
            }
            FileBrowserMode::Directory
            | FileBrowserMode::DownloadLocSelection { .. }
            | FileBrowserMode::ConfigPathSelection { .. } => TreeFilter::default(),
        };
    }

    let extensions = match browser_mode {
        FileBrowserMode::File(extensions) => Some(extensions.clone()),
        FileBrowserMode::Directory
        | FileBrowserMode::DownloadLocSelection { .. }
        | FileBrowserMode::ConfigPathSelection { .. } => None,
    };

    match search_mode {
        SearchMode::Fuzzy => {
            let matcher = SkimMatcherV2::default();
            let query = search_query.to_lowercase();
            TreeFilter::rule_only(search_query, move |node| {
                filesystem_node_allowed_for_mode(node, extensions.as_deref())
                    && matcher
                        .fuzzy_match(&filesystem_search_haystack(node).to_lowercase(), &query)
                        .is_some()
            })
        }
        SearchMode::Regex => {
            let regex = regex::RegexBuilder::new(search_query)
                .case_insensitive(true)
                .build();
            match regex {
                Ok(regex) => TreeFilter::rule_only(search_query, move |node| {
                    filesystem_node_allowed_for_mode(node, extensions.as_deref())
                        && regex.is_match(&filesystem_search_haystack(node))
                }),
                Err(_) => TreeFilter::rule_only(search_query, |_| false),
            }
        }
    }
}

fn filesystem_node_allowed_for_mode(
    node: &RawNode<FileMetadata>,
    extensions: Option<&[String]>,
) -> bool {
    match extensions {
        Some(exts) => filesystem_node_allowed(node, exts),
        None => true,
    }
}

fn filesystem_node_allowed(node: &RawNode<FileMetadata>, extensions: &[String]) -> bool {
    node.is_dir || extensions.iter().any(|ext| node.name.ends_with(ext))
}

fn filesystem_search_haystack(node: &RawNode<FileMetadata>) -> String {
    format!("{} {}", node.name, node.full_path.to_string_lossy())
}

pub fn build_torrent_preview_filter(
    search_query: &str,
    search_mode: SearchMode,
) -> TreeFilter<TorrentPreviewPayload> {
    if search_query.trim().is_empty() {
        return TreeFilter::default();
    }

    match search_mode {
        SearchMode::Fuzzy => {
            let matcher = SkimMatcherV2::default();
            let query = search_query.to_lowercase();
            TreeFilter::rule_only(search_query, move |node| {
                let haystack = torrent_preview_search_haystack(node).to_lowercase();
                matcher.fuzzy_match(&haystack, &query).is_some()
            })
        }
        SearchMode::Regex => {
            let regex = regex::RegexBuilder::new(search_query)
                .case_insensitive(true)
                .build();
            match regex {
                Ok(regex) => TreeFilter::rule_only(search_query, move |node| {
                    regex.is_match(&torrent_preview_search_haystack(node))
                }),
                Err(_) => TreeFilter::rule_only(search_query, |_| false),
            }
        }
    }
}

fn torrent_preview_search_haystack(node: &RawNode<TorrentPreviewPayload>) -> String {
    format!("{} {}", node.name, node.full_path.to_string_lossy())
}

pub fn handle_filesystem_navigation(
    key_code: KeyCode,
    ctx: BrowserFilesystemNavContext<'_>,
) -> bool {
    if let Some(action) = map_filesystem_key_to_action(key_code) {
        let reduced = reduce_filesystem_navigation_action(
            action,
            BrowserFilesystemReduceContext {
                state: ctx.state,
                data: ctx.data,
                browser_mode: ctx.browser_mode,
                search_state: ctx.search_state,
                search_query: ctx.search_query,
                search_mode: ctx.search_mode,
                list_height: ctx.list_height,
            },
        );
        for effect in reduced.effects {
            match effect {
                BrowserFsEffect::FetchFileTree {
                    path,
                    browser_mode,
                    highlight_path,
                } => {
                    spawn_app_command_sender(
                        ctx.app_command_tx.clone(),
                        ctx.shutdown_tx.subscribe(),
                        AppCommand::FetchFileTree {
                            browser_generation: ctx.browser_generation,
                            path,
                            browser_mode,
                            preserve_browser_mode: true,
                            highlight_path,
                        },
                    );
                }
            }
        }
        reduced.consumed
    } else {
        false
    }
}

pub fn reduce_browser_dialog_action(
    action: BrowserDialogAction,
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
    has_pending_torrent_link: bool,
) -> BrowserDialogReduceResult {
    let mut result = BrowserDialogReduceResult {
        effects: Vec::new(),
    };

    match action {
        BrowserDialogAction::ConfirmSelection => {
            result
                .effects
                .push(BrowserDialogEffect::ExecuteConfirmDecision(
                    resolve_confirm_decision(state, browser_mode),
                ));
            result.effects.push(BrowserDialogEffect::ClearSearch);
        }
        BrowserDialogAction::Escape => {
            if let Some(config_ui) = escape_to_config_mode(browser_mode) {
                result.effects.push(BrowserDialogEffect::ClearSearch);
                result
                    .effects
                    .push(BrowserDialogEffect::ToConfig(config_ui));
                return result;
            }

            if matches!(browser_mode, FileBrowserMode::DownloadLocSelection { .. })
                && has_pending_torrent_link
            {
                result.effects.push(BrowserDialogEffect::CleanupPendingLink);
            }

            result.effects.push(BrowserDialogEffect::ClearSearch);
            result
                .effects
                .push(BrowserDialogEffect::ToNormalAndClearPending);
        }
        BrowserDialogAction::CancelDownloadSelection => {
            if has_pending_torrent_link {
                result.effects.push(BrowserDialogEffect::CleanupPendingLink);
            }
            result.effects.push(BrowserDialogEffect::ClearSearch);
            result
                .effects
                .push(BrowserDialogEffect::ToNormalAndClearPending);
        }
    }

    result
}

pub async fn execute_browser_dialog_effects(app: &mut App, effects: Vec<BrowserDialogEffect>) {
    for effect in effects {
        match effect {
            BrowserDialogEffect::ExecuteConfirmDecision(decision) => {
                if let Some(transition) = execute_confirm_decision(app, decision).await {
                    apply_browser_transition(app, transition);
                }
            }
            BrowserDialogEffect::ToConfig(config_ui) => {
                app.app_state.ui.config = config_ui;
                apply_browser_transition(app, BrowserTransition::ToConfig);
            }
            BrowserDialogEffect::CleanupPendingLink => {
                app.cleanup_pending_magnet_preview_runtime();
            }
            BrowserDialogEffect::ToNormalAndClearPending => {
                apply_browser_close_transition(app);
                let should_clear_pending_magnet_preview =
                    !app.app_state.pending_torrent_link.is_empty();
                app.app_state.pending_torrent_path = None;
                app.app_state.pending_torrent_link.clear();
                if should_clear_pending_magnet_preview {
                    app.app_state.pending_magnet_preview_info_hash = None;
                }
                app.app_state.pending_manual_ingest = None;
            }
            BrowserDialogEffect::ClearSearch => {
                app.app_state.ui.file_browser.search_state = BrowserSearchState::Closed;
                app.app_state.ui.file_browser.search_query.clear();
            }
        }
    }
}

fn apply_browser_transition(app: &mut App, transition: BrowserTransition) {
    match transition {
        BrowserTransition::ToNormal => {
            app.app_state
                .ui
                .file_browser
                .invalidate_browser_generation();
            app.app_state
                .ui
                .file_browser
                .return_to_torrent_management_on_close = false;
            app.app_state.mode = AppMode::Normal;
        }
        BrowserTransition::ToConfig => {
            app.app_state
                .ui
                .file_browser
                .invalidate_browser_generation();
            app.app_state
                .ui
                .file_browser
                .return_to_torrent_management_on_close = false;
            app.app_state.mode = AppMode::Config;
        }
        BrowserTransition::Close => apply_browser_close_transition(app),
    }
}

fn apply_browser_close_transition(app: &mut App) {
    let return_to_torrent_management = app
        .app_state
        .ui
        .file_browser
        .return_to_torrent_management_on_close;
    app.app_state
        .ui
        .file_browser
        .invalidate_browser_generation();
    app.app_state
        .ui
        .file_browser
        .return_to_torrent_management_on_close = false;
    app.app_state.mode = if return_to_torrent_management {
        AppMode::TorrentManagement
    } else {
        AppMode::Normal
    };
}

pub fn confirm_config_path_selection(
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
) -> Option<ConfigUiState> {
    if let FileBrowserMode::ConfigPathSelection {
        target_item,
        current_settings,
        selected_index,
        items,
    } = browser_mode
    {
        let mut new_settings = current_settings.clone();
        let selected_path = state.current_path.clone();

        match target_item {
            ConfigItem::DefaultDownloadFolder if !crate::config::is_shared_config_mode() => {
                new_settings.default_download_folder = Some(selected_path)
            }
            ConfigItem::WatchFolder => new_settings.watch_folder = Some(selected_path),
            _ => {}
        }

        return Some(ConfigUiState {
            settings_edit: new_settings,
            selected_index: *selected_index,
            items: items.clone(),
            editing: None,
        });
    }
    None
}

pub fn escape_to_config_mode(browser_mode: &FileBrowserMode) -> Option<ConfigUiState> {
    if let FileBrowserMode::ConfigPathSelection {
        current_settings,
        selected_index,
        items,
        ..
    } = browser_mode
    {
        return Some(ConfigUiState {
            settings_edit: current_settings.clone(),
            selected_index: *selected_index,
            items: items.clone(),
            editing: None,
        });
    }
    None
}

pub fn selected_torrent_file_for_confirm(
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
) -> Option<std::path::PathBuf> {
    if let FileBrowserMode::File(extensions) = browser_mode {
        if let Some(path) = state.cursor_path.clone() {
            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
            if extensions.iter().any(|ext| name.ends_with(ext)) {
                return Some(path);
            }
        }
    }
    None
}

pub fn resolve_confirm_decision(
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
) -> ConfirmDecision {
    if let Some(config_ui) = confirm_config_path_selection(state, browser_mode) {
        return ConfirmDecision::ToConfig(config_ui);
    }
    if let Some(payload) = build_download_confirm_payload(state, browser_mode) {
        return ConfirmDecision::Download(payload);
    }
    if let Some(path) = selected_torrent_file_for_confirm(state, browser_mode) {
        return ConfirmDecision::File(path);
    }
    ConfirmDecision::None
}

fn priority_overrides(
    priorities: HashMap<usize, FilePriority>,
) -> Vec<ControlFilePriorityOverride> {
    let mut overrides: Vec<_> = priorities
        .into_iter()
        .filter(|(_, priority)| !matches!(priority, FilePriority::Normal))
        .map(|(file_index, priority)| ControlFilePriorityOverride {
            file_index,
            priority,
        })
        .collect();
    overrides.sort_by_key(|override_value| override_value.file_index);
    overrides
}

fn existing_torrent_priorities(app: &App, info_hash: &[u8]) -> HashMap<usize, FilePriority> {
    app.app_state
        .torrents
        .get(info_hash)
        .map(|torrent| torrent.latest_state.file_priorities.clone())
        .unwrap_or_default()
}

pub async fn execute_confirm_decision(
    app: &mut App,
    decision: ConfirmDecision,
) -> Option<BrowserTransition> {
    match decision {
        ConfirmDecision::ToConfig(config_ui) => {
            tracing::info!(target: "superseedr", "Confirming Config Path Selection");
            app.app_state.ui.config = config_ui;
            Some(BrowserTransition::ToConfig)
        }
        ConfirmDecision::Download(payload) => match payload.target {
            DownloadSelectionTarget::PendingAdd => {
                if let Some(pending_path) = app.app_state.pending_torrent_path.clone() {
                    match app.prepare_add_torrent_file_request(
                        pending_path.clone(),
                        Some(payload.base_path.clone()),
                        payload.container_name_to_use.clone(),
                        payload.file_priorities.clone(),
                    ) {
                        Ok(request) => {
                            app.app_state.pending_torrent_path = None;
                            let pending_ingest = app.app_state.pending_manual_ingest.take();
                            spawn_app_command_sender(
                                app.app_command_tx.clone(),
                                app.shutdown_tx.subscribe(),
                                AppCommand::SubmitManualAddRequest {
                                    request,
                                    pending_ingest,
                                },
                            );
                        }
                        Err(error) => {
                            app.app_state.system_error = Some(error);
                            return None;
                        }
                    }
                } else if !app.app_state.pending_torrent_link.is_empty() {
                    let pending_ingest = app.app_state.pending_manual_ingest.take();
                    let request = app.prepare_add_magnet_request(
                        app.app_state.pending_torrent_link.clone(),
                        Some(payload.base_path),
                        payload.container_name_to_use,
                        payload.file_priorities,
                    );
                    spawn_app_command_sender(
                        app.app_command_tx.clone(),
                        app.shutdown_tx.subscribe(),
                        AppCommand::SubmitManualAddRequest {
                            request,
                            pending_ingest,
                        },
                    );
                    app.app_state.pending_torrent_link.clear();
                } else {
                    tracing::warn!(target: "superseedr", "SHIFT+Y pressed but no pending content was found");
                }
                Some(BrowserTransition::ToNormal)
            }
            DownloadSelectionTarget::ExistingTorrent { info_hash } => {
                let file_priorities = if payload.has_preview_files {
                    payload.file_priorities
                } else {
                    existing_torrent_priorities(app, &info_hash)
                };
                let existing = app.app_state.torrents.get(&info_hash).map(|torrent| {
                    (
                        torrent.latest_state.download_path.clone(),
                        torrent.latest_state.container_name.clone(),
                    )
                });
                let (download_path, container_name) = existing.unwrap_or_default();
                let request = ControlRequest::SetTorrentConfig {
                    info_hash_hex: hex::encode(info_hash),
                    download_path,
                    container_name,
                    file_priorities: priority_overrides(file_priorities),
                };
                spawn_app_command_sender(
                    app.app_command_tx.clone(),
                    app.shutdown_tx.subscribe(),
                    AppCommand::SubmitControlRequest(request),
                );
                Some(BrowserTransition::Close)
            }
        },
        ConfirmDecision::File(path) => {
            if path
                .file_name()
                .and_then(|n| n.to_str())
                .is_some_and(|name| name.ends_with(".torrent"))
            {
                spawn_app_command_sender(
                    app.app_command_tx.clone(),
                    app.shutdown_tx.subscribe(),
                    AppCommand::AddTorrentFromFile(path),
                );
            }
            Some(BrowserTransition::ToNormal)
        }
        ConfirmDecision::None => None,
    }
}

pub fn build_download_confirm_payload(
    state: &TreeViewState,
    browser_mode: &FileBrowserMode,
) -> Option<DownloadConfirmPayload> {
    if let FileBrowserMode::DownloadLocSelection {
        target,
        container_name,
        use_container,
        preview_tree,
        ..
    } = browser_mode
    {
        let base_path = state.current_path.clone();
        let is_unhydrated_pending_magnet = pending_magnet_metadata_editing_locked(browser_mode);
        let container_name_to_use = if *use_container {
            if is_unhydrated_pending_magnet {
                None
            } else {
                Some(container_name.clone())
            }
        } else {
            Some(String::new())
        };

        let mut file_priorities = HashMap::new();
        for node in preview_tree {
            node.collect_priorities(&mut file_priorities);
        }

        return Some(DownloadConfirmPayload {
            base_path,
            container_name_to_use,
            file_priorities,
            target: target.clone(),
            has_preview_files: !preview_tree.is_empty(),
        });
    }
    None
}

#[cfg(test)]
pub fn pending_link_info_hash(pending_torrent_link: &str) -> Option<Vec<u8>> {
    if pending_torrent_link.is_empty() {
        return None;
    }
    let (btih, btmh) = crate::app::parse_hybrid_hashes(pending_torrent_link);
    btih.or(btmh)
}

pub fn apply_priority_cycle(
    nodes: &mut [RawNode<TorrentPreviewPayload>],
    target_path: &Path,
) -> bool {
    for node in &mut *nodes {
        let found = node.find_and_act(target_path, &mut |target_node| {
            let new_priority = target_node.payload.priority.next();
            target_node.apply_recursive(&|n| {
                n.payload.priority = new_priority;
            });
        });

        if found {
            refresh_torrent_preview_directory_priorities(nodes);
            return true;
        }
    }
    false
}

pub fn expand_preview_tree(
    preview_state: &mut TreeViewState,
    preview_tree: &[RawNode<TorrentPreviewPayload>],
) {
    for node in preview_tree {
        node.expand_all(preview_state);
    }
}

pub fn collapse_preview_tree(
    preview_state: &mut TreeViewState,
    preview_tree: &[RawNode<TorrentPreviewPayload>],
) {
    preview_state.expanded_paths.clear();
    preview_state.top_most_offset = 0;

    let cursor_is_root = match &preview_state.cursor_path {
        Some(cursor_path) => preview_tree
            .iter()
            .any(|node| node.full_path == *cursor_path),
        None => false,
    };
    if !cursor_is_root {
        preview_state.cursor_path = preview_tree.first().map(|node| node.full_path.clone());
    }
}

fn common_file_priority(nodes: &[RawNode<TorrentPreviewPayload>]) -> Option<FilePriority> {
    let mut common = None;
    let mut is_mixed = false;
    collect_common_file_priority(nodes, &mut common, &mut is_mixed);

    if is_mixed {
        Some(FilePriority::Mixed)
    } else {
        common
    }
}

fn collect_common_file_priority(
    nodes: &[RawNode<TorrentPreviewPayload>],
    common: &mut Option<FilePriority>,
    is_mixed: &mut bool,
) {
    for node in nodes {
        if node.is_dir {
            collect_common_file_priority(&node.children, common, is_mixed);
            continue;
        }

        match common {
            Some(priority) if *priority != node.payload.priority => {
                *is_mixed = true;
            }
            Some(_) => {}
            None => {
                *common = Some(node.payload.priority);
            }
        }
    }
}

pub fn apply_priority_cycle_to_all(nodes: &mut [RawNode<TorrentPreviewPayload>]) -> bool {
    let Some(next_priority) = common_file_priority(nodes).map(|priority| priority.next()) else {
        return false;
    };

    for node in &mut *nodes {
        node.apply_recursive(&|target| {
            target.payload.priority = next_priority;
        });
    }
    refresh_torrent_preview_directory_priorities(nodes);
    true
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::{
        AppRuntimeMode, BrowserPane, ConfigItem, TorrentDisplayState, TorrentMetrics,
        TorrentPreviewPayload,
    };
    use crate::config::Settings;
    use crate::tui::tree::{RawNode, TreeViewState};
    use ratatui::crossterm::event::KeyModifiers;
    use std::path::PathBuf;

    async fn app_with_existing_torrent(mode: AppMode) -> App {
        let settings = Settings {
            client_port: 0,
            ..Default::default()
        };
        let mut app = App::new(settings, AppRuntimeMode::Normal)
            .await
            .expect("build app");
        let info_hash = vec![7; 20];
        app.app_state.mode = mode;
        app.app_state.torrents.insert(
            info_hash.clone(),
            TorrentDisplayState {
                latest_state: TorrentMetrics {
                    info_hash: info_hash.clone(),
                    torrent_name: "Sample Packet".to_string(),
                    ..Default::default()
                },
                ..Default::default()
            },
        );
        app.open_existing_torrent_file_browser(info_hash);
        app
    }

    #[tokio::test]
    async fn escape_from_existing_torrent_browser_returns_to_torrent_management_origin() {
        let mut app = app_with_existing_torrent(AppMode::TorrentManagement).await;

        handle_event(
            CrosstermEvent::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)),
            &mut app,
        )
        .await;

        assert!(matches!(app.app_state.mode, AppMode::TorrentManagement));
        let _ = app.shutdown_tx.send(());
    }

    #[tokio::test]
    async fn escape_from_normal_existing_torrent_browser_still_returns_to_normal() {
        let mut app = app_with_existing_torrent(AppMode::Normal).await;

        handle_event(
            CrosstermEvent::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)),
            &mut app,
        )
        .await;

        assert!(matches!(app.app_state.mode, AppMode::Normal));
        let _ = app.shutdown_tx.send(());
    }

    #[tokio::test]
    async fn confirm_existing_torrent_browser_returns_to_torrent_management_origin() {
        let mut app = app_with_existing_torrent(AppMode::TorrentManagement).await;

        handle_event(
            CrosstermEvent::Key(KeyEvent::new(KeyCode::Char('Y'), KeyModifiers::NONE)),
            &mut app,
        )
        .await;

        assert!(matches!(app.app_state.mode, AppMode::TorrentManagement));
        let _ = app.shutdown_tx.send(());
    }

    #[test]
    fn search_reducer_clears_on_escape() {
        let mut search_state = BrowserSearchState::Editing;
        let mut query = String::from("abc");
        let mut search_mode = SearchMode::Fuzzy;
        let action = map_search_key_to_browser_action(KeyEvent::from(KeyCode::Esc), search_state)
            .expect("expected search action");
        let out = reduce_browser_action(action, &mut search_state, &mut query, &mut search_mode);
        assert!(out.redraw);
        assert_eq!(search_state, BrowserSearchState::Closed);
        assert!(query.is_empty());
    }

    #[test]
    fn applied_search_escape_closes_search_before_dialog() {
        let mut search_state = BrowserSearchState::Applied;
        let mut query = String::from("abc");
        let mut search_mode = SearchMode::Regex;
        let action = map_search_key_to_browser_action(KeyEvent::from(KeyCode::Esc), search_state)
            .expect("expected applied search escape action");

        let out = reduce_browser_action(action, &mut search_state, &mut query, &mut search_mode);

        assert!(out.redraw);
        assert_eq!(search_state, BrowserSearchState::Closed);
        assert!(query.is_empty());
    }

    #[test]
    fn closed_search_escape_falls_through_to_dialog() {
        assert!(map_search_key_to_browser_action(
            KeyEvent::from(KeyCode::Esc),
            BrowserSearchState::Closed
        )
        .is_none());
    }

    #[test]
    fn reducer_search_char_appends_and_consumes() {
        let mut search_state = BrowserSearchState::Editing;
        let mut query = String::from("ab");
        let mut search_mode = SearchMode::Fuzzy;

        let out = reduce_browser_action(
            BrowserAction::Char('c'),
            &mut search_state,
            &mut query,
            &mut search_mode,
        );

        assert!(out.redraw);
        assert_eq!(search_state, BrowserSearchState::Editing);
        assert_eq!(query, "abc");
    }

    #[test]
    fn reducer_search_noop_still_consumes_when_searching() {
        let mut search_state = BrowserSearchState::Editing;
        let mut query = String::from("abc");
        let mut search_mode = SearchMode::Fuzzy;

        let out = reduce_browser_action(
            BrowserAction::Noop,
            &mut search_state,
            &mut query,
            &mut search_mode,
        );

        assert!(out.redraw);
        assert_eq!(search_state, BrowserSearchState::Editing);
        assert_eq!(query, "abc");
    }

    #[test]
    fn reducer_search_tab_toggles_mode() {
        let mut search_state = BrowserSearchState::Editing;
        let mut query = String::from("abc");
        let mut search_mode = SearchMode::Fuzzy;
        let action = map_search_key_to_browser_action(KeyEvent::from(KeyCode::Tab), search_state)
            .expect("expected mode toggle");

        let out = reduce_browser_action(action, &mut search_state, &mut query, &mut search_mode);

        assert!(out.redraw);
        assert_eq!(search_state, BrowserSearchState::Editing);
        assert_eq!(query, "abc");
        assert_eq!(search_mode, SearchMode::Regex);
    }

    #[test]
    fn clear_browser_search_disables_and_clears_query() {
        let mut search_state = BrowserSearchState::Editing;
        let mut query = String::from("abc");

        clear_browser_search(&mut search_state, &mut query);

        assert_eq!(search_state, BrowserSearchState::Closed);
        assert!(query.is_empty());
    }

    #[test]
    fn reducer_search_enter_applies_non_empty_query() {
        let mut search_state = BrowserSearchState::Editing;
        let mut query = String::from("abc");
        let mut search_mode = SearchMode::Regex;

        let out = reduce_browser_action(
            BrowserAction::Enter,
            &mut search_state,
            &mut query,
            &mut search_mode,
        );

        assert!(out.redraw);
        assert_eq!(search_state, BrowserSearchState::Applied);
        assert_eq!(query, "abc");
    }

    #[test]
    fn reducer_filesystem_start_search_sets_flag_and_clears_query() {
        let mut search_state = BrowserSearchState::Closed;
        let mut query = String::from("abc");
        let mut state = TreeViewState {
            top_most_offset: 12,
            ..Default::default()
        };
        let data: Vec<RawNode<FileMetadata>> = vec![];
        let mode = FileBrowserMode::Directory;
        let mut search_mode = SearchMode::Fuzzy;

        let out = reduce_filesystem_navigation_action(
            BrowserFsAction::StartSearch,
            BrowserFilesystemReduceContext {
                state: &mut state,
                data: &data,
                browser_mode: &mode,
                search_state: &mut search_state,
                search_query: &mut query,
                search_mode: &mut search_mode,
                list_height: 5,
            },
        );

        assert!(out.consumed);
        assert_eq!(search_state, BrowserSearchState::Editing);
        assert!(query.is_empty());
        assert_eq!(search_mode, SearchMode::Regex);
        assert_eq!(state.top_most_offset, 0);
    }

    #[test]
    fn reducer_filesystem_enter_dir_emits_fetch_effect() {
        let mut search_state = BrowserSearchState::Editing;
        let mut query = String::from("abc");
        let mut state = TreeViewState {
            current_path: PathBuf::from("."),
            cursor_path: Some(PathBuf::from(".")),
            ..Default::default()
        };
        let data: Vec<RawNode<FileMetadata>> = vec![];
        let mode = FileBrowserMode::Directory;
        let mut search_mode = SearchMode::Fuzzy;

        let out = reduce_filesystem_navigation_action(
            BrowserFsAction::EnterDir,
            BrowserFilesystemReduceContext {
                state: &mut state,
                data: &data,
                browser_mode: &mode,
                search_state: &mut search_state,
                search_query: &mut query,
                search_mode: &mut search_mode,
                list_height: 5,
            },
        );

        assert!(out.consumed);
        assert_eq!(search_state, BrowserSearchState::Closed);
        assert!(query.is_empty());
        assert_eq!(out.effects.len(), 1);
        assert!(matches!(
            out.effects[0],
            BrowserFsEffect::FetchFileTree { ref path, highlight_path: None, .. }
                if path == &PathBuf::from(".")
        ));
    }

    #[test]
    fn reducer_download_edit_insert_updates_buffer_and_cursor() {
        let mut mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: "ab".to_string(),
            use_container: true,
            is_editing_name: true,
            focused_pane: BrowserPane::TorrentPreview,
            preview_tree: vec![RawNode {
                name: "x".to_string(),
                full_path: PathBuf::from("x"),
                children: vec![],
                payload: TorrentPreviewPayload::default(),
                is_dir: false,
            }],
            preview_state: TreeViewState::default(),
            cursor_pos: 2,
            original_name_backup: "ab".to_string(),
        };

        let out = reduce_browser_download_action(
            BrowserDownloadAction::Edit(BrowserDownloadEditAction::Insert('c')),
            &mut mode,
        );
        assert!(out.consumed);
        match mode {
            FileBrowserMode::DownloadLocSelection {
                container_name,
                cursor_pos,
                ..
            } => {
                assert_eq!(container_name, "abc");
                assert_eq!(cursor_pos, 3);
            }
            _ => panic!("expected DownloadLocSelection"),
        }
    }

    #[test]
    fn reducer_download_edit_cancel_restores_backup() {
        let mut name = String::from("abc");
        let mut is_editing_name = true;
        let mut cursor_pos = 3;
        let backup = String::from("orig");

        let out = reduce_download_name_edit_action(
            BrowserDownloadEditAction::Cancel,
            &mut name,
            &mut is_editing_name,
            &mut cursor_pos,
            &backup,
        );

        assert!(out.consumed);
        assert_eq!(name, "orig");
        assert!(!is_editing_name);
        assert_eq!(cursor_pos, 4);
    }

    #[test]
    fn reducer_download_shortcut_start_rename_sets_editing_state() {
        let mut use_container = true;
        let mut is_editing_name = false;
        let mut focused_pane = BrowserPane::FileSystem;
        let mut cursor_pos = 0;
        let mut original_name_backup = String::new();
        let container_name = String::from("seed");

        let out = reduce_download_shortcut_action(
            BrowserDownloadShortcutAction::StartRename,
            &container_name,
            &mut use_container,
            &mut is_editing_name,
            &mut focused_pane,
            &mut cursor_pos,
            &mut original_name_backup,
        );

        assert!(out.consumed);
        assert!(is_editing_name);
        assert_eq!(original_name_backup, "seed");
        assert_eq!(cursor_pos, 4);
        assert_eq!(focused_pane, BrowserPane::TorrentPreview);
    }

    #[test]
    fn map_download_shortcut_requires_container_for_rename() {
        let action = map_download_shortcut_key_to_action(KeyCode::Char('r'), false);
        assert!(action.is_none());
    }

    #[test]
    fn map_download_key_prefers_edit_action_while_editing() {
        let mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: "x".to_string(),
            use_container: true,
            is_editing_name: true,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 1,
            original_name_backup: "x".to_string(),
        };

        let action = map_download_key_to_action(KeyCode::Tab, &mode);

        assert!(matches!(
            action,
            Some(BrowserDownloadAction::Edit(BrowserDownloadEditAction::Noop))
        ));
    }

    #[test]
    fn map_download_key_locks_awaiting_magnet_metadata_edits() {
        let mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: AWAITING_MAGNET_METADATA_LABEL.to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: AWAITING_MAGNET_METADATA_LABEL.len(),
            original_name_backup: AWAITING_MAGNET_METADATA_LABEL.to_string(),
        };

        assert!(map_download_key_to_action(KeyCode::Char('r'), &mode).is_none());
        assert!(map_download_key_to_action(KeyCode::Char('x'), &mode).is_none());
        assert!(matches!(
            map_download_key_to_action(KeyCode::Tab, &mode),
            Some(BrowserDownloadAction::Shortcut(
                BrowserDownloadShortcutAction::TogglePane
            ))
        ));
    }

    #[test]
    fn map_download_key_locks_active_name_edit_while_awaiting_magnet_metadata() {
        let mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: AWAITING_MAGNET_METADATA_LABEL.to_string(),
            use_container: true,
            is_editing_name: true,
            focused_pane: BrowserPane::TorrentPreview,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: AWAITING_MAGNET_METADATA_LABEL.len(),
            original_name_backup: AWAITING_MAGNET_METADATA_LABEL.to_string(),
        };

        assert!(map_download_key_to_action(KeyCode::Char('c'), &mode).is_none());
        assert!(map_download_key_to_action(KeyCode::Backspace, &mode).is_none());
    }

    #[test]
    fn map_download_key_allows_pending_magnet_edits_after_hydration() {
        let mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: "Sample Files".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::TorrentPreview,
            preview_tree: vec![RawNode {
                name: "item.bin".to_string(),
                full_path: PathBuf::from("item.bin"),
                children: vec![],
                payload: TorrentPreviewPayload::default(),
                is_dir: false,
            }],
            preview_state: TreeViewState::default(),
            cursor_pos: 0,
            original_name_backup: "Sample Files".to_string(),
        };

        assert!(matches!(
            map_download_key_to_action(KeyCode::Char('r'), &mode),
            Some(BrowserDownloadAction::Shortcut(
                BrowserDownloadShortcutAction::StartRename
            ))
        ));
        assert!(matches!(
            map_download_key_to_action(KeyCode::Char('x'), &mode),
            Some(BrowserDownloadAction::Shortcut(
                BrowserDownloadShortcutAction::ToggleUseContainer
            ))
        ));
    }

    #[test]
    fn reduce_browser_download_action_ignores_locked_awaiting_magnet_edits() {
        let mut mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: AWAITING_MAGNET_METADATA_LABEL.to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: AWAITING_MAGNET_METADATA_LABEL.len(),
            original_name_backup: AWAITING_MAGNET_METADATA_LABEL.to_string(),
        };

        let toggle_out = reduce_browser_download_action(
            BrowserDownloadAction::Shortcut(BrowserDownloadShortcutAction::ToggleUseContainer),
            &mut mode,
        );
        assert!(!toggle_out.consumed);
        let edit_out = reduce_browser_download_action(
            BrowserDownloadAction::Edit(BrowserDownloadEditAction::Insert('x')),
            &mut mode,
        );
        assert!(!edit_out.consumed);

        let FileBrowserMode::DownloadLocSelection {
            container_name,
            use_container,
            ..
        } = mode
        else {
            panic!("expected DownloadLocSelection");
        };
        assert_eq!(container_name, AWAITING_MAGNET_METADATA_LABEL);
        assert!(use_container);
    }

    #[test]
    fn reduce_browser_download_shortcut_updates_mode() {
        let mut mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: "seed".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 4,
            original_name_backup: String::new(),
        };

        let out = reduce_browser_download_action(
            BrowserDownloadAction::Shortcut(BrowserDownloadShortcutAction::StartRename),
            &mut mode,
        );

        assert!(out.consumed);
        match mode {
            FileBrowserMode::DownloadLocSelection {
                is_editing_name,
                focused_pane,
                original_name_backup,
                ..
            } => {
                assert!(is_editing_name);
                assert_eq!(focused_pane, BrowserPane::TorrentPreview);
                assert_eq!(original_name_backup, "seed");
            }
            _ => panic!("expected DownloadLocSelection"),
        }
    }

    #[test]
    fn name_edit_guard_ignored_when_not_editing() {
        let mut mode = FileBrowserMode::ConfigPathSelection {
            target_item: ConfigItem::WatchFolder,
            current_settings: Box::default(),
            selected_index: 0,
            items: vec![],
        };
        let out = reduce_browser_download_action(
            BrowserDownloadAction::Edit(BrowserDownloadEditAction::Insert('x')),
            &mut mode,
        );
        assert!(!out.consumed);
    }

    #[test]
    fn reducer_download_shortcuts_toggle_pane() {
        let mut mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: "x".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 1,
            original_name_backup: "x".to_string(),
        };
        let out = reduce_browser_download_action(
            BrowserDownloadAction::Shortcut(BrowserDownloadShortcutAction::TogglePane),
            &mut mode,
        );
        assert!(out.consumed);
        match mode {
            FileBrowserMode::DownloadLocSelection { focused_pane, .. } => {
                assert_eq!(focused_pane, BrowserPane::TorrentPreview);
            }
            _ => panic!("expected DownloadLocSelection"),
        }
    }

    #[test]
    fn has_preview_content_matches_file_mode_torrent_extension() {
        let mode = FileBrowserMode::File(vec![".torrent".to_string()]);
        let path = PathBuf::from("demo.torrent");
        assert!(has_preview_content(&mode, false, false, Some(&path)));
    }

    #[test]
    fn has_preview_content_shows_existing_torrent_priority_pane() {
        let mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::ExistingTorrent {
                info_hash: vec![7; 20],
            },
            torrent_files: vec![],
            container_name: "Sample Files".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::TorrentPreview,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 0,
            original_name_backup: "Sample Files".to_string(),
        };

        assert!(has_preview_content(&mode, false, false, None));
    }

    #[test]
    fn preview_search_starts_only_from_preview_pane() {
        let mut mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: "Sample Files".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::TorrentPreview,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 0,
            original_name_backup: "Sample Files".to_string(),
        };

        assert!(preview_search_should_start(KeyCode::Char('/'), &mode));

        if let FileBrowserMode::DownloadLocSelection { focused_pane, .. } = &mut mode {
            *focused_pane = BrowserPane::FileSystem;
        }
        assert!(!preview_search_should_start(KeyCode::Char('/'), &mode));
    }

    #[test]
    fn torrent_preview_fuzzy_filter_matches_relative_path() {
        let tree = sample_preview_tree();
        let state = TreeViewState::default();
        let filter = build_torrent_preview_filter("alpb", SearchMode::Fuzzy);

        let rows = TreeMathHelper::get_visible_slice(&tree, &state, filter, 10);

        assert!(rows.iter().any(|row| row.node.name == "alpha.bin"));
        assert!(!rows.iter().any(|row| row.node.name == "beta.bin"));
    }

    #[test]
    fn torrent_preview_regex_filter_is_case_insensitive() {
        let tree = sample_preview_tree();
        let state = TreeViewState::default();
        let filter = build_torrent_preview_filter(r"group/[A-Z]+\.bin", SearchMode::Regex);

        let rows = TreeMathHelper::get_visible_slice(&tree, &state, filter, 10);

        assert!(rows.iter().any(|row| row.node.name == "alpha.bin"));
        assert!(rows.iter().any(|row| row.node.name == "beta.bin"));
    }

    #[test]
    fn torrent_preview_invalid_regex_matches_no_rows() {
        let tree = sample_preview_tree();
        let state = TreeViewState::default();
        let filter = build_torrent_preview_filter("[", SearchMode::Regex);

        let rows = TreeMathHelper::get_visible_slice(&tree, &state, filter, 10);

        assert!(rows.is_empty());
    }

    #[test]
    fn filesystem_fuzzy_filter_matches_relative_path() {
        let tree = sample_filesystem_tree();
        let state = TreeViewState::default();
        let filter =
            build_filesystem_filter(&FileBrowserMode::Directory, "alpb", SearchMode::Fuzzy);

        let rows = TreeMathHelper::get_visible_slice(&tree, &state, filter, 10);

        assert!(rows.iter().any(|row| row.node.name == "alpha.bin"));
        assert!(!rows.iter().any(|row| row.node.name == "beta.bin"));
    }

    #[test]
    fn filesystem_regex_filter_is_case_insensitive() {
        let tree = sample_filesystem_tree();
        let state = TreeViewState::default();
        let filter = build_filesystem_filter(
            &FileBrowserMode::Directory,
            r"group/[A-Z]+\.bin",
            SearchMode::Regex,
        );

        let rows = TreeMathHelper::get_visible_slice(&tree, &state, filter, 10);

        assert!(rows.iter().any(|row| row.node.name == "alpha.bin"));
        assert!(rows.iter().any(|row| row.node.name == "beta.bin"));
    }

    #[test]
    fn preview_reducer_navigate_consumes_direction_key() {
        let mut tree = vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![RawNode {
                name: "child".to_string(),
                full_path: PathBuf::from("root/child"),
                children: vec![],
                payload: TorrentPreviewPayload::default(),
                is_dir: false,
            }],
            payload: TorrentPreviewPayload::default(),
            is_dir: true,
        }];
        let mut state = TreeViewState::default();
        state.expanded_paths.insert(PathBuf::from("root"));
        state.cursor_path = Some(PathBuf::from("root"));
        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Down),
            &mut state,
            &mut tree,
            TreeFilter::default(),
            Some(10),
        );
        assert!(out.consumed);
        assert_eq!(state.cursor_path, Some(PathBuf::from("root/child")));
    }

    #[test]
    fn preview_reducer_navigates_filtered_rows() {
        let mut tree = sample_preview_tree();
        let mut state = TreeViewState {
            cursor_path: Some(PathBuf::from("root")),
            ..Default::default()
        };

        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Down),
            &mut state,
            &mut tree,
            build_torrent_preview_filter("beta", SearchMode::Fuzzy),
            Some(10),
        );

        assert!(out.consumed);
        assert_eq!(state.cursor_path, Some(PathBuf::from("root/group")));

        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Down),
            &mut state,
            &mut tree,
            build_torrent_preview_filter("beta", SearchMode::Fuzzy),
            Some(10),
        );

        assert!(out.consumed);
        assert_eq!(
            state.cursor_path,
            Some(PathBuf::from("root/group/beta.bin"))
        );
    }

    #[test]
    fn preview_reducer_passes_through_confirm_key() {
        let mut tree: Vec<RawNode<TorrentPreviewPayload>> = vec![];
        let mut state = TreeViewState::default();
        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char('Y')),
            &mut state,
            &mut tree,
            TreeFilter::default(),
            Some(10),
        );
        assert!(!out.consumed);
    }

    #[test]
    fn preview_reducer_ignores_unknown_key_with_consume() {
        let mut tree: Vec<RawNode<TorrentPreviewPayload>> = vec![];
        let mut state = TreeViewState::default();
        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char('z')),
            &mut state,
            &mut tree,
            TreeFilter::default(),
            Some(10),
        );
        assert!(out.consumed);
    }

    #[test]
    fn preview_reducer_cycles_priority_on_space() {
        let mut tree = vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![],
            payload: TorrentPreviewPayload::default(),
            is_dir: true,
        }];
        let mut state = TreeViewState {
            cursor_path: Some(PathBuf::from("root")),
            ..Default::default()
        };

        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char(' ')),
            &mut state,
            &mut tree,
            TreeFilter::default(),
            Some(10),
        );

        assert!(out.consumed);
        assert_eq!(tree[0].payload.priority, FilePriority::Skip);
    }

    #[test]
    fn preview_reducer_cycles_priority_on_p() {
        let mut tree = vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![],
            payload: TorrentPreviewPayload::default(),
            is_dir: true,
        }];
        let mut state = TreeViewState {
            cursor_path: Some(PathBuf::from("root")),
            ..Default::default()
        };

        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char('p')),
            &mut state,
            &mut tree,
            TreeFilter::default(),
            Some(10),
        );

        assert!(out.consumed);
        assert_eq!(tree[0].payload.priority, FilePriority::Skip);
    }

    #[test]
    fn preview_reducer_does_not_cycle_hidden_filtered_cursor() {
        let mut tree = sample_preview_tree();
        let mut state = TreeViewState {
            cursor_path: Some(PathBuf::from("root/group/alpha.bin")),
            ..Default::default()
        };

        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char('p')),
            &mut state,
            &mut tree,
            build_torrent_preview_filter("beta", SearchMode::Fuzzy),
            Some(10),
        );

        assert!(out.consumed);
        assert_eq!(
            tree[0].children[0].children[0].payload.priority,
            FilePriority::Normal
        );
        assert_eq!(
            tree[0].children[0].children[1].payload.priority,
            FilePriority::Normal
        );
    }

    #[test]
    fn download_name_edit_preserves_utf8_boundaries() {
        let mut name = String::from("Café Seed");
        let mut is_editing = true;
        let mut cursor_pos = "Café".len();

        reduce_download_name_edit_action(
            BrowserDownloadEditAction::Backspace,
            &mut name,
            &mut is_editing,
            &mut cursor_pos,
            "Café Seed",
        );

        assert_eq!(name, "Caf Seed");
        assert!(name.is_char_boundary(cursor_pos));
    }

    #[test]
    fn filesystem_cursor_visible_respects_search_filter() {
        let tree = sample_filesystem_tree();
        let mode = FileBrowserMode::File(vec![".bin".to_string()]);
        let state = TreeViewState {
            cursor_path: Some(PathBuf::from("root/group/alpha.bin")),
            ..Default::default()
        };

        assert!(filesystem_cursor_visible(
            &tree,
            &state,
            &mode,
            "alpha",
            SearchMode::Fuzzy,
            10,
        ));
        assert!(!filesystem_cursor_visible(
            &tree,
            &state,
            &mode,
            "beta",
            SearchMode::Fuzzy,
            10,
        ));
    }

    #[test]
    fn preview_keymap_includes_bulk_preview_shortcuts() {
        assert_eq!(
            map_preview_key_to_action(KeyCode::Char('P')),
            BrowserPreviewAction::CycleAllPriorities
        );
        assert_eq!(
            map_preview_key_to_action(KeyCode::Char('e')),
            BrowserPreviewAction::ExpandAll
        );
        assert_eq!(
            map_preview_key_to_action(KeyCode::Char('c')),
            BrowserPreviewAction::CollapseAll
        );
    }

    #[test]
    fn preview_reducer_expands_and_collapses_all() {
        let mut tree = vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![RawNode {
                name: "group".to_string(),
                full_path: PathBuf::from("root/group"),
                children: vec![RawNode {
                    name: "leaf".to_string(),
                    full_path: PathBuf::from("root/group/leaf"),
                    children: vec![],
                    payload: TorrentPreviewPayload::default(),
                    is_dir: false,
                }],
                payload: TorrentPreviewPayload::default(),
                is_dir: true,
            }],
            payload: TorrentPreviewPayload::default(),
            is_dir: true,
        }];
        let mut state = TreeViewState {
            cursor_path: Some(PathBuf::from("root/group/leaf")),
            ..Default::default()
        };

        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char('e')),
            &mut state,
            &mut tree,
            TreeFilter::default(),
            Some(10),
        );
        assert!(out.consumed);
        assert!(state.expanded_paths.contains(&PathBuf::from("root")));
        assert!(state.expanded_paths.contains(&PathBuf::from("root/group")));

        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char('c')),
            &mut state,
            &mut tree,
            TreeFilter::default(),
            Some(10),
        );
        assert!(out.consumed);
        assert!(state.expanded_paths.is_empty());
        assert_eq!(state.cursor_path, Some(PathBuf::from("root")));
    }

    #[test]
    fn preview_reducer_cycles_all_file_priorities_on_p() {
        let mut tree = vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![
                RawNode {
                    name: "alpha.bin".to_string(),
                    full_path: PathBuf::from("root/alpha.bin"),
                    children: vec![],
                    payload: TorrentPreviewPayload::default(),
                    is_dir: false,
                },
                RawNode {
                    name: "beta.bin".to_string(),
                    full_path: PathBuf::from("root/beta.bin"),
                    children: vec![],
                    payload: TorrentPreviewPayload::default(),
                    is_dir: false,
                },
            ],
            payload: TorrentPreviewPayload::default(),
            is_dir: true,
        }];
        let mut state = TreeViewState {
            cursor_path: Some(PathBuf::from("root")),
            ..Default::default()
        };

        let out = reduce_browser_preview_action(
            map_preview_key_to_action(KeyCode::Char('P')),
            &mut state,
            &mut tree,
            TreeFilter::default(),
            Some(10),
        );

        assert!(out.consumed);
        assert_eq!(tree[0].payload.priority, FilePriority::Skip);
        assert_eq!(tree[0].children[0].payload.priority, FilePriority::Skip);
        assert_eq!(tree[0].children[1].payload.priority, FilePriority::Skip);
    }

    #[test]
    fn filesystem_navigation_starts_search() {
        let mut state = TreeViewState {
            top_most_offset: 12,
            ..Default::default()
        };
        let data: Vec<RawNode<FileMetadata>> = vec![];
        let mode = FileBrowserMode::Directory;
        let (tx, _rx) = mpsc::channel(1);
        let (shutdown_tx, _) = broadcast::channel(1);
        let mut search_state = BrowserSearchState::Closed;
        let mut query = String::from("abc");
        let mut search_mode = SearchMode::Fuzzy;
        let consumed = handle_filesystem_navigation(
            KeyCode::Char('/'),
            BrowserFilesystemNavContext {
                state: &mut state,
                data: &data,
                browser_mode: &mode,
                search_state: &mut search_state,
                search_query: &mut query,
                search_mode: &mut search_mode,
                list_height: 5,
                app_command_tx: &tx,
                shutdown_tx: &shutdown_tx,
                browser_generation: 7,
            },
        );
        assert!(consumed);
        assert_eq!(search_state, BrowserSearchState::Editing);
        assert!(query.is_empty());
        assert_eq!(search_mode, SearchMode::Regex);
        assert_eq!(state.top_most_offset, 0);
    }

    #[tokio::test]
    async fn filesystem_navigation_queues_fetch_when_channel_is_full() {
        let dir = tempfile::tempdir().expect("create tempdir");
        let mut state = TreeViewState {
            current_path: dir.path().to_path_buf(),
            cursor_path: Some(dir.path().to_path_buf()),
            ..Default::default()
        };
        let data: Vec<RawNode<FileMetadata>> = vec![];
        let mode = FileBrowserMode::Directory;
        let (tx, mut rx) = mpsc::channel(1);
        let (shutdown_tx, _) = broadcast::channel(1);
        tx.try_send(AppCommand::RssSyncNow).expect("fill channel");
        let mut search_state = BrowserSearchState::Closed;
        let mut query = String::new();
        let mut search_mode = SearchMode::Regex;

        let consumed = handle_filesystem_navigation(
            KeyCode::Enter,
            BrowserFilesystemNavContext {
                state: &mut state,
                data: &data,
                browser_mode: &mode,
                search_state: &mut search_state,
                search_query: &mut query,
                search_mode: &mut search_mode,
                list_height: 5,
                app_command_tx: &tx,
                shutdown_tx: &shutdown_tx,
                browser_generation: 7,
            },
        );

        assert!(consumed);
        assert!(matches!(rx.recv().await, Some(AppCommand::RssSyncNow)));
        let next = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
            .await
            .expect("queued fetch should send after capacity opens");
        assert!(matches!(
            next,
            Some(AppCommand::FetchFileTree {
                browser_generation: 7,
                ref path,
                highlight_path: None,
                ..
            }) if path == dir.path()
        ));
    }

    fn sample_preview_tree() -> Vec<RawNode<TorrentPreviewPayload>> {
        vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![RawNode {
                name: "group".to_string(),
                full_path: PathBuf::from("root/group"),
                children: vec![
                    RawNode {
                        name: "alpha.bin".to_string(),
                        full_path: PathBuf::from("root/group/alpha.bin"),
                        children: vec![],
                        payload: TorrentPreviewPayload::default(),
                        is_dir: false,
                    },
                    RawNode {
                        name: "beta.bin".to_string(),
                        full_path: PathBuf::from("root/group/beta.bin"),
                        children: vec![],
                        payload: TorrentPreviewPayload::default(),
                        is_dir: false,
                    },
                ],
                payload: TorrentPreviewPayload::default(),
                is_dir: true,
            }],
            payload: TorrentPreviewPayload::default(),
            is_dir: true,
        }]
    }

    fn sample_filesystem_tree() -> Vec<RawNode<FileMetadata>> {
        vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![RawNode {
                name: "group".to_string(),
                full_path: PathBuf::from("root/group"),
                children: vec![
                    RawNode {
                        name: "alpha.bin".to_string(),
                        full_path: PathBuf::from("root/group/alpha.bin"),
                        children: vec![],
                        payload: FileMetadata {
                            size: 1,
                            modified: std::time::UNIX_EPOCH,
                        },
                        is_dir: false,
                    },
                    RawNode {
                        name: "beta.bin".to_string(),
                        full_path: PathBuf::from("root/group/beta.bin"),
                        children: vec![],
                        payload: FileMetadata {
                            size: 1,
                            modified: std::time::UNIX_EPOCH,
                        },
                        is_dir: false,
                    },
                ],
                payload: FileMetadata {
                    size: 0,
                    modified: std::time::UNIX_EPOCH,
                },
                is_dir: true,
            }],
            payload: FileMetadata {
                size: 0,
                modified: std::time::UNIX_EPOCH,
            },
            is_dir: true,
        }]
    }

    #[test]
    fn confirm_config_path_selection_returns_config_mode() {
        let mode = FileBrowserMode::ConfigPathSelection {
            target_item: ConfigItem::WatchFolder,
            current_settings: Box::default(),
            selected_index: 2,
            items: vec![ConfigItem::WatchFolder],
        };
        let state = TreeViewState {
            current_path: PathBuf::from("/tmp"),
            ..Default::default()
        };
        let out = confirm_config_path_selection(&state, &mode);
        assert!(matches!(out, Some(ConfigUiState { .. })));
    }

    #[test]
    fn resolve_confirm_decision_prefers_config_path_mode() {
        let mode = FileBrowserMode::ConfigPathSelection {
            target_item: ConfigItem::WatchFolder,
            current_settings: Box::default(),
            selected_index: 0,
            items: vec![ConfigItem::WatchFolder],
        };
        let state = TreeViewState {
            current_path: PathBuf::from("/tmp"),
            ..Default::default()
        };
        let decision = resolve_confirm_decision(&state, &mode);
        assert!(matches!(
            decision,
            ConfirmDecision::ToConfig(ConfigUiState { .. })
        ));
    }

    #[test]
    fn reducer_dialog_confirm_emits_execute_and_clear_search() {
        let mode = FileBrowserMode::Directory;
        let state = TreeViewState::default();

        let out = reduce_browser_dialog_action(
            BrowserDialogAction::ConfirmSelection,
            &state,
            &mode,
            false,
        );

        assert_eq!(out.effects.len(), 2);
        assert!(matches!(
            out.effects[0],
            BrowserDialogEffect::ExecuteConfirmDecision(_)
        ));
        assert!(matches!(out.effects[1], BrowserDialogEffect::ClearSearch));
    }

    #[test]
    fn reducer_dialog_escape_prefers_config_switch() {
        let mode = FileBrowserMode::ConfigPathSelection {
            target_item: ConfigItem::WatchFolder,
            current_settings: Box::default(),
            selected_index: 0,
            items: vec![ConfigItem::WatchFolder],
        };
        let state = TreeViewState::default();

        let out = reduce_browser_dialog_action(BrowserDialogAction::Escape, &state, &mode, true);

        assert_eq!(out.effects.len(), 2);
        assert!(matches!(out.effects[0], BrowserDialogEffect::ClearSearch));
        assert!(matches!(
            out.effects[1],
            BrowserDialogEffect::ToConfig(ConfigUiState { .. })
        ));
    }

    #[test]
    fn reducer_dialog_escape_directory_clears_search_and_exits_without_cleanup() {
        let mode = FileBrowserMode::Directory;
        let state = TreeViewState::default();

        let out = reduce_browser_dialog_action(BrowserDialogAction::Escape, &state, &mode, true);

        assert_eq!(out.effects.len(), 2);
        assert!(matches!(out.effects[0], BrowserDialogEffect::ClearSearch));
        assert!(matches!(
            out.effects[1],
            BrowserDialogEffect::ToNormalAndClearPending
        ));
    }

    #[test]
    fn reducer_dialog_escape_download_with_pending_cleans_then_exits() {
        let mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: "x".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 1,
            original_name_backup: "x".to_string(),
        };
        let state = TreeViewState::default();

        let out = reduce_browser_dialog_action(BrowserDialogAction::Escape, &state, &mode, true);

        assert_eq!(out.effects.len(), 3);
        assert!(matches!(
            out.effects[0],
            BrowserDialogEffect::CleanupPendingLink
        ));
        assert!(matches!(out.effects[1], BrowserDialogEffect::ClearSearch));
        assert!(matches!(
            out.effects[2],
            BrowserDialogEffect::ToNormalAndClearPending
        ));
    }

    #[test]
    fn reducer_dialog_cancel_download_emits_async_cleanup_and_exit() {
        let mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: "x".to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 1,
            original_name_backup: "x".to_string(),
        };
        let state = TreeViewState::default();

        let out = reduce_browser_dialog_action(
            BrowserDialogAction::CancelDownloadSelection,
            &state,
            &mode,
            true,
        );

        assert_eq!(out.effects.len(), 3);
        assert!(matches!(
            out.effects[0],
            BrowserDialogEffect::CleanupPendingLink
        ));
        assert!(matches!(out.effects[1], BrowserDialogEffect::ClearSearch));
        assert!(matches!(
            out.effects[2],
            BrowserDialogEffect::ToNormalAndClearPending
        ));
    }

    #[test]
    fn pending_link_hash_is_none_for_empty() {
        assert!(pending_link_info_hash("").is_none());
    }

    #[test]
    fn awaiting_magnet_metadata_confirms_without_placeholder_container_name() {
        let mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: AWAITING_MAGNET_METADATA_LABEL.to_string(),
            use_container: true,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 1,
            original_name_backup: AWAITING_MAGNET_METADATA_LABEL.to_string(),
        };
        let state = TreeViewState::default();

        let payload = build_download_confirm_payload(&state, &mode)
            .expect("unhydrated magnet can still be confirmed");

        assert_eq!(payload.container_name_to_use, None);
        assert!(payload.file_priorities.is_empty());
        assert!(!payload.has_preview_files);
    }

    #[test]
    fn awaiting_magnet_metadata_confirm_keeps_disabled_container_empty() {
        let mode = FileBrowserMode::DownloadLocSelection {
            target: DownloadSelectionTarget::PendingAdd,
            torrent_files: vec![],
            container_name: AWAITING_MAGNET_METADATA_LABEL.to_string(),
            use_container: false,
            is_editing_name: false,
            focused_pane: BrowserPane::FileSystem,
            preview_tree: vec![],
            preview_state: TreeViewState::default(),
            cursor_pos: 1,
            original_name_backup: AWAITING_MAGNET_METADATA_LABEL.to_string(),
        };
        let state = TreeViewState::default();

        let payload = build_download_confirm_payload(&state, &mode)
            .expect("unhydrated magnet can be confirmed without a container");

        assert_eq!(payload.container_name_to_use, Some(String::new()));
    }

    #[test]
    fn pending_link_info_hash_falls_back_to_btmh_only_magnet() {
        let pending_link = concat!(
            "magnet:?xt=urn:btmh:1220",
            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
        );

        let info_hash = pending_link_info_hash(pending_link).expect("btmh-only info hash");

        assert_eq!(info_hash.len(), 20);
        assert!(info_hash.iter().all(|byte| *byte == 0xaa));
    }

    #[test]
    fn apply_priority_cycle_updates_target_tree() {
        let mut nodes = vec![RawNode {
            name: "root".to_string(),
            full_path: PathBuf::from("root"),
            children: vec![RawNode {
                name: "leaf".to_string(),
                full_path: PathBuf::from("root/leaf"),
                children: vec![],
                payload: TorrentPreviewPayload::default(),
                is_dir: false,
            }],
            payload: TorrentPreviewPayload::default(),
            is_dir: true,
        }];

        let changed = apply_priority_cycle(&mut nodes, &PathBuf::from("root"));
        assert!(changed);
        assert_eq!(nodes[0].payload.priority, FilePriority::Skip);
        assert_eq!(nodes[0].children[0].payload.priority, FilePriority::Skip);
    }
}
