use crate::utils::{MouseInfo, Rectangle, Size};
use uuid::Uuid;
use winit::keyboard::{Key, NamedKey};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Options for popup action style.
pub enum PopupActionStyle {
    /// Primary option.
    Primary,
    /// Secondary option.
    Secondary,
    /// Danger option.
    Danger,
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// PopupAction data.
pub struct PopupAction {
    /// Stable object identifier.
    pub id: String,
    /// Label value.
    pub label: String,
    /// Style value.
    pub style: PopupActionStyle,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Options for popup size.
pub enum PopupSize {
    /// Small option.
    Small,
    /// Medium option.
    Medium,
    /// Large option.
    Large,
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// PopupConfig data.
pub struct PopupConfig {
    /// Stable object identifier.
    pub id: String,
    /// Window title.
    pub title: String,
    /// Human-readable error message.
    pub message: String,
    /// Actions value.
    pub actions: Vec<PopupAction>,
    /// Size value.
    pub size: PopupSize,
    /// Dismiss on escape value.
    pub dismiss_on_escape: bool,
    /// Dismiss on backdrop click value.
    pub dismiss_on_backdrop_click: bool,
    /// Auto dismiss ms value.
    pub auto_dismiss_ms: Option<u64>,
    /// Consume opening click value.
    pub consume_opening_click: bool,
    /// Click anywhere action id value.
    pub click_anywhere_action_id: Option<String>,
    /// Block input behind popup value.
    pub block_input_behind_popup: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// Options for popup dismiss reason.
pub enum PopupDismissReason {
    /// Escape option.
    Escape,
    /// Backdrop click option.
    BackdropClick,
    /// Replaced option.
    Replaced,
    /// Programmatic option.
    Programmatic,
}

#[derive(Debug, Clone, PartialEq, Eq)]
/// Options for popup event.
pub enum PopupEvent {
    /// Opened option.
    Opened {
        /// Item value.
        popup_id: String,
    },
    /// Action selected option.
    ActionSelected {
        /// Item value.
        popup_id: String,
        /// Item value.
        action_id: String,
    },
    /// Dismissed option.
    Dismissed {
        /// Item value.
        popup_id: String,
        /// Item value.
        reason: PopupDismissReason,
    },
    /// Auto dismissed option.
    AutoDismissed {
        /// Item value.
        popup_id: String,
    },
}

#[derive(Debug, Clone)]
pub(crate) struct PopupLayout {
    pub(crate) viewport: Rectangle,
    pub(crate) panel: Rectangle,
    pub(crate) title_rect: Rectangle,
    pub(crate) message_rect: Rectangle,
    pub(crate) action_rects: Vec<Rectangle>,
}

#[derive(Debug, Clone)]
pub(crate) struct ActivePopup {
    pub(crate) config: PopupConfig,
    elapsed_ms: u64,
    opened_frame: u64,
    awaiting_opening_release: bool,
    pub(crate) custom_panel_rect: Option<Rectangle>,
    pub(crate) custom_object_ids: Vec<Uuid>,
    pub(crate) hovered_action: Option<usize>,
    pub(crate) pressed_action: Option<usize>,
}

pub(crate) struct PopupRuntimeState {
    active: Option<ActivePopup>,
    events: Vec<PopupEvent>,
    pending_object_cleanup: Vec<Uuid>,
    frame_counter: u64,
    prev_lmb_down: bool,
}

impl PopupRuntimeState {
    pub(crate) fn new() -> Self {
        Self {
            active: None,
            events: Vec::new(),
            pending_object_cleanup: Vec::new(),
            frame_counter: 0,
            prev_lmb_down: false,
        }
    }

    pub(crate) fn show_popup(&mut self, config: PopupConfig) {
        let config = sanitize_config(config);
        if let Some(existing) = self.active.take() {
            self.pending_object_cleanup
                .extend(existing.custom_object_ids.iter().copied());
            self.events.push(PopupEvent::Dismissed {
                popup_id: existing.config.id,
                reason: PopupDismissReason::Replaced,
            });
        }
        self.active = Some(ActivePopup {
            awaiting_opening_release: config.consume_opening_click,
            config,
            elapsed_ms: 0,
            opened_frame: self.frame_counter,
            custom_panel_rect: None,
            custom_object_ids: Vec::new(),
            hovered_action: None,
            pressed_action: None,
        });
        let popup_id = self.active.as_ref().map(|p| p.config.id.clone()).unwrap();
        self.events.push(PopupEvent::Opened { popup_id });
    }

    pub(crate) fn show_popup_with_objects(
        &mut self,
        config: PopupConfig,
        panel_rect: Rectangle,
        object_ids: Vec<Uuid>,
    ) {
        let config = sanitize_config(config);
        if let Some(existing) = self.active.take() {
            self.pending_object_cleanup
                .extend(existing.custom_object_ids.iter().copied());
            self.events.push(PopupEvent::Dismissed {
                popup_id: existing.config.id,
                reason: PopupDismissReason::Replaced,
            });
        }
        self.active = Some(ActivePopup {
            awaiting_opening_release: config.consume_opening_click,
            config,
            elapsed_ms: 0,
            opened_frame: self.frame_counter,
            custom_panel_rect: Some(panel_rect),
            custom_object_ids: object_ids,
            hovered_action: None,
            pressed_action: None,
        });
        let popup_id = self.active.as_ref().map(|p| p.config.id.clone()).unwrap();
        self.events.push(PopupEvent::Opened { popup_id });
    }

    pub(crate) fn close_popup(&mut self, popup_id: &str) -> bool {
        let should_close = self
            .active
            .as_ref()
            .map(|p| p.config.id == popup_id)
            .unwrap_or(false);
        if !should_close {
            return false;
        }
        let closed = self.active.take().unwrap();
        self.pending_object_cleanup
            .extend(closed.custom_object_ids.iter().copied());
        self.events.push(PopupEvent::Dismissed {
            popup_id: closed.config.id,
            reason: PopupDismissReason::Programmatic,
        });
        true
    }

    pub(crate) fn is_open(&self) -> bool {
        self.active.is_some()
    }

    pub(crate) fn blocks_input_behind_popup(&self) -> bool {
        self.active
            .as_ref()
            .map(|p| p.config.block_input_behind_popup)
            .unwrap_or(false)
    }

    pub(crate) fn active(&self) -> Option<&ActivePopup> {
        self.active.as_ref()
    }

    pub(crate) fn drain_popup_object_cleanup(&mut self) -> Vec<Uuid> {
        std::mem::take(&mut self.pending_object_cleanup)
    }

    pub(crate) fn drain_events(&mut self) -> Vec<PopupEvent> {
        std::mem::take(&mut self.events)
    }

    pub(crate) fn update(
        &mut self,
        mouse_info: Option<MouseInfo>,
        key: &Option<Key>,
        delta_time_s: f32,
        window_size: Size,
    ) {
        let frame = self.frame_counter;
        self.frame_counter = self.frame_counter.wrapping_add(1);
        let lmb_down = mouse_info.map(|m| m.is_lmb_clicked).unwrap_or(false);
        let lmb_pressed = lmb_down && !self.prev_lmb_down;
        let lmb_released = !lmb_down && self.prev_lmb_down;
        self.prev_lmb_down = lmb_down;
        let mouse_pos = mouse_info.map(|m| m.mouse_pos).unwrap_or_default();

        let (
            layout,
            clicked_action_id,
            click_anywhere_action_id,
            dismiss_on_backdrop_click,
            click_interactions_allowed,
            dismiss_on_escape,
        ) = {
            let Some(active) = self.active.as_mut() else {
                return;
            };
            let layout = popup_layout_for_active(active, window_size);
            let hovered_idx = layout
                .action_rects
                .iter()
                .position(|rect| rect.contains(mouse_pos));
            active.hovered_action = hovered_idx;
            if lmb_down {
                active.pressed_action = hovered_idx;
            } else {
                active.pressed_action = None;
            }

            if active.awaiting_opening_release && lmb_released {
                active.awaiting_opening_release = false;
            }

            let click_interactions_allowed =
                frame != active.opened_frame && !active.awaiting_opening_release;
            let clicked_action_id = hovered_idx
                .and_then(|idx| active.config.actions.get(idx))
                .map(|action| action.id.clone());
            (
                layout,
                clicked_action_id,
                active.config.click_anywhere_action_id.clone(),
                active.config.dismiss_on_backdrop_click,
                click_interactions_allowed,
                active.config.dismiss_on_escape,
            )
        };

        if dismiss_on_escape && is_escape_pressed(key) {
            let closed = self.active.take().unwrap();
            self.pending_object_cleanup
                .extend(closed.custom_object_ids.iter().copied());
            self.events.push(PopupEvent::Dismissed {
                popup_id: closed.config.id,
                reason: PopupDismissReason::Escape,
            });
            return;
        }

        if click_interactions_allowed && lmb_pressed {
            if let Some(action_id) = clicked_action_id {
                let closed = self.active.take().unwrap();
                self.pending_object_cleanup
                    .extend(closed.custom_object_ids.iter().copied());
                self.events.push(PopupEvent::ActionSelected {
                    popup_id: closed.config.id,
                    action_id,
                });
                return;
            }

            if let Some(action_id) = click_anywhere_action_id {
                if layout.viewport.contains(mouse_pos) {
                    let closed = self.active.take().unwrap();
                    self.pending_object_cleanup
                        .extend(closed.custom_object_ids.iter().copied());
                    self.events.push(PopupEvent::ActionSelected {
                        popup_id: closed.config.id,
                        action_id,
                    });
                    return;
                }
            }

            if dismiss_on_backdrop_click && !layout.panel.contains(mouse_pos) {
                let closed = self.active.take().unwrap();
                self.pending_object_cleanup
                    .extend(closed.custom_object_ids.iter().copied());
                self.events.push(PopupEvent::Dismissed {
                    popup_id: closed.config.id,
                    reason: PopupDismissReason::BackdropClick,
                });
                return;
            }
        }

        let timeout_ms = self
            .active
            .as_ref()
            .and_then(|popup| popup.config.auto_dismiss_ms);
        if let Some(timeout_ms) = timeout_ms {
            if timeout_ms == 0 {
                let closed = self.active.take().unwrap();
                self.pending_object_cleanup
                    .extend(closed.custom_object_ids.iter().copied());
                self.events.push(PopupEvent::AutoDismissed {
                    popup_id: closed.config.id,
                });
                return;
            }
            let elapsed_ms = (delta_time_s.max(0.0) * 1000.0) as u64;
            if let Some(active) = self.active.as_mut() {
                active.elapsed_ms = active.elapsed_ms.saturating_add(elapsed_ms);
            }
            let timeout_reached = self
                .active
                .as_ref()
                .map(|popup| popup.elapsed_ms >= timeout_ms)
                .unwrap_or(false);
            if timeout_reached {
                let closed = self.active.take().unwrap();
                self.pending_object_cleanup
                    .extend(closed.custom_object_ids.iter().copied());
                self.events.push(PopupEvent::AutoDismissed {
                    popup_id: closed.config.id,
                });
            }
        }
    }
}

fn sanitize_config(mut config: PopupConfig) -> PopupConfig {
    if config.actions.len() > 2 {
        config.actions.truncate(2);
    }
    if config.actions.is_empty() {
        config.actions.push(PopupAction {
            id: "ok".to_string(),
            label: "OK".to_string(),
            style: PopupActionStyle::Primary,
        });
    }
    config
}

fn is_escape_pressed(key: &Option<Key>) -> bool {
    matches!(key, Some(Key::Named(NamedKey::Escape)))
}

fn estimated_message_line_count(message: &str, content_width: f32) -> usize {
    let chars_per_line = ((content_width / 9.0).floor() as usize).max(16);
    let mut total = 0usize;
    for line in message.lines() {
        let len = line.chars().count();
        total += len.div_ceil(chars_per_line).max(1);
    }
    total.clamp(1, 12)
}

pub(crate) fn popup_layout_for(config: &PopupConfig, window_size: Size) -> PopupLayout {
    let viewport = Rectangle::new(0.0, 0.0, window_size.width, window_size.height);
    let margin = 24.0f32;
    let max_w = (window_size.width - margin * 2.0).max(220.0);
    let preset_w: f32 = match config.size {
        PopupSize::Small => 360.0,
        PopupSize::Medium => 520.0,
        PopupSize::Large => 720.0,
    };
    let panel_w = preset_w.min(max_w);
    let panel_padding = 20.0f32;
    let title_h = 34.0f32;
    let body_line_h = 22.0f32;
    let body_content_w = (panel_w - panel_padding * 2.0).max(160.0);
    let body_lines = estimated_message_line_count(&config.message, body_content_w);
    let mut body_h = (body_lines as f32) * body_line_h + 8.0;
    let actions_h = 44.0f32;

    let max_panel_h = (window_size.height - margin * 2.0).max(180.0);
    let mut panel_h = panel_padding + title_h + 12.0 + body_h + 20.0 + actions_h + panel_padding;
    if panel_h > max_panel_h {
        let overflow = panel_h - max_panel_h;
        body_h = (body_h - overflow).max(body_line_h * 2.0);
        panel_h = panel_padding + title_h + 12.0 + body_h + 20.0 + actions_h + panel_padding;
    }

    let panel = Rectangle::new(
        ((window_size.width - panel_w) * 0.5).floor(),
        ((window_size.height - panel_h) * 0.5).floor(),
        panel_w,
        panel_h,
    );

    let title_rect = Rectangle::new(
        panel.x + panel_padding,
        panel.y + panel_padding,
        panel.width - panel_padding * 2.0,
        title_h,
    );
    let message_rect = Rectangle::new(
        panel.x + panel_padding,
        title_rect.y + title_rect.height + 12.0,
        panel.width - panel_padding * 2.0,
        body_h,
    );

    let action_count = config.actions.len().clamp(1, 2);
    let action_gap = 12.0f32;
    let actions_y = panel.y + panel.height - panel_padding - actions_h;
    let action_rects = if action_count == 1 {
        let w = (panel.width - panel_padding * 2.0).min(180.0);
        vec![Rectangle::new(
            panel.x + (panel.width - w) * 0.5,
            actions_y,
            w,
            actions_h,
        )]
    } else {
        let w = ((panel.width - panel_padding * 2.0 - action_gap) * 0.5).max(80.0);
        vec![
            Rectangle::new(panel.x + panel_padding, actions_y, w, actions_h),
            Rectangle::new(
                panel.x + panel_padding + w + action_gap,
                actions_y,
                w,
                actions_h,
            ),
        ]
    };

    PopupLayout {
        viewport,
        panel,
        title_rect,
        message_rect,
        action_rects,
    }
}

pub(crate) fn popup_layout_for_active(active: &ActivePopup, window_size: Size) -> PopupLayout {
    if let Some(panel) = active.custom_panel_rect {
        return PopupLayout {
            viewport: Rectangle::new(0.0, 0.0, window_size.width, window_size.height),
            panel,
            title_rect: panel,
            message_rect: panel,
            action_rects: Vec::new(),
        };
    }
    popup_layout_for(&active.config, window_size)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Position;

    fn base_config() -> PopupConfig {
        PopupConfig {
            id: "p1".to_string(),
            title: "Title".to_string(),
            message: "Message".to_string(),
            actions: vec![
                PopupAction {
                    id: "ok".to_string(),
                    label: "OK".to_string(),
                    style: PopupActionStyle::Primary,
                },
                PopupAction {
                    id: "cancel".to_string(),
                    label: "Cancel".to_string(),
                    style: PopupActionStyle::Secondary,
                },
            ],
            size: PopupSize::Medium,
            dismiss_on_escape: true,
            dismiss_on_backdrop_click: true,
            auto_dismiss_ms: None,
            consume_opening_click: false,
            click_anywhere_action_id: None,
            block_input_behind_popup: true,
        }
    }

    fn mouse(x: f32, y: f32, down: bool) -> MouseInfo {
        MouseInfo {
            is_lmb_clicked: down,
            is_rmb_clicked: false,
            is_mmb_clicked: false,
            mouse_pos: Position { x, y },
            scroll_dx: 0.0,
            scroll_dy: 0.0,
        }
    }

    #[test]
    fn replacing_popup_emits_replaced_event() {
        let mut state = PopupRuntimeState::new();
        state.show_popup(base_config());
        assert_eq!(
            state.drain_events(),
            vec![PopupEvent::Opened {
                popup_id: "p1".to_string()
            }]
        );
        let mut next = base_config();
        next.id = "p2".to_string();
        state.show_popup(next);

        let events = state.drain_events();
        assert_eq!(
            events,
            vec![
                PopupEvent::Dismissed {
                    popup_id: "p1".to_string(),
                    reason: PopupDismissReason::Replaced
                },
                PopupEvent::Opened {
                    popup_id: "p2".to_string()
                }
            ]
        );
        assert!(state.is_open());
        assert_eq!(state.active().unwrap().config.id, "p2");
    }

    #[test]
    fn close_popup_is_programmatic_and_id_scoped() {
        let mut state = PopupRuntimeState::new();
        state.show_popup(base_config());
        let _ = state.drain_events();
        assert!(!state.close_popup("wrong"));
        assert!(state.close_popup("p1"));
        assert!(!state.is_open());
        assert_eq!(
            state.drain_events(),
            vec![PopupEvent::Dismissed {
                popup_id: "p1".to_string(),
                reason: PopupDismissReason::Programmatic
            }]
        );
    }

    #[test]
    fn action_click_emits_action_selected() {
        let mut state = PopupRuntimeState::new();
        let cfg = base_config();
        let layout = popup_layout_for(
            &cfg,
            Size {
                width: 1280.0,
                height: 720.0,
            },
        );
        let button = layout.action_rects[0];

        state.show_popup(cfg);
        let _ = state.drain_events();
        state.update(
            Some(mouse(button.x + 4.0, button.y + 4.0, false)),
            &None,
            0.016,
            Size {
                width: 1280.0,
                height: 720.0,
            },
        );
        state.update(
            Some(mouse(button.x + 4.0, button.y + 4.0, true)),
            &None,
            0.016,
            Size {
                width: 1280.0,
                height: 720.0,
            },
        );

        assert!(!state.is_open());
        assert_eq!(
            state.drain_events(),
            vec![PopupEvent::ActionSelected {
                popup_id: "p1".to_string(),
                action_id: "ok".to_string()
            }]
        );
    }

    #[test]
    fn escape_dismiss_respects_flag() {
        let mut state = PopupRuntimeState::new();
        let mut cfg = base_config();
        cfg.dismiss_on_escape = false;
        state.show_popup(cfg);
        let _ = state.drain_events();
        state.update(
            None,
            &Some(Key::Named(NamedKey::Escape)),
            0.016,
            Size {
                width: 800.0,
                height: 600.0,
            },
        );
        assert!(state.is_open());
        assert!(state.drain_events().is_empty());

        let mut cfg2 = base_config();
        cfg2.id = "p2".to_string();
        cfg2.dismiss_on_escape = true;
        state.show_popup(cfg2);
        let _ = state.drain_events();
        state.update(
            None,
            &Some(Key::Named(NamedKey::Escape)),
            0.016,
            Size {
                width: 800.0,
                height: 600.0,
            },
        );
        assert_eq!(
            state.drain_events(),
            vec![PopupEvent::Dismissed {
                popup_id: "p2".to_string(),
                reason: PopupDismissReason::Escape
            }]
        );
    }

    #[test]
    fn backdrop_click_dismiss_respects_flag() {
        let mut state = PopupRuntimeState::new();
        let mut cfg = base_config();
        cfg.dismiss_on_backdrop_click = false;
        state.show_popup(cfg);
        let _ = state.drain_events();
        state.update(
            Some(mouse(2.0, 2.0, true)),
            &None,
            0.016,
            Size {
                width: 1000.0,
                height: 700.0,
            },
        );
        assert!(state.is_open());
        assert!(state.drain_events().is_empty());

        let mut cfg2 = base_config();
        cfg2.id = "p2".to_string();
        cfg2.dismiss_on_backdrop_click = true;
        state.show_popup(cfg2);
        let _ = state.drain_events();
        state.update(
            Some(mouse(2.0, 2.0, false)),
            &None,
            0.016,
            Size {
                width: 1000.0,
                height: 700.0,
            },
        );
        state.update(
            Some(mouse(2.0, 2.0, true)),
            &None,
            0.016,
            Size {
                width: 1000.0,
                height: 700.0,
            },
        );
        assert_eq!(
            state.drain_events(),
            vec![PopupEvent::Dismissed {
                popup_id: "p2".to_string(),
                reason: PopupDismissReason::BackdropClick
            }]
        );
    }

    #[test]
    fn auto_dismiss_fires_when_timeout_reached() {
        let mut state = PopupRuntimeState::new();
        let mut cfg = base_config();
        cfg.auto_dismiss_ms = Some(20);
        state.show_popup(cfg);
        let _ = state.drain_events();
        state.update(
            None,
            &None,
            0.010,
            Size {
                width: 1000.0,
                height: 700.0,
            },
        );
        assert!(state.is_open());
        state.update(
            None,
            &None,
            0.011,
            Size {
                width: 1000.0,
                height: 700.0,
            },
        );
        assert!(!state.is_open());
        assert_eq!(
            state.drain_events(),
            vec![PopupEvent::AutoDismissed {
                popup_id: "p1".to_string()
            }]
        );
    }

    #[test]
    fn actions_are_sanitized_to_v1_limit() {
        let mut state = PopupRuntimeState::new();
        let mut cfg = base_config();
        cfg.actions.push(PopupAction {
            id: "extra".to_string(),
            label: "Extra".to_string(),
            style: PopupActionStyle::Danger,
        });
        state.show_popup(cfg);
        let _ = state.drain_events();
        assert_eq!(state.active().unwrap().config.actions.len(), 2);

        let mut cfg2 = base_config();
        cfg2.id = "p2".to_string();
        cfg2.actions.clear();
        state.show_popup(cfg2);
        let _ = state.drain_events();
        assert_eq!(state.active().unwrap().config.actions.len(), 1);
        assert_eq!(state.active().unwrap().config.actions[0].id, "ok");
    }

    #[test]
    fn opening_frame_click_cannot_dismiss_popup() {
        let mut state = PopupRuntimeState::new();
        let mut cfg = base_config();
        cfg.dismiss_on_backdrop_click = true;
        state.show_popup(cfg);
        let _ = state.drain_events();

        state.update(
            Some(mouse(2.0, 2.0, true)),
            &None,
            0.016,
            Size {
                width: 1000.0,
                height: 700.0,
            },
        );

        assert!(state.is_open());
        assert!(state.drain_events().is_empty());
    }

    #[test]
    fn consume_opening_click_waits_for_release_before_click_actions() {
        let mut state = PopupRuntimeState::new();
        let mut cfg = base_config();
        cfg.consume_opening_click = true;
        state.show_popup(cfg);
        let _ = state.drain_events();

        state.update(
            Some(mouse(2.0, 2.0, true)),
            &None,
            0.016,
            Size {
                width: 1000.0,
                height: 700.0,
            },
        );
        assert!(state.is_open());

        state.update(
            Some(mouse(2.0, 2.0, false)),
            &None,
            0.016,
            Size {
                width: 1000.0,
                height: 700.0,
            },
        );
        assert!(state.is_open());

        state.update(
            Some(mouse(2.0, 2.0, true)),
            &None,
            0.016,
            Size {
                width: 1000.0,
                height: 700.0,
            },
        );
        assert_eq!(
            state.drain_events(),
            vec![PopupEvent::Dismissed {
                popup_id: "p1".to_string(),
                reason: PopupDismissReason::BackdropClick
            }]
        );
    }

    #[test]
    fn click_anywhere_action_fires_on_backdrop_and_panel() {
        let mut state = PopupRuntimeState::new();
        let mut cfg = base_config();
        cfg.click_anywhere_action_id = Some("ok".to_string());
        state.show_popup(cfg.clone());
        let _ = state.drain_events();
        state.update(
            Some(mouse(2.0, 2.0, false)),
            &None,
            0.016,
            Size {
                width: 1200.0,
                height: 800.0,
            },
        );

        state.update(
            Some(mouse(2.0, 2.0, true)),
            &None,
            0.016,
            Size {
                width: 1200.0,
                height: 800.0,
            },
        );
        assert_eq!(
            state.drain_events(),
            vec![PopupEvent::ActionSelected {
                popup_id: "p1".to_string(),
                action_id: "ok".to_string()
            }]
        );

        cfg.id = "p2".to_string();
        state.show_popup(cfg);
        let _ = state.drain_events();
        let layout = popup_layout_for(
            state.active().as_ref().map(|a| &a.config).unwrap(),
            Size {
                width: 1200.0,
                height: 800.0,
            },
        );
        state.update(
            Some(mouse(layout.panel.x + 4.0, layout.panel.y + 4.0, false)),
            &None,
            0.016,
            Size {
                width: 1200.0,
                height: 800.0,
            },
        );
        state.update(
            Some(mouse(layout.panel.x + 4.0, layout.panel.y + 4.0, true)),
            &None,
            0.016,
            Size {
                width: 1200.0,
                height: 800.0,
            },
        );
        assert_eq!(
            state.drain_events(),
            vec![PopupEvent::ActionSelected {
                popup_id: "p2".to_string(),
                action_id: "ok".to_string()
            }]
        );
    }

    #[test]
    fn popup_can_disable_background_input_blocking() {
        let mut state = PopupRuntimeState::new();
        let mut cfg = base_config();
        cfg.block_input_behind_popup = false;
        state.show_popup(cfg);
        assert!(!state.blocks_input_behind_popup());
    }

    #[test]
    fn custom_popup_replacement_enqueues_object_cleanup() {
        let mut state = PopupRuntimeState::new();
        state.show_popup_with_objects(
            base_config(),
            Rectangle::new(100.0, 100.0, 300.0, 200.0),
            vec![Uuid::nil()],
        );
        let _ = state.drain_events();
        let mut next = base_config();
        next.id = "p2".to_string();
        state.show_popup(next);

        assert_eq!(state.drain_popup_object_cleanup(), vec![Uuid::nil()]);
    }

    #[test]
    fn custom_popup_uses_manual_panel_bounds() {
        let mut state = PopupRuntimeState::new();
        let panel = Rectangle::new(120.0, 140.0, 420.0, 260.0);
        state.show_popup_with_objects(base_config(), panel, vec![]);
        let active = state.active().unwrap();
        let layout = popup_layout_for_active(
            active,
            Size {
                width: 1280.0,
                height: 720.0,
            },
        );
        assert_eq!(layout.panel, panel);
        assert!(layout.action_rects.is_empty());
    }
}
