use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::HookMetadata;
use codex_app_server_protocol::HookSource;
use codex_app_server_protocol::HookTrustStatus;
use codex_app_server_protocol::HooksListEntry;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use ratatui::buffer::Buffer;
use ratatui::layout::Constraint;
use ratatui::layout::Layout;
use ratatui::layout::Rect;
use ratatui::style::Styled;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use strum::IntoEnumIterator;
use unicode_width::UnicodeWidthStr;

use super::CancellationEvent;
use super::bottom_pane_view::BottomPaneView;
use super::popup_consts::MAX_POPUP_ROWS;
use super::scroll_state::ScrollState;
use super::selection_popup_common::render_menu_surface;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::hooks_rpc::HookTrustUpdate;
use crate::hooks_rpc::hook_needs_review;
use crate::key_hint;
use crate::key_hint::KeyBindingListExt;
use crate::keymap::ListKeymap;
use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
use crate::render::renderable::Renderable;
use crate::status::format_directory_display;
use crate::style::accent_style;

const EVENT_COLUMN_WIDTH: usize = 22;
const COUNT_COLUMN_WIDTH: usize = 12;
const MAX_COMMAND_DETAIL_LINES: usize = 3;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HooksBrowserPage {
    Events,
    Handlers(HookEventName),
}

pub(crate) struct HooksBrowserView {
    entry: HooksListEntry,
    page: HooksBrowserPage,
    state: ScrollState,
    complete: bool,
    app_event_tx: AppEventSender,
    keymap: ListKeymap,
}

impl HooksBrowserView {
    #[cfg(test)]
    pub(crate) fn new(
        hooks: Vec<HookMetadata>,
        warnings: Vec<String>,
        errors: Vec<codex_app_server_protocol::HookErrorInfo>,
        app_event_tx: AppEventSender,
    ) -> Self {
        Self::from_entry(
            HooksListEntry {
                cwd: std::path::PathBuf::new(),
                hooks,
                warnings,
                errors,
            },
            app_event_tx,
            crate::keymap::RuntimeKeymap::defaults().list,
        )
    }

    pub(crate) fn from_entry(
        mut entry: HooksListEntry,
        app_event_tx: AppEventSender,
        keymap: ListKeymap,
    ) -> Self {
        entry.hooks.sort_by_key(|hook| hook.display_order);
        let mut view = Self {
            entry,
            page: HooksBrowserPage::Events,
            state: ScrollState::new(),
            complete: false,
            app_event_tx,
            keymap,
        };
        if view.page_len() > 0 {
            view.state.selected_idx = Some(
                view.event_rows()
                    .iter()
                    .position(|row| row.needs_review > 0)
                    .unwrap_or(0),
            );
        }
        view
    }

    fn event_rows(&self) -> Vec<EventRow> {
        codex_protocol::protocol::HookEventName::iter()
            .map(|event_name| {
                let event_name: HookEventName = event_name.into();
                let installed = self
                    .entry
                    .hooks
                    .iter()
                    .filter(|hook| hook.event_name == event_name)
                    .count();
                let active = self
                    .entry
                    .hooks
                    .iter()
                    .filter(|hook| hook.event_name == event_name && hook_is_active(hook))
                    .count();
                let needs_review = self
                    .entry
                    .hooks
                    .iter()
                    .filter(|hook| hook.event_name == event_name && hook_needs_review(hook))
                    .count();
                EventRow {
                    event_name,
                    installed,
                    active,
                    needs_review,
                }
            })
            .collect()
    }

    fn handlers_for_event(&self, event_name: HookEventName) -> impl Iterator<Item = &HookMetadata> {
        self.entry
            .hooks
            .iter()
            .filter(move |hook| hook.event_name == event_name)
    }

    fn selected_event(&self) -> Option<HookEventName> {
        self.state
            .selected_idx
            .and_then(|idx| codex_protocol::protocol::HookEventName::iter().nth(idx))
            .map(Into::into)
    }

    fn selected_hook_index(&self, event_name: HookEventName) -> Option<usize> {
        let selected_visible_idx = self.state.selected_idx?;
        self.entry
            .hooks
            .iter()
            .enumerate()
            .filter(|(_, hook)| hook.event_name == event_name)
            .nth(selected_visible_idx)
            .map(|(idx, _)| idx)
    }

    fn selected_hook(&self, event_name: HookEventName) -> Option<&HookMetadata> {
        self.selected_hook_index(event_name)
            .and_then(|idx| self.entry.hooks.get(idx))
    }

    fn move_up(&mut self) {
        let len = self.page_len();
        self.state.move_up_wrap(len);
        self.state.ensure_visible(len, self.max_visible_rows());
    }

    fn move_down(&mut self) {
        let len = self.page_len();
        self.state.move_down_wrap(len);
        self.state.ensure_visible(len, self.max_visible_rows());
    }

    fn page_up(&mut self) {
        let len = self.page_len();
        self.state.page_up_clamped(len, self.max_visible_rows());
    }

    fn page_down(&mut self) {
        let len = self.page_len();
        self.state.page_down_clamped(len, self.max_visible_rows());
    }

    fn jump_top(&mut self) {
        let len = self.page_len();
        self.state.jump_top(len, self.max_visible_rows());
    }

    fn jump_bottom(&mut self) {
        let len = self.page_len();
        self.state.jump_bottom(len, self.max_visible_rows());
    }

    fn page_len(&self) -> usize {
        match self.page {
            HooksBrowserPage::Events => codex_protocol::protocol::HookEventName::iter().count(),
            HooksBrowserPage::Handlers(event_name) => self.handlers_for_event(event_name).count(),
        }
    }

    fn max_visible_rows(&self) -> usize {
        MAX_POPUP_ROWS.min(self.page_len().max(1))
    }

    fn open_selected_event(&mut self) {
        let Some(event_name) = self.selected_event() else {
            return;
        };
        self.page = HooksBrowserPage::Handlers(event_name);
        self.state = ScrollState::new();
        if self.page_len() > 0 {
            self.state.selected_idx = Some(0);
        }
    }

    fn toggle_selected_hook(&mut self, event_name: HookEventName) {
        let Some(idx) = self.selected_hook_index(event_name) else {
            return;
        };
        let Some(hook) = self.entry.hooks.get_mut(idx) else {
            return;
        };
        if hook.is_managed {
            return;
        }
        if hook_needs_review(hook) {
            return;
        }

        hook.enabled = !hook.enabled;
        self.app_event_tx.send(AppEvent::SetHookEnabled {
            key: hook.key.clone(),
            enabled: hook.enabled,
        });
    }

    fn trust_selected_hook(&mut self, event_name: HookEventName) {
        let Some(idx) = self.selected_hook_index(event_name) else {
            return;
        };
        let Some(hook) = self.entry.hooks.get_mut(idx) else {
            return;
        };
        if !hook_needs_review(hook) {
            return;
        }

        hook.trust_status = HookTrustStatus::Trusted;
        self.app_event_tx.send(AppEvent::TrustHook {
            key: hook.key.clone(),
            current_hash: hook.current_hash.clone(),
        });
    }

    fn trust_all_hooks(&mut self) {
        let mut updates = Vec::new();
        for hook in &mut self.entry.hooks {
            if !hook_needs_review(hook) {
                continue;
            }

            hook.trust_status = HookTrustStatus::Trusted;
            updates.push(HookTrustUpdate {
                key: hook.key.clone(),
                current_hash: hook.current_hash.clone(),
            });
        }
        if !updates.is_empty() {
            self.app_event_tx.send(AppEvent::TrustHooks { updates });
        }
    }

    fn close(&mut self) {
        self.complete = true;
    }

    fn return_to_events(&mut self) {
        let selected_event_name = match self.page {
            HooksBrowserPage::Events => None,
            HooksBrowserPage::Handlers(event_name) => Some(event_name),
        };
        self.page = HooksBrowserPage::Events;
        self.state = ScrollState::new();
        self.state.selected_idx = selected_event_name
            .and_then(|event_name| {
                codex_protocol::protocol::HookEventName::iter()
                    .position(|candidate| HookEventName::from(candidate) == event_name)
            })
            .or_else(|| (self.page_len() > 0).then_some(0));
    }

    fn event_header_lines() -> Vec<Line<'static>> {
        vec![
            "Hooks".bold().into(),
            "Lifecycle hooks from config and enabled plugins."
                .dim()
                .into(),
        ]
    }

    fn review_needed_total_count(&self) -> usize {
        self.entry
            .hooks
            .iter()
            .filter(|hook| hook_needs_review(hook))
            .count()
    }

    #[allow(clippy::disallowed_methods)]
    fn handler_header_lines(
        event_name: HookEventName,
        review_needed_count: usize,
    ) -> Vec<Line<'static>> {
        let mut lines = vec![format!("{} hooks", event_label(event_name)).bold().into()];
        match review_needed_message(review_needed_count) {
            None => lines.push(
                "Turn hooks on or off. Your changes are saved automatically."
                    .dim()
                    .into(),
            ),
            Some(message) => lines.push(message.yellow().into()),
        }
        lines
    }

    fn review_needed_count(&self, event_name: HookEventName) -> usize {
        self.handlers_for_event(event_name)
            .filter(|hook| hook_needs_review(hook))
            .count()
    }

    #[allow(clippy::disallowed_methods)]
    fn event_table_lines(&self) -> Vec<Line<'static>> {
        let rows = self.event_rows();
        let show_review = rows.iter().any(|row| row.needs_review > 0);
        let mut lines = Vec::new();
        let mut header = vec![
            format!("{:<EVENT_COLUMN_WIDTH$}", "Event").into(),
            format!("{:<COUNT_COLUMN_WIDTH$}", "Installed").into(),
            format!("{:<COUNT_COLUMN_WIDTH$}", "Active").into(),
        ];
        if show_review {
            header.push(format!("{:<COUNT_COLUMN_WIDTH$}", "Review").into());
        }
        header.push("Description".into());
        lines.push(Line::from(header));
        for (idx, row) in rows.into_iter().enumerate() {
            let selected = self.state.selected_idx == Some(idx);
            let needs_review = row.needs_review > 0;
            let mut row_line = vec![
                Span::from(format!(
                    "{:<EVENT_COLUMN_WIDTH$}",
                    event_label(row.event_name)
                )),
                Span::from(format!("{:<COUNT_COLUMN_WIDTH$}", row.installed)),
                Span::from(format!("{:<COUNT_COLUMN_WIDTH$}", row.active)),
            ];
            if show_review {
                let review_count = Span::from(format!("{:<COUNT_COLUMN_WIDTH$}", row.needs_review));
                row_line.push(if needs_review {
                    review_count.yellow()
                } else {
                    review_count
                });
            }
            row_line.push(Span::from(event_description(row.event_name)));

            if selected {
                let style = accent_style();
                for span in &mut row_line {
                    *span = span.clone().set_style(style);
                }
            } else {
                row_line[1] = row_line[1].clone().dim();
                row_line[2] = row_line[2].clone().dim();
                if show_review && !needs_review {
                    row_line[3] = row_line[3].clone().dim();
                }
                let description_idx = row_line.len() - 1;
                row_line[description_idx] = row_line[description_idx].clone().dim();
            }
            lines.push(Line::from(row_line));
        }
        lines
    }

    fn event_issue_lines(&self) -> Vec<Line<'static>> {
        let mut lines = Vec::new();
        if self.entry.warnings.is_empty() && self.entry.errors.is_empty() {
            return lines;
        }

        lines.push("Issues".bold().into());
        lines.extend(
            self.entry
                .warnings
                .iter()
                .map(|warning| format!("⚠ {warning}").into()),
        );
        lines.extend(self.entry.errors.iter().map(|error| {
            format!("■ {}: {}", error.path.display(), error.message)
                .red()
                .into()
        }));
        lines
    }

    #[allow(clippy::disallowed_methods)]
    fn event_page_lines(&self) -> Vec<Line<'static>> {
        let mut lines = Self::event_header_lines();
        lines.push(Line::default());

        if let Some(message) = review_needed_message(self.review_needed_total_count()) {
            lines.push(format!("⚠ {message}").yellow().into());
            lines.push(Line::default());
        }

        let issue_lines = self.event_issue_lines();
        if !issue_lines.is_empty() {
            lines.extend(issue_lines);
            lines.push(Line::default());
        }

        lines.extend(self.event_table_lines());
        lines
    }

    #[allow(clippy::disallowed_methods)]
    fn handler_row_lines(&self, event_name: HookEventName, width: usize) -> Vec<Line<'static>> {
        self.handlers_for_event(event_name)
            .enumerate()
            .map(|(idx, hook)| {
                let marker = if hook_needs_review(hook) {
                    '!'
                } else if hook_is_active(hook) {
                    'x'
                } else {
                    ' '
                };
                let row = match hook.trust_status {
                    HookTrustStatus::Modified => {
                        format!("[{marker}] {} · modified", hook_title(idx))
                    }
                    HookTrustStatus::Untrusted => format!("[{marker}] {} · new", hook_title(idx)),
                    HookTrustStatus::Managed | HookTrustStatus::Trusted => {
                        format!("[{marker}] {}", hook_title(idx))
                    }
                };
                let mut line = Line::from(row);
                line = truncate_line_with_ellipsis_if_overflow(line, width);
                let needs_review = hook_needs_review(hook);
                if self.state.selected_idx == Some(idx) {
                    if needs_review {
                        line = line.yellow().bold();
                    } else {
                        line = line.patch_style(accent_style());
                    }
                } else if needs_review {
                    line = line.yellow();
                } else if hook.is_managed {
                    line = line.dim();
                }
                line
            })
            .collect()
    }

    fn detail_lines(&self, event_name: HookEventName, width: usize) -> Vec<Line<'static>> {
        let Some(hook) = self.selected_hook(event_name) else {
            return vec!["No hooks installed for this event.".dim().into()];
        };

        let mut lines = vec![detail_line("Event", event_label(event_name))];
        if let Some(matcher) = hook.matcher.as_deref() {
            lines.extend(detail_wrapped_lines(
                "Matcher", matcher, width, /*max_lines*/ None,
            ));
        }
        lines.extend(detail_wrapped_lines(
            "Source",
            &detail_source_value(hook),
            width,
            /*max_lines*/ None,
        ));
        lines.extend(detail_wrapped_lines(
            "Command",
            hook.command.as_deref().unwrap_or("-"),
            width,
            Some(MAX_COMMAND_DETAIL_LINES),
        ));
        lines.push(detail_line("Timeout", &format!("{}s", hook.timeout_sec)));
        lines.push(detail_line("Trust", hook_trust_label(hook.trust_status)));
        lines
    }

    fn render_footer(&self, area: Rect, buf: &mut Buffer) {
        let hint_area = Rect {
            x: area.x + 2,
            y: area.y,
            width: area.width.saturating_sub(2),
            height: area.height,
        };
        let footer = match self.page {
            HooksBrowserPage::Events if self.review_needed_total_count() > 0 => Line::from(vec![
                "Press ".into(),
                key_hint::plain(KeyCode::Char('t')).into(),
                " to trust all; ".into(),
                key_hint::plain(KeyCode::Enter).into(),
                " to review hooks; ".into(),
                key_hint::plain(KeyCode::Esc).into(),
                " to close".into(),
            ]),
            HooksBrowserPage::Events => Line::from(vec![
                "Press ".into(),
                key_hint::plain(KeyCode::Enter).into(),
                " to view hooks; ".into(),
                key_hint::plain(KeyCode::Esc).into(),
                " to close".into(),
            ]),
            HooksBrowserPage::Handlers(event_name) => {
                let selected_hook = self.selected_hook(event_name);
                if selected_hook.is_none() {
                    Line::from(vec![
                        "Press ".into(),
                        key_hint::plain(KeyCode::Esc).into(),
                        " to go back".into(),
                    ])
                } else if selected_hook.is_some_and(|hook| hook.is_managed) {
                    Line::from(vec![
                        "Managed hooks are always on; press ".into(),
                        key_hint::plain(KeyCode::Esc).into(),
                        " to go back".into(),
                    ])
                } else if selected_hook.is_some_and(hook_needs_review) {
                    Line::from(vec![
                        "Press ".into(),
                        key_hint::plain(KeyCode::Char('t')).into(),
                        " to trust; ".into(),
                        key_hint::plain(KeyCode::Esc).into(),
                        " to go back".into(),
                    ])
                } else {
                    Line::from(vec![
                        "Press ".into(),
                        key_hint::plain(KeyCode::Char(' ')).into(),
                        " or ".into(),
                        key_hint::plain(KeyCode::Enter).into(),
                        " to toggle; ".into(),
                        key_hint::plain(KeyCode::Esc).into(),
                        " to go back".into(),
                    ])
                }
            }
        };
        footer.dim().render(hint_area, buf);
    }
}

impl BottomPaneView for HooksBrowserView {
    fn handle_key_event(&mut self, key_event: KeyEvent) {
        match key_event {
            _ if self.keymap.move_up.is_pressed(key_event) => self.move_up(),
            _ if self.keymap.move_down.is_pressed(key_event) => self.move_down(),
            _ if self.keymap.page_up.is_pressed(key_event) => self.page_up(),
            _ if self.keymap.page_down.is_pressed(key_event) => self.page_down(),
            _ if self.keymap.jump_top.is_pressed(key_event) => self.jump_top(),
            _ if self.keymap.jump_bottom.is_pressed(key_event) => self.jump_bottom(),
            _ if self.keymap.accept.is_pressed(key_event)
                && self.page == HooksBrowserPage::Events =>
            {
                self.open_selected_event()
            }
            _ if self.keymap.accept.is_pressed(key_event) => {
                if let HooksBrowserPage::Handlers(event_name) = self.page {
                    self.toggle_selected_hook(event_name);
                }
            }
            KeyEvent {
                code: KeyCode::Char(' '),
                modifiers: KeyModifiers::NONE,
                ..
            } => {
                if let HooksBrowserPage::Handlers(event_name) = self.page {
                    self.toggle_selected_hook(event_name);
                }
            }
            KeyEvent {
                code: KeyCode::Char('t'),
                modifiers: KeyModifiers::NONE,
                ..
            } => match self.page {
                HooksBrowserPage::Events => self.trust_all_hooks(),
                HooksBrowserPage::Handlers(event_name) => self.trust_selected_hook(event_name),
            },
            _ if self.keymap.cancel.is_pressed(key_event) => match self.page {
                HooksBrowserPage::Events => self.close(),
                HooksBrowserPage::Handlers(_) => self.return_to_events(),
            },
            _ => {}
        }
    }

    fn is_complete(&self) -> bool {
        self.complete
    }

    fn on_ctrl_c(&mut self) -> CancellationEvent {
        self.close();
        CancellationEvent::Handled
    }

    fn prefer_esc_to_handle_key_event(&self) -> bool {
        true
    }
}

impl Renderable for HooksBrowserView {
    fn desired_height(&self, width: u16) -> u16 {
        let content_width = width.saturating_sub(4) as usize;
        let height = match self.page {
            HooksBrowserPage::Events => self.event_page_lines().len(),
            HooksBrowserPage::Handlers(event_name) => {
                let row_count = self.handler_row_lines(event_name, content_width).len();
                let header_line_count =
                    Self::handler_header_lines(event_name, self.review_needed_count(event_name))
                        .len();
                if row_count == 0 {
                    header_line_count + 2
                } else {
                    let visible_row_count = row_count.min(MAX_POPUP_ROWS);
                    header_line_count
                        + 1
                        + visible_row_count
                        + 1
                        + self.detail_lines(event_name, content_width).len()
                }
            }
        };
        (height + 3).try_into().unwrap_or(u16::MAX)
    }

    fn render(&self, area: Rect, buf: &mut Buffer) {
        if area.is_empty() {
            return;
        }

        let [content_area, footer_area] =
            Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(area);
        let content_area = render_menu_surface(content_area, buf);
        let width = content_area.width as usize;
        let lines = match self.page {
            HooksBrowserPage::Events => self.event_page_lines(),
            HooksBrowserPage::Handlers(event_name) => {
                let mut lines =
                    Self::handler_header_lines(event_name, self.review_needed_count(event_name));
                let rows = self.handler_row_lines(event_name, width);
                if rows.is_empty() {
                    lines.push(Line::default());
                    lines.push(Line::from(
                        "No hooks installed for this event.".dim().italic(),
                    ));
                    lines.push(Line::default());
                    Paragraph::new(lines).render(content_area, buf);
                    self.render_footer(footer_area, buf);
                    return;
                }
                let list_height = rows.len().clamp(1, MAX_POPUP_ROWS) as u16;
                lines.push(Line::default());
                let header_height = lines.len() as u16;
                let [header_area, list_area, detail_area] = Layout::vertical([
                    Constraint::Length(header_height),
                    Constraint::Length(list_height),
                    Constraint::Fill(1),
                ])
                .areas(content_area);
                Paragraph::new(lines.clone()).render(header_area, buf);
                let visible_rows = rows
                    .into_iter()
                    .skip(self.state.scroll_top)
                    .take(list_height as usize)
                    .collect::<Vec<_>>();
                Paragraph::new(visible_rows).render(list_area, buf);
                let mut detail_lines = vec![Line::default()];
                detail_lines.extend(self.detail_lines(event_name, width));
                Paragraph::new(detail_lines).render(detail_area, buf);
                self.render_footer(footer_area, buf);
                return;
            }
        };
        Paragraph::new(lines).render(content_area, buf);
        self.render_footer(footer_area, buf);
    }
}

fn hook_is_active(hook: &HookMetadata) -> bool {
    hook.enabled
        && matches!(
            hook.trust_status,
            HookTrustStatus::Managed | HookTrustStatus::Trusted
        )
}

fn review_needed_message(count: usize) -> Option<String> {
    match count {
        0 => None,
        1 => Some("1 hook needs review before it can run.".to_string()),
        count => Some(format!("{count} hooks need review before they can run.")),
    }
}

struct EventRow {
    event_name: HookEventName,
    installed: usize,
    active: usize,
    needs_review: usize,
}

fn hook_trust_label(status: HookTrustStatus) -> &'static str {
    match status {
        HookTrustStatus::Managed => "Managed",
        HookTrustStatus::Trusted => "Trusted",
        HookTrustStatus::Untrusted => "New hook - review required",
        HookTrustStatus::Modified => "Modified since last trusted - review required",
    }
}

fn event_label(event_name: HookEventName) -> &'static str {
    match event_name {
        HookEventName::PreToolUse => "PreToolUse",
        HookEventName::PermissionRequest => "PermissionRequest",
        HookEventName::PostToolUse => "PostToolUse",
        HookEventName::PreCompact => "PreCompact",
        HookEventName::PostCompact => "PostCompact",
        HookEventName::SessionStart => "SessionStart",
        HookEventName::UserPromptSubmit => "UserPromptSubmit",
        HookEventName::SubagentStart => "SubagentStart",
        HookEventName::SubagentStop => "SubagentStop",
        HookEventName::Stop => "Stop",
    }
}

fn event_description(event_name: HookEventName) -> &'static str {
    match event_name {
        HookEventName::PreToolUse => "Before a tool executes",
        HookEventName::PermissionRequest => "When permission is requested",
        HookEventName::PostToolUse => "After a tool executes",
        HookEventName::PreCompact => "Before context compaction",
        HookEventName::PostCompact => "After context compaction",
        HookEventName::SessionStart => "When a new session starts",
        HookEventName::UserPromptSubmit => "When the user submits a prompt",
        HookEventName::SubagentStart => "When a subagent is created",
        HookEventName::SubagentStop => "Right before a subagent ends its turn",
        HookEventName::Stop => "Right before Codex ends its turn",
    }
}

fn hook_title(idx: usize) -> String {
    format!("Hook {}", idx + 1)
}

fn hook_source_summary(hook: &HookMetadata) -> String {
    match hook.source {
        HookSource::Plugin => hook
            .plugin_id
            .as_deref()
            .map(|plugin_id| format!("Plugin - {plugin_id}"))
            .unwrap_or_else(|| "Plugin".to_string()),
        _ => config_source_label(hook.source).to_string(),
    }
}

fn detail_source_value(hook: &HookMetadata) -> String {
    match hook.source {
        HookSource::Plugin => hook_source_summary(hook),
        HookSource::System
        | HookSource::Mdm
        | HookSource::CloudRequirements
        | HookSource::CloudManagedConfig
        | HookSource::LegacyManagedConfigFile
        | HookSource::LegacyManagedConfigMdm => config_source_label(hook.source).to_string(),
        _ => format!(
            "{} - {}",
            config_source_label(hook.source),
            format_directory_display(&hook.source_path, /*max_width*/ None)
        ),
    }
}

fn config_source_label(source: HookSource) -> &'static str {
    match source {
        HookSource::System => "Admin config",
        HookSource::User => "User config",
        HookSource::Project => "Project config",
        HookSource::Mdm => "Admin config",
        HookSource::SessionFlags => "Session flags",
        HookSource::Plugin => unreachable!("plugin hooks are handled by summary_source"),
        HookSource::CloudRequirements => "Admin config",
        HookSource::CloudManagedConfig => "Cloud-managed config",
        HookSource::LegacyManagedConfigFile => "Admin config",
        HookSource::LegacyManagedConfigMdm => "Admin config",
        HookSource::Unknown => "Unknown source",
    }
}

fn detail_line(label: &str, value: &str) -> Line<'static> {
    Line::from(vec![format!("{label:<10}").into(), value.to_string().dim()])
}

fn detail_wrapped_lines(
    label: &str,
    value: &str,
    width: usize,
    max_lines: Option<usize>,
) -> Vec<Line<'static>> {
    let prefix = format!("{label:<10}");
    let available = width.saturating_sub(prefix.width()).max(1);
    let mut wrapped = textwrap::wrap(value, available).into_iter();
    let first = wrapped.next().unwrap_or_default().into_owned();
    let mut lines = vec![Line::from(vec![prefix.into(), first.dim()])];
    lines
        .extend(wrapped.map(|line| Line::from(vec!["          ".into(), line.into_owned().dim()])));
    let Some(max_lines) = max_lines else {
        return lines;
    };
    if lines.len() <= max_lines {
        return lines;
    }

    lines.truncate(max_lines);
    if let Some(last_line) = lines.last_mut() {
        let prefix_width = last_line.spans[..last_line.spans.len().saturating_sub(1)]
            .iter()
            .map(ratatui::prelude::Span::width)
            .sum::<usize>();
        let max_width = width.saturating_sub(prefix_width);
        let Some(last_span) = last_line.spans.last_mut() else {
            return lines;
        };
        let truncated = truncate_line_with_ellipsis_if_overflow(
            Line::from(format!("{}…", last_span.content)),
            max_width,
        );
        let content = truncated
            .spans
            .into_iter()
            .map(|span| span.content.into_owned())
            .collect::<String>();
        last_span.content = content.into();
    }
    lines
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app_event::AppEvent;
    use crate::app_event_sender::AppEventSender;
    use crate::bottom_pane::bottom_pane_view::BottomPaneView;
    use crate::render::renderable::Renderable;
    use crate::test_support::PathBufExt;
    use crate::test_support::test_path_buf;
    use crate::test_support::test_path_display;
    use codex_app_server_protocol::HookErrorInfo;
    use codex_app_server_protocol::HookEventName;
    use codex_app_server_protocol::HookHandlerType;
    use codex_app_server_protocol::HookMetadata;
    use codex_app_server_protocol::HookSource;
    use codex_app_server_protocol::HookTrustStatus;
    use crossterm::event::KeyCode;
    use crossterm::event::KeyEvent;
    use insta::assert_snapshot;
    use ratatui::buffer::Buffer;
    use ratatui::layout::Rect;
    use ratatui::style::Color;
    use ratatui::style::Modifier;
    use tokio::sync::mpsc::unbounded_channel;

    fn render_lines(view: &HooksBrowserView, width: u16) -> String {
        let height = view.desired_height(width);
        let area = Rect::new(0, 0, width, height);
        let mut buf = Buffer::empty(area);
        view.render(area, &mut buf);

        (0..area.height)
            .map(|row| {
                let rendered = (0..area.width)
                    .map(|col| {
                        let symbol = buf[(area.x + col, area.y + row)].symbol();
                        if symbol.is_empty() {
                            " ".to_string()
                        } else {
                            symbol.to_string()
                        }
                    })
                    .collect::<String>();
                let normalized = rendered
                    .replace(&test_path_display("/tmp/hooks.json"), "/tmp/hooks.json")
                    .replace(&test_path_display("/tmp/h.json"), "/tmp/h.json");
                format!("{normalized:width$}", width = area.width as usize)
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    fn render_buffer(view: &HooksBrowserView, width: u16) -> Buffer {
        let height = view.desired_height(width);
        let area = Rect::new(0, 0, width, height);
        let mut buf = Buffer::empty(area);
        view.render(area, &mut buf);
        buf
    }

    #[allow(clippy::too_many_arguments)]
    fn hook(
        key: &str,
        event_name: HookEventName,
        source: HookSource,
        plugin_id: Option<&str>,
        command: &str,
        enabled: bool,
        is_managed: bool,
        display_order: i64,
    ) -> HookMetadata {
        let current_hash = "sha256:current".to_string();
        HookMetadata {
            key: key.to_string(),
            event_name,
            handler_type: HookHandlerType::Command,
            is_managed,
            matcher: Some("Bash".to_string()),
            command: Some(command.to_string()),
            timeout_sec: 30,
            status_message: None,
            source_path: test_path_buf("/tmp/hooks.json").abs(),
            source,
            plugin_id: plugin_id.map(str::to_string),
            display_order,
            enabled,
            current_hash,
            trust_status: if is_managed {
                HookTrustStatus::Managed
            } else {
                HookTrustStatus::Trusted
            },
        }
    }

    fn view() -> HooksBrowserView {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        HooksBrowserView::new(
            vec![
                hook(
                    "plugin:superpowers",
                    HookEventName::PreToolUse,
                    HookSource::Plugin,
                    Some("superpowers@openai-curated"),
                    "${CODEX_PLUGIN_ROOT}/hooks/pre-tool-use-check.sh",
                    /*enabled*/ true,
                    /*is_managed*/ false,
                    /*display_order*/ 0,
                ),
                hook(
                    "path:user-config",
                    HookEventName::PreToolUse,
                    HookSource::User,
                    /*plugin_id*/ None,
                    "~/bin/check-shell-with-a-command-that-is-way-too-long-for-the-summary-column.sh",
                    /*enabled*/ false,
                    /*is_managed*/ false,
                    /*display_order*/ 1,
                ),
                hook(
                    "path:managed",
                    HookEventName::PermissionRequest,
                    HookSource::System,
                    /*plugin_id*/ None,
                    "/enterprise/hooks/permission-check.sh",
                    /*enabled*/ true,
                    /*is_managed*/ true,
                    /*display_order*/ 2,
                ),
            ],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        )
    }

    #[test]
    fn renders_event_browser() {
        let view = view();
        assert_snapshot!("hooks_browser_events", render_lines(&view, /*width*/ 112));
    }

    #[test]
    fn selected_event_rows_use_the_shared_accent_style() {
        let view = view();
        let buf = render_buffer(&view, /*width*/ 112);
        let expected = accent_style();

        let selected_cell = buf
            .content
            .iter()
            .find(|cell| {
                let style = cell.style();
                cell.symbol() == "P"
                    && style.fg == expected.fg
                    && style.add_modifier.contains(Modifier::BOLD)
            })
            .expect("selected event row should use the shared accent style");

        assert_eq!(selected_cell.style().fg, expected.fg);
    }

    #[test]
    fn renders_event_browser_with_review_column_when_needed() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let mut untrusted_hook = hook(
            "path:untrusted",
            HookEventName::PreToolUse,
            HookSource::User,
            /*plugin_id*/ None,
            "/tmp/pre-tool-use-check.sh",
            /*enabled*/ false,
            /*is_managed*/ false,
            /*display_order*/ 0,
        );
        untrusted_hook.trust_status = HookTrustStatus::Untrusted;
        let view = HooksBrowserView::new(
            vec![untrusted_hook],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );

        assert_snapshot!(
            "hooks_browser_events_with_review_column",
            render_lines(&view, /*width*/ 112)
        );
        assert_eq!(
            view.event_table_lines()[1].spans[3].style.fg,
            Some(Color::Cyan)
        );
        assert!(
            view.event_table_lines()[1].spans[3]
                .style
                .add_modifier
                .contains(ratatui::style::Modifier::BOLD)
        );
    }

    #[test]
    fn renders_event_browser_with_issues() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let view = HooksBrowserView::new(
            Vec::new(),
            vec!["skipped invalid matcher for PreToolUse".to_string()],
            vec![HookErrorInfo {
                path: test_path_buf("/tmp/hooks.json"),
                message: "failed to parse hooks config".to_string(),
            }],
            AppEventSender::new(tx_raw),
        );

        assert_snapshot!(
            "hooks_browser_events_with_issues",
            render_lines(&view, /*width*/ 112)
        );
    }

    #[test]
    fn renders_handler_browser_with_details() {
        let mut view = view();
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        assert_snapshot!("hooks_browser_handlers", render_lines(&view, /*width*/ 112));
    }

    #[test]
    fn renders_untrusted_enabled_handler_as_inactive() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let mut untrusted_hook = hook(
            "path:untrusted",
            HookEventName::PreToolUse,
            HookSource::User,
            /*plugin_id*/ None,
            "~/bin/untrusted.sh",
            /*enabled*/ true,
            /*is_managed*/ false,
            /*display_order*/ 0,
        );
        untrusted_hook.trust_status = HookTrustStatus::Untrusted;
        let mut view = HooksBrowserView::new(
            vec![untrusted_hook],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));

        assert_snapshot!(
            "hooks_browser_untrusted_enabled_handler",
            render_lines(&view, /*width*/ 112)
        );
    }

    #[test]
    fn review_needed_handler_rows_use_warning_color() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let mut untrusted_hook = hook(
            "path:untrusted",
            HookEventName::PreToolUse,
            HookSource::User,
            /*plugin_id*/ None,
            "~/bin/untrusted.sh",
            /*enabled*/ false,
            /*is_managed*/ false,
            /*display_order*/ 1,
        );
        untrusted_hook.trust_status = HookTrustStatus::Untrusted;
        let mut view = HooksBrowserView::new(
            vec![
                hook(
                    "path:trusted",
                    HookEventName::PreToolUse,
                    HookSource::User,
                    /*plugin_id*/ None,
                    "~/bin/trusted.sh",
                    /*enabled*/ true,
                    /*is_managed*/ false,
                    /*display_order*/ 0,
                ),
                untrusted_hook,
            ],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));

        assert_eq!(
            view.handler_row_lines(HookEventName::PreToolUse, /*width*/ 112)[1]
                .style
                .fg,
            Some(Color::Yellow)
        );
    }

    #[test]
    fn review_needed_handler_header_uses_warning_color() {
        assert_eq!(
            HooksBrowserView::handler_header_lines(
                HookEventName::PreToolUse,
                /*review_needed_count*/ 1,
            )[1]
            .spans[0]
                .style
                .fg,
            Some(Color::Yellow)
        );
    }

    #[test]
    fn renders_managed_handler_without_toggle_hint() {
        let mut view = view();
        view.handle_key_event(KeyEvent::from(KeyCode::Down));
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        assert_snapshot!(
            "hooks_browser_managed_handler",
            render_lines(&view, /*width*/ 112)
        );
    }

    #[test]
    fn renders_selected_managed_handler() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let mut view = HooksBrowserView::new(
            vec![
                hook(
                    "path:managed-1",
                    HookEventName::PreToolUse,
                    HookSource::System,
                    /*plugin_id*/ None,
                    "/enterprise/hooks/pre-tool-use-1.sh",
                    /*enabled*/ true,
                    /*is_managed*/ true,
                    /*display_order*/ 0,
                ),
                hook(
                    "path:managed-2",
                    HookEventName::PreToolUse,
                    HookSource::System,
                    /*plugin_id*/ None,
                    "/enterprise/hooks/pre-tool-use-2.sh",
                    /*enabled*/ true,
                    /*is_managed*/ true,
                    /*display_order*/ 1,
                ),
            ],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        view.handle_key_event(KeyEvent::from(KeyCode::Down));
        assert_snapshot!(
            "hooks_browser_selected_managed_handler",
            render_lines(&view, /*width*/ 112)
        );
    }

    #[test]
    fn renders_scrolled_handler_window() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let hooks = (0..=MAX_POPUP_ROWS)
            .map(|idx| {
                hook(
                    &format!("path:hook-{idx}"),
                    HookEventName::PreToolUse,
                    HookSource::User,
                    /*plugin_id*/ None,
                    &format!("/tmp/hook-{idx}.sh"),
                    /*enabled*/ true,
                    /*is_managed*/ false,
                    idx as i64,
                )
            })
            .collect();
        let mut view =
            HooksBrowserView::new(hooks, Vec::new(), Vec::new(), AppEventSender::new(tx_raw));
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        for _ in 0..MAX_POPUP_ROWS {
            view.handle_key_event(KeyEvent::from(KeyCode::Down));
        }
        assert_snapshot!(
            "hooks_browser_scrolled_handlers",
            render_lines(&view, /*width*/ 112)
        );
    }

    #[test]
    fn renders_command_details_with_three_line_cap() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let mut capped_command_hook = hook(
            "path:long-command",
            HookEventName::PreToolUse,
            HookSource::User,
            /*plugin_id*/ None,
            "one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty",
            /*enabled*/ true,
            /*is_managed*/ false,
            /*display_order*/ 0,
        );
        capped_command_hook.source_path = test_path_buf("/tmp/h.json").abs();
        let mut view = HooksBrowserView::new(
            vec![capped_command_hook],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        assert_snapshot!(
            "hooks_browser_capped_command_details",
            render_lines(&view, /*width*/ 44)
        );
    }

    #[test]
    fn renders_empty_handler_browser_message() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let mut view = HooksBrowserView::new(
            Vec::new(),
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );
        view.handle_key_event(KeyEvent::from(KeyCode::Down));
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        assert_snapshot!(
            "hooks_browser_empty_handlers",
            render_lines(&view, /*width*/ 112)
        );
    }

    #[test]
    fn managed_hooks_count_as_active() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let view = HooksBrowserView::new(
            vec![hook(
                "path:managed",
                HookEventName::PreToolUse,
                HookSource::System,
                /*plugin_id*/ None,
                "/enterprise/hooks/pre-tool-use-check.sh",
                /*enabled*/ true,
                /*is_managed*/ true,
                /*display_order*/ 0,
            )],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );

        let rows = view.event_rows();
        let pre_tool_use = rows
            .into_iter()
            .find(|row| row.event_name == HookEventName::PreToolUse)
            .expect("pre tool use row");

        assert_eq!(pre_tool_use.installed, 1);
        assert_eq!(pre_tool_use.active, 1);
    }

    #[test]
    fn review_needed_hooks_are_not_active() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let mut untrusted_hook = hook(
            "path:untrusted",
            HookEventName::PreToolUse,
            HookSource::User,
            /*plugin_id*/ None,
            "/tmp/pre-tool-use-check.sh",
            /*enabled*/ true,
            /*is_managed*/ false,
            /*display_order*/ 0,
        );
        untrusted_hook.trust_status = HookTrustStatus::Untrusted;
        let view = HooksBrowserView::new(
            vec![untrusted_hook],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );

        let rows = view.event_rows();
        let pre_tool_use = rows
            .into_iter()
            .find(|row| row.event_name == HookEventName::PreToolUse)
            .expect("pre tool use row");

        assert_eq!(pre_tool_use.installed, 1);
        assert_eq!(pre_tool_use.active, 0);
        assert_eq!(pre_tool_use.needs_review, 1);
    }

    #[test]
    fn review_needed_event_is_selected_by_default() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let mut untrusted_hook = hook(
            "path:untrusted",
            HookEventName::PermissionRequest,
            HookSource::User,
            /*plugin_id*/ None,
            "/tmp/permission-request-check.sh",
            /*enabled*/ false,
            /*is_managed*/ false,
            /*display_order*/ 0,
        );
        untrusted_hook.trust_status = HookTrustStatus::Untrusted;
        let view = HooksBrowserView::new(
            vec![untrusted_hook],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );

        assert_eq!(
            view.selected_event(),
            Some(HookEventName::PermissionRequest)
        );
    }

    #[test]
    fn renders_review_needed_handler() {
        let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
        let mut untrusted_hook = hook(
            "path:untrusted",
            HookEventName::PreToolUse,
            HookSource::User,
            /*plugin_id*/ None,
            "/tmp/pre-tool-use-check.sh",
            /*enabled*/ false,
            /*is_managed*/ false,
            /*display_order*/ 0,
        );
        untrusted_hook.trust_status = HookTrustStatus::Untrusted;
        let mut view = HooksBrowserView::new(
            vec![untrusted_hook],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));

        assert_snapshot!(
            "hooks_browser_review_needed_handler",
            render_lines(&view, /*width*/ 112)
        );
    }

    fn assert_unmanaged_toggle_key(key_code: KeyCode) {
        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
        let mut view = HooksBrowserView::new(
            vec![hook(
                "plugin:superpowers",
                HookEventName::PreToolUse,
                HookSource::Plugin,
                Some("superpowers@openai-curated"),
                "hooks/pre-tool-use-check.sh",
                /*enabled*/ true,
                /*is_managed*/ false,
                /*display_order*/ 0,
            )],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        view.handle_key_event(KeyEvent::from(key_code));

        match rx.try_recv().expect("toggle event") {
            AppEvent::SetHookEnabled { key, enabled } => {
                assert_eq!(key, "plugin:superpowers");
                assert!(!enabled);
            }
            other => panic!("expected hook toggle event, got {other:?}"),
        }
    }

    #[test]
    fn toggle_keys_toggle_unmanaged_handler() {
        for key_code in [KeyCode::Char(' '), KeyCode::Enter] {
            assert_unmanaged_toggle_key(key_code);
        }
    }

    #[test]
    fn space_does_not_toggle_managed_handler() {
        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
        let mut view = HooksBrowserView::new(
            vec![hook(
                "path:managed",
                HookEventName::PreToolUse,
                HookSource::System,
                /*plugin_id*/ None,
                "/enterprise/hooks/pre-tool-use-check.sh",
                /*enabled*/ true,
                /*is_managed*/ true,
                /*display_order*/ 0,
            )],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));

        assert!(rx.try_recv().is_err());
    }

    #[test]
    fn trust_key_trusts_review_needed_handler_without_changing_enablement() {
        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
        let mut untrusted_hook = hook(
            "path:untrusted",
            HookEventName::PreToolUse,
            HookSource::User,
            /*plugin_id*/ None,
            "/tmp/pre-tool-use-check.sh",
            /*enabled*/ false,
            /*is_managed*/ false,
            /*display_order*/ 0,
        );
        untrusted_hook.trust_status = HookTrustStatus::Untrusted;
        let current_hash = untrusted_hook.current_hash.clone();
        let mut view = HooksBrowserView::new(
            vec![untrusted_hook],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        view.handle_key_event(KeyEvent::from(KeyCode::Char('t')));

        match rx.try_recv().expect("trust event") {
            AppEvent::TrustHook {
                key,
                current_hash: hash_to_trust,
            } => {
                assert_eq!(key, "path:untrusted");
                assert_eq!(hash_to_trust, current_hash);
            }
            other => panic!("expected hook trust event, got {other:?}"),
        }
    }

    #[test]
    fn trust_key_preserves_disabled_modified_handler() {
        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
        let mut modified_hook = hook(
            "path:modified",
            HookEventName::PreToolUse,
            HookSource::User,
            /*plugin_id*/ None,
            "/tmp/pre-tool-use-check.sh",
            /*enabled*/ false,
            /*is_managed*/ false,
            /*display_order*/ 0,
        );
        modified_hook.trust_status = HookTrustStatus::Modified;
        let current_hash = modified_hook.current_hash.clone();
        let mut view = HooksBrowserView::new(
            vec![modified_hook],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        view.handle_key_event(KeyEvent::from(KeyCode::Char('t')));

        let hook = view.entry.hooks.first().expect("trusted hook");
        assert!(!hook.enabled);
        assert_eq!(hook.trust_status, HookTrustStatus::Trusted);
        match rx.try_recv().expect("trust event") {
            AppEvent::TrustHook {
                key,
                current_hash: hash_to_trust,
            } => {
                assert_eq!(key, "path:modified");
                assert_eq!(hash_to_trust, current_hash);
            }
            other => panic!("expected hook trust event, got {other:?}"),
        }
    }

    #[test]
    fn trust_key_on_event_page_trusts_all_review_needed_hooks() {
        let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
        let mut untrusted_hook = hook(
            "path:untrusted",
            HookEventName::PreToolUse,
            HookSource::User,
            /*plugin_id*/ None,
            "/tmp/pre-tool-use-check.sh",
            /*enabled*/ false,
            /*is_managed*/ false,
            /*display_order*/ 0,
        );
        untrusted_hook.trust_status = HookTrustStatus::Untrusted;
        let mut modified_hook = hook(
            "path:modified",
            HookEventName::Stop,
            HookSource::User,
            /*plugin_id*/ None,
            "/tmp/stop-check.sh",
            /*enabled*/ false,
            /*is_managed*/ false,
            /*display_order*/ 1,
        );
        modified_hook.trust_status = HookTrustStatus::Modified;
        let mut view = HooksBrowserView::new(
            vec![
                untrusted_hook,
                modified_hook,
                hook(
                    "path:trusted",
                    HookEventName::PreToolUse,
                    HookSource::User,
                    /*plugin_id*/ None,
                    "/tmp/trusted.sh",
                    /*enabled*/ true,
                    /*is_managed*/ false,
                    /*display_order*/ 2,
                ),
            ],
            Vec::new(),
            Vec::new(),
            AppEventSender::new(tx_raw),
        );

        view.handle_key_event(KeyEvent::from(KeyCode::Char('t')));

        assert_eq!(
            view.entry
                .hooks
                .iter()
                .map(|hook| hook.trust_status)
                .collect::<Vec<_>>(),
            vec![
                HookTrustStatus::Trusted,
                HookTrustStatus::Trusted,
                HookTrustStatus::Trusted,
            ]
        );
        match rx.try_recv().expect("trust event") {
            AppEvent::TrustHooks { updates } => assert_eq!(
                updates,
                vec![
                    HookTrustUpdate {
                        key: "path:untrusted".to_string(),
                        current_hash: "sha256:current".to_string(),
                    },
                    HookTrustUpdate {
                        key: "path:modified".to_string(),
                        current_hash: "sha256:current".to_string(),
                    },
                ]
            ),
            other => panic!("expected hook trust event, got {other:?}"),
        }
        assert!(rx.try_recv().is_err());
    }

    #[test]
    fn escape_returns_to_the_selected_event() {
        let mut view = view();
        view.handle_key_event(KeyEvent::from(KeyCode::Down));
        view.handle_key_event(KeyEvent::from(KeyCode::Enter));
        view.handle_key_event(KeyEvent::from(KeyCode::Esc));

        assert_eq!(view.page, HooksBrowserPage::Events);
        assert_eq!(
            view.selected_event(),
            Some(HookEventName::PermissionRequest)
        );
    }

    #[test]
    fn esc_routes_through_the_view() {
        assert!(view().prefer_esc_to_handle_key_event());
    }
}
