//! Renderer-agnostic window/split layout machinery for the hjkl editor stack.
//!
//! A [`LayoutTree`] holds either a single [`Leaf`](LayoutTree::Leaf) (one window) or a
//! [`Split`](LayoutTree::Split) that recursively divides space between two sub-trees.
//! All geometry is expressed as [`LayoutRect`] — a plain `u16` rectangle that TUI
//! hosts convert from `ratatui::layout::Rect` and GUI hosts convert from their own
//! coordinate types at the boundary.
//!
//! # Quick start
//!
//! ```rust
//! use hjkl_layout::{LayoutTree, SplitDir, LayoutRect};
//!
//! let mut tree = LayoutTree::Leaf(0);
//! assert_eq!(tree.leaves(), vec![0]);
//!
//! // Split window 0 horizontally — window 1 below window 0.
//! tree.replace_leaf(0, |id| LayoutTree::Split {
//!     dir: SplitDir::Horizontal,
//!     ratio: 0.5,
//!     a: Box::new(LayoutTree::Leaf(id)),
//!     b: Box::new(LayoutTree::Leaf(1)),
//!     last_rect: None,
//! });
//! assert_eq!(tree.leaves(), vec![0, 1]);
//! assert_eq!(tree.neighbor_below(0), Some(1));
//! assert_eq!(tree.neighbor_below(1), None);
//! ```

/// Stable id into the host window list. Never reused — new windows get the next
/// value from the host's `next_window_id` counter.
pub type WindowId = usize;

/// Renderer-agnostic rectangle used by the layout tree.
///
/// TUI hosts convert `ratatui::layout::Rect` at the boundary; GUI hosts convert
/// from their floating-point coordinate space. All fields are `u16` which matches
/// ratatui's field types directly and is sufficient for terminal geometry (max
/// 65535 columns/rows).
///
/// `#[non_exhaustive]` — additional fields may be added in minor releases.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct LayoutRect {
    /// Column offset from the top-left corner of the terminal/window.
    pub x: u16,
    /// Row offset from the top-left corner of the terminal/window.
    pub y: u16,
    /// Width in columns.
    pub w: u16,
    /// Height in rows.
    pub h: u16,
}

impl LayoutRect {
    /// Construct a new [`LayoutRect`].
    pub fn new(x: u16, y: u16, w: u16, h: u16) -> Self {
        Self { x, y, w, h }
    }
}

/// Per-tab layout + focus state.
///
/// Each tab owns one [`LayoutTree`] (the window arrangement within that tab)
/// and records which window in that tree currently has focus. Windows and
/// slots are shared across tabs — a `WindowId` refers into the host's window
/// list regardless of which tab it lives in.
///
/// `#[non_exhaustive]` — additional fields may be added in minor releases.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Tab {
    /// Spatial layout tree for this tab. Leaves reference [`WindowId`]s.
    pub layout: LayoutTree,
    /// The window that has focus within this tab.
    pub focused_window: WindowId,
}

impl Tab {
    /// Create a new tab with the given layout and focused window.
    pub fn new(layout: LayoutTree, focused_window: WindowId) -> Self {
        Self {
            layout,
            focused_window,
        }
    }
}

impl Default for Tab {
    fn default() -> Self {
        Self {
            layout: LayoutTree::Leaf(0),
            focused_window: 0,
        }
    }
}

/// Per-window scroll + geometry state.
///
/// `#[non_exhaustive]` — additional fields may be added in minor releases.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Window {
    /// Index into the host's slot list for the buffer this window displays.
    pub slot: usize,
    /// The rect this window occupied in the last rendered frame.  Written
    /// by the renderer every frame; used by direction-navigation.
    /// `None` until the first render.
    pub last_rect: Option<LayoutRect>,
}

impl Window {
    /// Create a new window pointing at the given slot index.
    ///
    /// Per-window cursor + scroll live on the host's per-window `Editor`
    /// (#151 Phase D) — a `Window` is pure geometry (slot + last rendered rect).
    pub fn new(slot: usize) -> Self {
        Self {
            slot,
            last_rect: None,
        }
    }
}

impl Default for Window {
    fn default() -> Self {
        Self::new(0)
    }
}

/// Direction of a split.
///
/// `#[non_exhaustive]` — new directions may be added in minor releases.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SplitDir {
    /// Stacked top-to-bottom (`:split` / `:sp`).
    Horizontal,
    /// Side-by-side left-to-right (`:vsplit` / `:vsp`).
    Vertical,
}

/// Which geometric axis a split is oriented along.
///
/// Returned by [`SplitDir::axis`] so consumers outside the crate can match
/// exhaustively without bumping into the `#[non_exhaustive]` restriction on
/// [`SplitDir`] itself.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Axis {
    /// The split divides space top-to-bottom (rows).
    Row,
    /// The split divides space left-to-right (columns).
    Col,
}

impl SplitDir {
    /// Map this direction to its [`Axis`].
    ///
    /// `Horizontal` splits stacked vertically → they separate **rows**
    /// (`Axis::Row`). `Vertical` splits arranged side-by-side → they
    /// separate **columns** (`Axis::Col`).
    ///
    /// Unknown future variants fall back to `Axis::Row` so callers always
    /// receive a valid answer without panicking.
    pub fn axis(self) -> Axis {
        // `#[allow(unreachable_patterns)]` because from INSIDE this crate all
        // variants are known; the wildcard exists so external consumers relying
        // on `axis()` are future-proof when new variants are added.
        #[allow(unreachable_patterns)]
        match self {
            SplitDir::Horizontal => Axis::Row,
            SplitDir::Vertical => Axis::Col,
            _ => Axis::Row,
        }
    }
}

/// A binary spatial tree that partitions the editor area into windows.
///
/// # Examples
///
/// ```rust
/// use hjkl_layout::{LayoutTree, SplitDir};
///
/// let tree = LayoutTree::Split {
///     dir: SplitDir::Horizontal,
///     ratio: 0.5,
///     a: Box::new(LayoutTree::Leaf(0)),
///     b: Box::new(LayoutTree::Leaf(1)),
///     last_rect: None,
/// };
/// assert_eq!(tree.leaves(), vec![0, 1]);
/// assert_eq!(tree.neighbor_below(0), Some(1));
/// ```
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum LayoutTree {
    /// A single window occupying the full allocated area.
    Leaf(WindowId),
    /// Two sub-trees dividing the available space.
    Split {
        /// Axis along which the space is divided.
        dir: SplitDir,
        /// Fraction of the available space allocated to `a`. `0.0 < ratio < 1.0`.
        ratio: f32,
        /// The first (top / left) sub-tree.
        a: Box<LayoutTree>,
        /// The second (bottom / right) sub-tree.
        b: Box<LayoutTree>,
        /// Rect this split last occupied. Filled by the renderer each frame;
        /// read by resize commands to convert line/col deltas to ratio updates.
        /// None before the first render.
        last_rect: Option<LayoutRect>,
    },
}

impl Default for LayoutTree {
    fn default() -> Self {
        Self::Leaf(0)
    }
}

impl LayoutTree {
    /// Create a new single-leaf layout tree containing `id`.
    pub fn new(id: WindowId) -> Self {
        Self::Leaf(id)
    }

    /// Pre-order traversal — returns all leaf ids in the order they appear
    /// top-to-bottom / left-to-right in the layout.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use hjkl_layout::{LayoutTree, SplitDir};
    ///
    /// let tree = LayoutTree::Split {
    ///     dir: SplitDir::Horizontal,
    ///     ratio: 0.5,
    ///     a: Box::new(LayoutTree::Leaf(0)),
    ///     b: Box::new(LayoutTree::Leaf(1)),
    ///     last_rect: None,
    /// };
    /// assert_eq!(tree.leaves(), vec![0, 1]);
    /// ```
    pub fn leaves(&self) -> Vec<WindowId> {
        let mut out = Vec::new();
        self.collect_leaves(&mut out);
        out
    }

    fn collect_leaves(&self, out: &mut Vec<WindowId>) {
        match self {
            LayoutTree::Leaf(id) => out.push(*id),
            LayoutTree::Split { a, b, .. } => {
                a.collect_leaves(out);
                b.collect_leaves(out);
            }
        }
    }

    /// Return the next leaf in pre-order traversal, wrapping around.
    ///
    /// Returns `None` only if `id` is not in the tree (shouldn't happen in
    /// practice).
    pub fn next_leaf(&self, id: WindowId) -> Option<WindowId> {
        let leaves = self.leaves();
        let pos = leaves.iter().position(|&l| l == id)?;
        Some(leaves[(pos + 1) % leaves.len()])
    }

    /// Return the previous leaf in pre-order traversal, wrapping around.
    ///
    /// Returns `None` only if `id` is not in the tree (shouldn't happen in
    /// practice).
    pub fn prev_leaf(&self, id: WindowId) -> Option<WindowId> {
        let leaves = self.leaves();
        let pos = leaves.iter().position(|&l| l == id)?;
        let len = leaves.len();
        Some(leaves[(pos + len - 1) % len])
    }

    /// Return `true` if `id` appears anywhere in the tree.
    pub fn contains(&self, id: WindowId) -> bool {
        match self {
            LayoutTree::Leaf(leaf_id) => *leaf_id == id,
            LayoutTree::Split { a, b, .. } => a.contains(id) || b.contains(id),
        }
    }

    /// Find the leaf for `id` and replace it in-place with `f(id)`.
    /// Returns `true` if the leaf was found and replaced.
    pub fn replace_leaf<F: FnOnce(WindowId) -> LayoutTree + 'static>(
        &mut self,
        id: WindowId,
        f: F,
    ) -> bool {
        self.replace_leaf_boxed(id, Box::new(f))
    }

    fn replace_leaf_boxed(
        &mut self,
        id: WindowId,
        f: Box<dyn FnOnce(WindowId) -> LayoutTree>,
    ) -> bool {
        match self {
            LayoutTree::Leaf(leaf_id) if *leaf_id == id => {
                *self = f(id);
                true
            }
            LayoutTree::Leaf(_) => false,
            LayoutTree::Split { a, b, .. } => {
                // We need to check `a` first; if not found, check `b`.
                // Because `Box<dyn FnOnce>` is not `Copy`, we do this by
                // checking containment first, then calling.
                if a.contains(id) {
                    a.replace_leaf_boxed(id, f)
                } else {
                    b.replace_leaf_boxed(id, f)
                }
            }
        }
    }

    /// Return the id of the next leaf below `id` in a `Horizontal` split,
    /// using pre-order traversal semantics.
    ///
    /// "Below" means: walking up from `id`, find the innermost enclosing
    /// `Horizontal` split where `id` lives in `a`; the answer is then the
    /// first (leftmost) leaf of `b`.  If `id` is the bottom-most window
    /// (or there are no horizontal splits above it), returns `None`.
    pub fn neighbor_below(&self, id: WindowId) -> Option<WindowId> {
        self.neighbor_direction(id, NavDir::Below)
    }

    /// Return the id of the next leaf above `id` in a `Horizontal` split.
    pub fn neighbor_above(&self, id: WindowId) -> Option<WindowId> {
        self.neighbor_direction(id, NavDir::Above)
    }

    /// Return the id of the next leaf to the left of `id` in a `Vertical`
    /// split.  Horizontal splits are passed through.
    pub fn neighbor_left(&self, id: WindowId) -> Option<WindowId> {
        self.neighbor_direction(id, NavDir::Left)
    }

    /// Return the id of the next leaf to the right of `id` in a `Vertical`
    /// split.  Horizontal splits are passed through.
    pub fn neighbor_right(&self, id: WindowId) -> Option<WindowId> {
        self.neighbor_direction(id, NavDir::Right)
    }

    /// Internal unified helper for directional navigation.
    ///
    /// - `Below` / `Above` act on `Horizontal` splits; `Vertical` is a pass-through.
    /// - `Left` / `Right` act on `Vertical` splits; `Horizontal` is a pass-through.
    ///
    /// In each "active" split direction:
    /// - For the "forward" direction (Below / Right), when `id` is in `a`:
    ///   try to find a deeper neighbour inside `a` first; failing that, cross to `b`.
    ///   When `id` is in `b`: recurse into `b` only (no cross available).
    /// - For the "backward" direction (Above / Left), symmetric.
    fn neighbor_direction(&self, id: WindowId, dir: NavDir) -> Option<WindowId> {
        match self {
            LayoutTree::Leaf(_) => None,
            LayoutTree::Split {
                dir: split_dir,
                a,
                b,
                ..
            } => {
                // Which split direction is "active" for this nav direction?
                let active_split = match dir {
                    NavDir::Below | NavDir::Above => SplitDir::Horizontal,
                    NavDir::Left | NavDir::Right => SplitDir::Vertical,
                };
                // Is this a "forward" traversal (a→b) or "backward" (b→a)?
                let forward = matches!(dir, NavDir::Below | NavDir::Right);

                if *split_dir == active_split {
                    if a.contains(id) {
                        if forward {
                            // Try deeper forward-neighbour inside `a`.
                            let inner = a.neighbor_direction(id, dir);
                            if inner.is_some() {
                                return inner;
                            }
                            // Cross to `b`.
                            Some(first_leaf(b))
                        } else {
                            // Backward, `id` in `a` (the "first" half) — recurse only.
                            a.neighbor_direction(id, dir)
                        }
                    } else if b.contains(id) {
                        if forward {
                            // Forward, `id` in `b` (the "second" half) — recurse only.
                            b.neighbor_direction(id, dir)
                        } else {
                            // Try deeper backward-neighbour inside `b`.
                            let inner = b.neighbor_direction(id, dir);
                            if inner.is_some() {
                                return inner;
                            }
                            // Cross to `a`.
                            Some(last_leaf(a))
                        }
                    } else {
                        None
                    }
                } else {
                    // Pass-through: this split axis is orthogonal — recurse without offering a sibling.
                    if a.contains(id) {
                        a.neighbor_direction(id, dir)
                    } else if b.contains(id) {
                        b.neighbor_direction(id, dir)
                    } else {
                        None
                    }
                }
            }
        }
    }

    /// Walk the tree looking for the innermost enclosing Split with matching
    /// `dir` that contains `id`. Returns a mutable reference to the ratio,
    /// a copy of the last_rect, and whether the focused leaf is in `a`.
    /// Returns None if no such enclosing Split exists.
    pub fn enclosing_split_mut(
        &mut self,
        id: WindowId,
        dir: SplitDir,
    ) -> Option<(&mut f32, Option<LayoutRect>, bool)> {
        match self {
            LayoutTree::Leaf(_) => None,
            LayoutTree::Split {
                dir: my_dir,
                ratio,
                a,
                b,
                last_rect,
            } => {
                let in_a = a.contains(id);
                let in_b = b.contains(id);
                if !in_a && !in_b {
                    return None;
                }

                let my_dir = *my_dir;
                let saved_rect = *last_rect;

                // Try deeper first (innermost wins).
                let inner = if in_a {
                    a.enclosing_split_mut(id, dir)
                } else {
                    b.enclosing_split_mut(id, dir)
                };
                if inner.is_some() {
                    return inner;
                }

                // No deeper match — am I a candidate?
                if my_dir == dir {
                    Some((ratio, saved_rect, in_a))
                } else {
                    None
                }
            }
        }
    }

    /// Reset all splits in the tree to 0.5 ratio.
    pub fn equalize_all(&mut self) {
        if let LayoutTree::Split { ratio, a, b, .. } = self {
            *ratio = 0.5;
            a.equalize_all();
            b.equalize_all();
        }
    }

    /// For each enclosing Split on the path from root to leaf `id`, invoke
    /// `f` with the split's mutable state. Order: outermost first.
    pub fn for_each_ancestor<F>(&mut self, id: WindowId, f: &mut F)
    where
        F: FnMut(SplitDir, &mut f32, bool, Option<LayoutRect>),
    {
        if let LayoutTree::Split {
            dir,
            ratio,
            a,
            b,
            last_rect,
        } = self
        {
            let in_a = a.contains(id);
            let in_b = b.contains(id);
            if !in_a && !in_b {
                return;
            }
            // Outermost first: call f on this node before recursing.
            f(*dir, ratio, in_a, *last_rect);
            if in_a {
                a.for_each_ancestor(id, f);
            } else {
                b.for_each_ancestor(id, f);
            }
        }
    }

    /// Walk the tree and compute the [`LayoutRect`] each leaf window occupies
    /// within `area`, mirroring the renderer's `split_rect` + separator logic
    /// exactly so headless geometry matches what the TUI renderer would produce.
    ///
    /// # Split math (mirrors `render.rs::split_rect` + separator carving)
    ///
    /// For a **Horizontal** split (stacks top-to-bottom, `Axis::Row`):
    ///   `a_h = round(area.h * ratio).clamp(1, area.h - 1)`
    ///   `b_h = area.h - a_h`
    ///   If `a_h >= 2` and `b_h > 0`: the bottom row of `a` becomes a separator;
    ///   `a` shrinks by 1 (height becomes `a_h - 1`), `b` starts after the separator.
    ///
    /// For a **Vertical** split (side-by-side, `Axis::Col`):
    ///   `a_w = round(area.w * ratio).clamp(1, area.w - 1)`
    ///   `b_w = area.w - a_w`
    ///   If `a_w >= 2` and `b_w > 0`: the rightmost column of `a` becomes a separator;
    ///   `a` shrinks by 1 (width becomes `a_w - 1`), `b` starts right after `a`.
    ///
    /// Leaf → single entry `(id, area)`.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use hjkl_layout::{LayoutTree, SplitDir, LayoutRect};
    ///
    /// let tree = LayoutTree::Leaf(0);
    /// let area = LayoutRect::new(0, 0, 80, 23);
    /// let rects = tree.window_rects(area);
    /// assert_eq!(rects, vec![(0, area)]);
    /// ```
    pub fn window_rects(&self, area: LayoutRect) -> Vec<(WindowId, LayoutRect)> {
        let mut out = Vec::new();
        self.collect_rects(area, &mut out);
        out
    }

    fn collect_rects(&self, area: LayoutRect, out: &mut Vec<(WindowId, LayoutRect)>) {
        match self {
            LayoutTree::Leaf(id) => out.push((*id, area)),
            LayoutTree::Split {
                dir, ratio, a, b, ..
            } => {
                let (rect_a, rect_b) = headless_split_rect(area, *dir, *ratio);
                a.collect_rects(rect_a, out);
                b.collect_rects(rect_b, out);
            }
        }
    }

    /// Swap the two children of the deepest Split that directly contains
    /// `Leaf(id)` as one of its `a` or `b` children.
    ///
    /// Returns `true` if the swap was applied (i.e. there is an enclosing
    /// Split — `false` when `id` is the only window).
    pub fn swap_with_sibling(&mut self, id: WindowId) -> bool {
        match self {
            LayoutTree::Leaf(_) => false,
            LayoutTree::Split { a, b, .. } => {
                let a_is_focused_leaf = matches!(a.as_ref(), LayoutTree::Leaf(leaf) if *leaf == id);
                let b_is_focused_leaf = matches!(b.as_ref(), LayoutTree::Leaf(leaf) if *leaf == id);
                if a_is_focused_leaf || b_is_focused_leaf {
                    std::mem::swap(a, b);
                    return true;
                }
                // Recurse into whichever side contains id.
                if a.contains(id) {
                    return a.swap_with_sibling(id);
                }
                if b.contains(id) {
                    return b.swap_with_sibling(id);
                }
                false
            }
        }
    }

    /// Remove the leaf `id` from the tree.  When its parent `Split` is left
    /// with only the sibling, that split is replaced by the sibling subtree
    /// (collapse).
    ///
    /// Returns the `WindowId` of the leaf that should receive focus after
    /// removal (the sibling that survived the collapse), or `Err` if `id` is
    /// the only remaining leaf.
    ///
    /// # Errors
    ///
    /// Returns `Err("E444: Cannot close last window")` when attempting to
    /// remove the only leaf in the tree.
    pub fn remove_leaf(&mut self, id: WindowId) -> Result<WindowId, &'static str> {
        if matches!(self, LayoutTree::Leaf(_)) {
            return Err("E444: Cannot close last window");
        }
        match self.try_remove_leaf(id) {
            Some(focus) => Ok(focus),
            None => Err("E444: Cannot close last window"),
        }
    }

    /// Recursive helper for `remove_leaf`.  Returns `Some(new_focus)` when
    /// `id` was found and removed (or the caller needs to collapse this node),
    /// `None` when `id` was not in this subtree.
    fn try_remove_leaf(&mut self, id: WindowId) -> Option<WindowId> {
        match self {
            LayoutTree::Leaf(_) => None, // can't remove the only leaf
            LayoutTree::Split { a, b, .. } => {
                // Case 1: `a` is the leaf we want to remove.
                if matches!(a.as_ref(), LayoutTree::Leaf(leaf) if *leaf == id) {
                    let new_focus = first_leaf(b);
                    // Collapse: replace self with b.
                    *self = *b.clone();
                    return Some(new_focus);
                }
                // Case 2: `b` is the leaf we want to remove.
                if matches!(b.as_ref(), LayoutTree::Leaf(leaf) if *leaf == id) {
                    let new_focus = last_leaf(a);
                    // Collapse: replace self with a.
                    *self = *a.clone();
                    return Some(new_focus);
                }
                // Case 3: recurse into `a`.
                if a.contains(id) {
                    return a.try_remove_leaf(id);
                }
                // Case 4: recurse into `b`.
                if b.contains(id) {
                    return b.try_remove_leaf(id);
                }
                None
            }
        }
    }
}

/// Pure headless split — mirrors `render.rs::split_rect` + separator carving.
///
/// Divides `area` at `ratio` along the axis implied by `dir`, then carves
/// out the 1-cell separator so each child's rect is exactly what the TUI
/// renderer would pass to `render_layout`.
///
/// ## Separator rules (copied verbatim from `render.rs::render_layout`)
///
/// **Vertical** (side-by-side, `Axis::Col`): separator is the rightmost cell
/// of `rect_a`. Applied only when `rect_a.w >= 2` AND `rect_b.w > 0`; `a`
/// shrinks by 1 column, `b` position/size are unchanged.
///
/// **Horizontal** (stacked, `Axis::Row`): separator is the bottom cell of
/// `rect_a`. Applied only when `rect_a.h >= 2` AND `rect_b.h > 0`; `a`
/// shrinks by 1 row, `b` position/size are unchanged.
fn headless_split_rect(area: LayoutRect, dir: SplitDir, ratio: f32) -> (LayoutRect, LayoutRect) {
    match dir.axis() {
        Axis::Row => {
            // Horizontal split: divide height.
            let a_h = ((area.h as f32) * ratio).round() as u16;
            let a_h = a_h.clamp(1, area.h.saturating_sub(1).max(1));
            let b_h = area.h.saturating_sub(a_h);
            let mut rect_a = LayoutRect::new(area.x, area.y, area.w, a_h);
            let rect_b = LayoutRect::new(area.x, area.y + a_h, area.w, b_h);
            // Carve separator: bottom row of rect_a, only when safe.
            if rect_a.h >= 2 && rect_b.h > 0 {
                rect_a.h -= 1;
            }
            (rect_a, rect_b)
        }
        Axis::Col => {
            // Vertical split: divide width.
            let a_w = ((area.w as f32) * ratio).round() as u16;
            let a_w = a_w.clamp(1, area.w.saturating_sub(1).max(1));
            let b_w = area.w.saturating_sub(a_w);
            let mut rect_a = LayoutRect::new(area.x, area.y, a_w, area.h);
            let rect_b = LayoutRect::new(area.x + a_w, area.y, b_w, area.h);
            // Carve separator: rightmost column of rect_a, only when safe.
            if rect_a.w >= 2 && rect_b.w > 0 {
                rect_a.w -= 1;
            }
            (rect_a, rect_b)
        }
    }
}

/// Internal direction enum used by `neighbor_direction`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NavDir {
    Below,
    Above,
    Left,
    Right,
}

/// First (top / left) leaf in a subtree.
fn first_leaf(tree: &LayoutTree) -> WindowId {
    match tree {
        LayoutTree::Leaf(id) => *id,
        LayoutTree::Split { a, .. } => first_leaf(a),
    }
}

/// Last (bottom / right) leaf in a subtree.
fn last_leaf(tree: &LayoutTree) -> WindowId {
    match tree {
        LayoutTree::Leaf(id) => *id,
        LayoutTree::Split { b, .. } => last_leaf(b),
    }
}

// ── Unit tests ────────────────────────────────────────────────────────────────

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

    fn leaf(id: WindowId) -> LayoutTree {
        LayoutTree::Leaf(id)
    }

    fn hsplit(ratio: f32, a: LayoutTree, b: LayoutTree) -> LayoutTree {
        LayoutTree::Split {
            dir: SplitDir::Horizontal,
            ratio,
            a: Box::new(a),
            b: Box::new(b),
            last_rect: None,
        }
    }

    fn vsplit(ratio: f32, a: LayoutTree, b: LayoutTree) -> LayoutTree {
        LayoutTree::Split {
            dir: SplitDir::Vertical,
            ratio,
            a: Box::new(a),
            b: Box::new(b),
            last_rect: None,
        }
    }

    fn hsplit_with_rect(ratio: f32, a: LayoutTree, b: LayoutTree, rect: LayoutRect) -> LayoutTree {
        LayoutTree::Split {
            dir: SplitDir::Horizontal,
            ratio,
            a: Box::new(a),
            b: Box::new(b),
            last_rect: Some(rect),
        }
    }

    fn vsplit_with_rect(ratio: f32, a: LayoutTree, b: LayoutTree, rect: LayoutRect) -> LayoutTree {
        LayoutTree::Split {
            dir: SplitDir::Vertical,
            ratio,
            a: Box::new(a),
            b: Box::new(b),
            last_rect: Some(rect),
        }
    }

    // ── LayoutRect ────────────────────────────────────────────────────────────

    #[test]
    fn layout_rect_new_roundtrips_fields() {
        let r = LayoutRect::new(1, 2, 80, 24);
        assert_eq!(r.x, 1);
        assert_eq!(r.y, 2);
        assert_eq!(r.w, 80);
        assert_eq!(r.h, 24);
    }

    #[test]
    fn layout_rect_default_is_zero() {
        let r = LayoutRect::default();
        assert_eq!(r, LayoutRect::new(0, 0, 0, 0));
    }

    // ── Tab ───────────────────────────────────────────────────────────────────

    #[test]
    fn tab_new_and_default() {
        let t = Tab::new(LayoutTree::Leaf(5), 5);
        assert_eq!(t.focused_window, 5);
        assert_eq!(t.layout.leaves(), vec![5]);

        let d = Tab::default();
        assert_eq!(d.focused_window, 0);
        assert_eq!(d.layout.leaves(), vec![0]);
    }

    // ── Window ────────────────────────────────────────────────────────────────

    #[test]
    fn window_new_and_default() {
        let w = Window::new(3);
        assert_eq!(w.slot, 3);
        assert!(w.last_rect.is_none());

        let d = Window::default();
        assert_eq!(d.slot, 0);
    }

    // ── LayoutTree::new / default ─────────────────────────────────────────────

    #[test]
    fn layout_tree_new_creates_leaf() {
        let t = LayoutTree::new(7);
        assert_eq!(t.leaves(), vec![7]);
    }

    #[test]
    fn layout_tree_default_is_leaf_zero() {
        let t = LayoutTree::default();
        assert_eq!(t.leaves(), vec![0]);
    }

    // ── leaves() ─────────────────────────────────────────────────────────────

    #[test]
    fn leaves_single_leaf() {
        let tree = leaf(0);
        assert_eq!(tree.leaves(), vec![0]);
    }

    #[test]
    fn leaves_two_leaf_split() {
        let tree = hsplit(0.5, leaf(0), leaf(1));
        assert_eq!(tree.leaves(), vec![0, 1]);
    }

    #[test]
    fn leaves_nested_horizontal_splits() {
        // 0 / (1 / 2)
        let tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
        assert_eq!(tree.leaves(), vec![0, 1, 2]);
    }

    #[test]
    fn leaves_nested_left_split() {
        let tree = hsplit(0.5, hsplit(0.5, leaf(0), leaf(1)), leaf(2));
        assert_eq!(tree.leaves(), vec![0, 1, 2]);
    }

    // ── contains() ───────────────────────────────────────────────────────────

    #[test]
    fn contains_returns_true_for_present_id() {
        let tree = hsplit(0.5, leaf(0), leaf(1));
        assert!(tree.contains(0));
        assert!(tree.contains(1));
        assert!(!tree.contains(2));
    }

    // ── replace_leaf() ───────────────────────────────────────────────────────

    #[test]
    fn replace_leaf_on_single_leaf() {
        let mut tree = leaf(0);
        let replaced = tree.replace_leaf(0, |_| leaf(99));
        assert!(replaced);
        assert_eq!(tree.leaves(), vec![99]);
    }

    #[test]
    fn replace_leaf_in_split_left() {
        let mut tree = hsplit(0.5, leaf(0), leaf(1));
        let replaced = tree.replace_leaf(0, |id| hsplit(0.5, leaf(id + 10), leaf(id)));
        assert!(replaced);
        assert_eq!(tree.leaves(), vec![10, 0, 1]);
    }

    #[test]
    fn replace_leaf_not_found_returns_false() {
        let mut tree = hsplit(0.5, leaf(0), leaf(1));
        let replaced = tree.replace_leaf(99, |_| leaf(99));
        assert!(!replaced);
        assert_eq!(tree.leaves(), vec![0, 1]);
    }

    // ── neighbor_below() / neighbor_above() ──────────────────────────────────

    #[test]
    fn neighbor_below_two_leaf() {
        let tree = hsplit(0.5, leaf(0), leaf(1));
        assert_eq!(tree.neighbor_below(0), Some(1));
        assert_eq!(tree.neighbor_below(1), None);
    }

    #[test]
    fn neighbor_above_two_leaf() {
        let tree = hsplit(0.5, leaf(0), leaf(1));
        assert_eq!(tree.neighbor_above(0), None);
        assert_eq!(tree.neighbor_above(1), Some(0));
    }

    #[test]
    fn neighbor_below_three_leaf_nested_bottom() {
        // 0 / (1 / 2)
        let tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
        assert_eq!(tree.neighbor_below(0), Some(1));
        assert_eq!(tree.neighbor_below(1), Some(2));
        assert_eq!(tree.neighbor_below(2), None);
    }

    #[test]
    fn neighbor_above_three_leaf_nested_bottom() {
        let tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
        assert_eq!(tree.neighbor_above(0), None);
        assert_eq!(tree.neighbor_above(1), Some(0));
        assert_eq!(tree.neighbor_above(2), Some(1));
    }

    #[test]
    fn neighbor_below_three_leaf_nested_top() {
        let tree = hsplit(0.5, hsplit(0.5, leaf(0), leaf(1)), leaf(2));
        assert_eq!(tree.neighbor_below(0), Some(1));
        assert_eq!(tree.neighbor_below(1), Some(2));
        assert_eq!(tree.neighbor_below(2), None);
    }

    #[test]
    fn neighbor_above_three_leaf_nested_top() {
        let tree = hsplit(0.5, hsplit(0.5, leaf(0), leaf(1)), leaf(2));
        assert_eq!(tree.neighbor_above(0), None);
        assert_eq!(tree.neighbor_above(1), Some(0));
        assert_eq!(tree.neighbor_above(2), Some(1));
    }

    // ── remove_leaf() ────────────────────────────────────────────────────────

    #[test]
    fn remove_leaf_only_leaf_errors() {
        let mut tree = leaf(0);
        assert!(tree.remove_leaf(0).is_err());
    }

    #[test]
    fn remove_leaf_collapses_parent_keeps_sibling() {
        let mut tree = hsplit(0.5, leaf(0), leaf(1));
        let focus = tree.remove_leaf(0).unwrap();
        assert_eq!(focus, 1);
        assert_eq!(tree.leaves(), vec![1]);
    }

    #[test]
    fn remove_leaf_b_side_collapses_to_a() {
        let mut tree = hsplit(0.5, leaf(0), leaf(1));
        let focus = tree.remove_leaf(1).unwrap();
        assert_eq!(focus, 0);
        assert_eq!(tree.leaves(), vec![0]);
    }

    #[test]
    fn remove_leaf_nested_middle() {
        // 0 / (1 / 2)  → remove 1 → 0 / 2
        let mut tree = hsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
        let focus = tree.remove_leaf(1).unwrap();
        assert_eq!(focus, 2);
        assert_eq!(tree.leaves(), vec![0, 2]);
    }

    // ── neighbor_left() / neighbor_right() ───────────────────────────────────

    #[test]
    fn neighbor_left_in_vertical_split() {
        let tree = vsplit(0.5, leaf(0), leaf(1));
        assert_eq!(tree.neighbor_left(0), None);
        assert_eq!(tree.neighbor_left(1), Some(0));
    }

    #[test]
    fn neighbor_right_in_vertical_split() {
        let tree = vsplit(0.5, leaf(0), leaf(1));
        assert_eq!(tree.neighbor_right(0), Some(1));
        assert_eq!(tree.neighbor_right(1), None);
    }

    #[test]
    fn neighbor_left_no_op_in_horizontal_split() {
        let tree = hsplit(0.5, leaf(0), leaf(1));
        assert_eq!(tree.neighbor_left(0), None);
        assert_eq!(tree.neighbor_left(1), None);
        assert_eq!(tree.neighbor_right(0), None);
        assert_eq!(tree.neighbor_right(1), None);
    }

    #[test]
    fn neighbor_left_three_leaf_vertical() {
        let tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
        assert_eq!(tree.neighbor_left(0), None);
        assert_eq!(tree.neighbor_left(1), Some(0));
        assert_eq!(tree.neighbor_left(2), Some(1));
    }

    #[test]
    fn neighbor_right_three_leaf_vertical() {
        let tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
        assert_eq!(tree.neighbor_right(0), Some(1));
        assert_eq!(tree.neighbor_right(1), Some(2));
        assert_eq!(tree.neighbor_right(2), None);
    }

    // ── next_leaf() / prev_leaf() ─────────────────────────────────────────────

    #[test]
    fn next_leaf_cycles_through_all_leaves() {
        let tree = vsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
        assert_eq!(tree.next_leaf(0), Some(1));
        assert_eq!(tree.next_leaf(1), Some(2));
        assert_eq!(tree.next_leaf(2), Some(0));
    }

    #[test]
    fn prev_leaf_wraps_around() {
        let tree = vsplit(0.5, leaf(0), hsplit(0.5, leaf(1), leaf(2)));
        assert_eq!(tree.prev_leaf(0), Some(2));
        assert_eq!(tree.prev_leaf(1), Some(0));
        assert_eq!(tree.prev_leaf(2), Some(1));
    }

    #[test]
    fn next_leaf_single_leaf_wraps_to_self() {
        let tree = leaf(0);
        assert_eq!(tree.next_leaf(0), Some(0));
    }

    #[test]
    fn next_prev_returns_none_for_unknown_id() {
        let tree = vsplit(0.5, leaf(0), leaf(1));
        assert_eq!(tree.next_leaf(99), None);
        assert_eq!(tree.prev_leaf(99), None);
    }

    // ── enclosing_split_mut() ────────────────────────────────────────────────

    #[test]
    fn enclosing_split_mut_returns_innermost() {
        // outer: hsplit 0 / inner: hsplit 1 / 2
        let outer_rect = LayoutRect::new(0, 0, 80, 40);
        let inner_rect = LayoutRect::new(0, 20, 80, 20);
        let mut tree = hsplit_with_rect(
            0.4,
            leaf(0),
            hsplit_with_rect(0.6, leaf(1), leaf(2), inner_rect),
            outer_rect,
        );
        let result = tree.enclosing_split_mut(1, SplitDir::Horizontal);
        assert!(result.is_some(), "should find enclosing horizontal split");
        let (ratio, rect, in_a) = result.unwrap();
        assert!(
            (*ratio - 0.6).abs() < 1e-5,
            "innermost split ratio should be 0.6, got {ratio}"
        );
        assert_eq!(
            rect,
            Some(inner_rect),
            "should return inner rect, not outer"
        );
        assert!(in_a, "id=1 is in the 'a' side of the inner split");
    }

    #[test]
    fn enclosing_split_mut_skips_wrong_dir() {
        let mut tree = vsplit(0.5, leaf(0), leaf(1));
        let result = tree.enclosing_split_mut(0, SplitDir::Horizontal);
        assert!(
            result.is_none(),
            "should not match a Vertical split for Horizontal dir"
        );
    }

    #[test]
    fn enclosing_split_mut_returns_none_for_only_leaf() {
        let mut tree = leaf(0);
        let result = tree.enclosing_split_mut(0, SplitDir::Horizontal);
        assert!(result.is_none(), "single leaf has no enclosing split");
    }

    #[test]
    fn equalize_all_resets_nested_splits_to_half() {
        let mut tree = hsplit(0.3, leaf(0), hsplit(0.7, leaf(1), leaf(2)));
        tree.equalize_all();
        fn check_all_half(t: &LayoutTree) {
            if let LayoutTree::Split { ratio, a, b, .. } = t {
                assert!(
                    (ratio - 0.5).abs() < 1e-5,
                    "ratio should be 0.5, got {ratio}"
                );
                check_all_half(a);
                check_all_half(b);
            }
        }
        check_all_half(&tree);
    }

    #[test]
    fn for_each_ancestor_visits_outermost_first() {
        let outer_rect = LayoutRect::new(0, 0, 80, 24);
        let inner_rect = LayoutRect::new(24, 0, 56, 24);
        let mut tree = vsplit_with_rect(
            0.3,
            leaf(0),
            hsplit_with_rect(0.7, leaf(1), leaf(2), inner_rect),
            outer_rect,
        );
        let mut visited_dirs: Vec<SplitDir> = Vec::new();
        let mut visited_ratios: Vec<f32> = Vec::new();
        tree.for_each_ancestor(1, &mut |dir, ratio, _in_a, _rect| {
            visited_dirs.push(dir);
            visited_ratios.push(*ratio);
        });
        assert_eq!(
            visited_dirs,
            vec![SplitDir::Vertical, SplitDir::Horizontal],
            "outermost (Vertical) should be visited first"
        );
        assert!(
            (visited_ratios[0] - 0.3).abs() < 1e-5,
            "outer ratio should be 0.3"
        );
        assert!(
            (visited_ratios[1] - 0.7).abs() < 1e-5,
            "inner ratio should be 0.7"
        );
    }

    // ── swap_with_sibling() ───────────────────────────────────────────────────

    #[test]
    fn swap_with_sibling_swaps_two_leaves() {
        let mut tree = hsplit(0.5, leaf(0), leaf(1));
        let swapped = tree.swap_with_sibling(0);
        assert!(swapped, "swap should succeed in a two-leaf split");
        assert_eq!(tree.leaves(), vec![1, 0], "leaves should be swapped");
    }

    #[test]
    fn swap_with_sibling_in_nested_split_swaps_at_focused_parent() {
        let mut tree = hsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
        let swapped = tree.swap_with_sibling(1);
        assert!(swapped, "swap should succeed");
        assert_eq!(
            tree.leaves(),
            vec![0, 2, 1],
            "inner leaves should be swapped"
        );
    }

    #[test]
    fn swap_with_sibling_returns_false_for_only_leaf() {
        let mut tree = leaf(0);
        let swapped = tree.swap_with_sibling(0);
        assert!(!swapped, "single leaf has no sibling to swap with");
    }

    // ── mixed_layout_navigation ───────────────────────────────────────────────

    #[test]
    fn mixed_layout_navigation() {
        // Layout:
        //   ┌───┬───┐
        //   │ 0 │ 1 │
        //   ├───┴───┤
        //   │   2   │
        //   └───────┘
        let tree = hsplit(0.5, vsplit(0.5, leaf(0), leaf(1)), leaf(2));

        assert_eq!(tree.neighbor_right(0), Some(1));
        assert_eq!(tree.neighbor_left(1), Some(0));
        assert_eq!(tree.neighbor_right(1), None);
        assert_eq!(tree.neighbor_left(0), None);

        assert_eq!(tree.neighbor_below(0), Some(2));
        assert_eq!(tree.neighbor_below(1), Some(2));
        assert_eq!(tree.neighbor_below(2), None);
        assert_eq!(tree.neighbor_above(2), Some(1));
        assert_eq!(tree.neighbor_above(0), None);
        assert_eq!(tree.neighbor_above(1), None);

        assert_eq!(tree.next_leaf(0), Some(1));
        assert_eq!(tree.next_leaf(1), Some(2));
        assert_eq!(tree.next_leaf(2), Some(0));
        assert_eq!(tree.prev_leaf(0), Some(2));
        assert_eq!(tree.prev_leaf(2), Some(1));
    }

    // ── window_rects() ────────────────────────────────────────────────────────

    #[test]
    fn window_rects_single_leaf_gets_full_area() {
        let tree = leaf(0);
        let area = LayoutRect::new(0, 0, 80, 23);
        let rects = tree.window_rects(area);
        assert_eq!(rects, vec![(0, area)]);
    }

    #[test]
    fn window_rects_vsplit_two_side_by_side() {
        // Vertical split (side-by-side): area.w=80, ratio=0.5
        // a_w = round(80 * 0.5) = 40, clamped => 40
        // b_w = 80 - 40 = 40
        // Separator: rect_a.w(40) >= 2 and rect_b.w(40) > 0 → rect_a.w = 39
        // rect_a = (0,0,39,23), rect_b = (40,0,40,23)
        let tree = vsplit(0.5, leaf(0), leaf(1));
        let area = LayoutRect::new(0, 0, 80, 23);
        let rects = tree.window_rects(area);
        assert_eq!(rects.len(), 2);
        let (id_a, ra) = rects[0];
        let (id_b, rb) = rects[1];
        assert_eq!(id_a, 0);
        assert_eq!(id_b, 1);
        // widths: 39 + 1 (sep) + 40 = 80
        assert_eq!(
            ra.w + 1 + rb.w,
            80,
            "widths + separator must sum to parent width"
        );
        assert_eq!(ra.h, 23);
        assert_eq!(rb.h, 23);
        // rect_b starts right after rect_a + separator
        assert_eq!(rb.x, ra.x + ra.w + 1);
    }

    #[test]
    fn window_rects_hsplit_stacked() {
        // Horizontal split (stacked): area.h=23, ratio=0.5
        // a_h = round(23 * 0.5) = 12 (banker's rounding on some platforms; 11.5 → 12)
        // Actually: 23 * 0.5 = 11.5, round() = 12 in Rust (round half away from zero)
        // b_h = 23 - 12 = 11
        // Separator: rect_a.h(12) >= 2 and rect_b.h(11) > 0 → rect_a.h = 11
        let tree = hsplit(0.5, leaf(0), leaf(1));
        let area = LayoutRect::new(0, 0, 80, 23);
        let rects = tree.window_rects(area);
        assert_eq!(rects.len(), 2);
        let (_, ra) = rects[0];
        let (_, rb) = rects[1];
        // heights: ra.h + 1 (sep) + rb.h == area.h
        assert_eq!(
            ra.h + 1 + rb.h,
            23,
            "heights + separator must sum to parent height"
        );
        assert_eq!(ra.w, 80);
        assert_eq!(rb.w, 80);
        // rect_b starts after rect_a + separator row
        assert_eq!(rb.y, ra.y + ra.h + 1);
    }

    #[test]
    fn window_rects_nested_vsplit_inside_vsplit() {
        // vsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2))) over 80x23
        // Outer: a_w=40, sep → rect_a.w=39; b_w=40 starting at x=40
        // Inner (over 40-wide area starting at x=40): a_w=20, sep → 19; b_w=20 at x=60
        let tree = vsplit(0.5, leaf(0), vsplit(0.5, leaf(1), leaf(2)));
        let area = LayoutRect::new(0, 0, 80, 23);
        let rects = tree.window_rects(area);
        assert_eq!(rects.len(), 3);
        // total width coverage: sum(widths) + 2 separators = 80
        let total_w: u16 = rects.iter().map(|(_, r)| r.w).sum::<u16>() + 2;
        assert_eq!(total_w, 80, "all window widths + 2 separators == 80");
    }

    // ── remove_leaf error message ─────────────────────────────────────────────

    #[test]
    fn remove_leaf_error_contains_e444() {
        let mut tree = leaf(0);
        let err = tree.remove_leaf(0).unwrap_err();
        assert!(err.contains("E444"), "error must mention E444, got: {err}");
    }

    // ── Layout update via enclosing_split_mut ─────────────────────────────────

    #[test]
    fn enclosing_split_mut_ratio_update_persists() {
        let mut tree = hsplit(0.5, leaf(0), leaf(1));
        {
            let (ratio, _, _) = tree.enclosing_split_mut(0, SplitDir::Horizontal).unwrap();
            *ratio = 0.75;
        }
        // Verify the ratio was actually mutated.
        if let LayoutTree::Split { ratio, .. } = &tree {
            assert!((*ratio - 0.75).abs() < 1e-5, "ratio should now be 0.75");
        } else {
            panic!("tree should still be a Split");
        }
    }
}
