//! Layout algorithm — turns a [`Widget`] tree into resolved screen rects.
//!
//! [`layout`] is a single recursive top-down pass that mixes a one-shot
//! intrinsic-size measurement (for `FitContent` and `Grow` accounting) with
//! the actual placement. The resulting [`LayoutTree`] is a flat `Vec` of
//! [`LayoutNode`]s; each node records its own `rect`, `content_rect`
//! (padding-inset), and the indices of its direct children. The layout
//! function itself has no GPU, no input, no allocation outside the result —
//! every test in this stage runs headlessly.
//!
//! # Slot vs rect, and why anchor children skip resizing
//!
//! The recursion uses two entry points:
//!
//! - [`arrange_in_slot`] is for stack / grid children and the root: the slot
//!   is the **outer space** the widget can occupy; the algorithm applies the
//!   widget's margin, sizing, and alignment to derive its rect.
//! - [`arrange_in_rect`] is for anchor children: the rect is *already* what
//!   the anchor decided; the widget's margin / sizing / alignment are skipped
//!   so the anchor is authoritative. Padding still applies (it's an inside-
//!   the-rect concern). This matches the Unity/Godot convention that "anchor
//!   determines rect" — sizing knobs would let the child silently disagree
//!   with the anchor it was placed by.
//!
//! # DPI scale factor
//!
//! Every linear input (sizing, padding, margin, gaps, anchor offsets) is in
//! logical pixels and multiplied by [`layout`]'s `scale` argument at resolve
//! time. The widget tree is DPI-independent; the layout call is where the
//! display's scale factor enters.

use glam::Vec2;
use serde::{Deserialize, Serialize};

use crate::math::Rect;

use super::style::{Align, Insets, LayoutStyle, Sizing};
use super::widget::{AnchorGroup, Grid, Stack, StackDirection, Widget, WidgetId, WidgetKind};

/// One node in a resolved [`LayoutTree`] — the widget's id and its on-screen
/// rectangles.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LayoutNode {
    /// Mirror of [`Widget::id`].
    pub id: WidgetId,
    /// The outer rectangle the widget occupies, after margin / sizing /
    /// alignment.
    pub rect: Rect,
    /// `rect` minus the widget's padding — the area children are arranged
    /// inside.
    pub content_rect: Rect,
    /// Indices into [`LayoutTree::nodes`] of the direct children, in the same
    /// order as on the input widget.
    pub children: Vec<u32>,
}

/// Result of laying out a widget tree — a flat array of [`LayoutNode`]s with
/// the root at index 0.
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct LayoutTree {
    nodes: Vec<LayoutNode>,
}

impl LayoutTree {
    /// All nodes, root first, in the pre-order produced by [`layout`].
    pub fn nodes(&self) -> &[LayoutNode] {
        &self.nodes
    }

    /// The root node (always present after a successful layout).
    pub fn root(&self) -> Option<&LayoutNode> {
        self.nodes.first()
    }

    /// Look up the first node with the given non-empty id.
    ///
    /// Returns `None` if `id` is empty or no node matches. Linear scan — fine
    /// for the dozens-of-widgets trees Stage 8 currently targets; a hash map
    /// can be added if a profile says it's hot.
    pub fn find(&self, id: &WidgetId) -> Option<&LayoutNode> {
        if id.is_empty() {
            return None;
        }
        self.nodes.iter().find(|n| n.id == *id)
    }

    /// The direct children of the node at `index`.
    pub fn children_of(&self, index: usize) -> impl Iterator<Item = &LayoutNode> + '_ {
        self.nodes[index]
            .children
            .iter()
            .map(move |i| &self.nodes[*i as usize])
    }
}

/// Lay out `root` inside `viewport` at the given DPI `scale`, producing a
/// [`LayoutTree`] with one entry per widget in pre-order.
pub fn layout(root: &Widget, viewport: Rect, scale: f32) -> LayoutTree {
    let mut nodes = Vec::with_capacity(root.node_count());
    arrange_in_slot(root, viewport, scale, &mut nodes);
    LayoutTree { nodes }
}

// ---------- internal: recursive arrangement ----------

fn arrange_in_slot(widget: &Widget, slot: Rect, scale: f32, out: &mut Vec<LayoutNode>) -> u32 {
    let margin = widget.style.margin.scaled(scale);
    let outer = shrink(slot, margin);
    let outer_size = outer.size();

    let intrinsic = measure(widget, outer_size, scale);
    let resolved_w = resolve_axis(widget.style.width, outer_size.x, intrinsic.x, scale);
    let resolved_h = resolve_axis(widget.style.height, outer_size.y, intrinsic.y, scale);
    let resolved = Vec2::new(resolved_w, resolved_h);

    let extra = (outer_size - resolved).max(Vec2::ZERO);
    let offset = Vec2::new(
        align_offset(widget.style.align_horizontal, extra.x),
        align_offset(widget.style.align_vertical, extra.y),
    );
    let rect = Rect::from_min_size(outer.min + offset, resolved);

    arrange_in_rect(widget, rect, scale, out)
}

fn arrange_in_rect(widget: &Widget, rect: Rect, scale: f32, out: &mut Vec<LayoutNode>) -> u32 {
    let padding = widget.style.padding.scaled(scale);
    let content_rect = shrink(rect, padding);

    let my_idx = out.len() as u32;
    out.push(LayoutNode {
        id: widget.id.clone(),
        rect,
        content_rect,
        children: Vec::new(),
    });

    match &widget.kind {
        WidgetKind::Leaf { .. } => {}
        WidgetKind::Stack(stack) => arrange_stack(my_idx, stack, content_rect, scale, out),
        WidgetKind::Grid(grid) => arrange_grid(my_idx, grid, content_rect, scale, out),
        WidgetKind::Anchor(group) => arrange_anchor(my_idx, group, content_rect, scale, out),
    }

    my_idx
}

fn arrange_stack(
    parent_idx: u32,
    stack: &Stack,
    content: Rect,
    scale: f32,
    out: &mut Vec<LayoutNode>,
) {
    let n = stack.children.len();
    if n == 0 {
        return;
    }

    let gap = stack.gap * scale;
    let total_gap = gap * n.saturating_sub(1) as f32;
    let content_main = main_extent(stack.direction, content.size());
    let content_cross = cross_extent(stack.direction, content.size());

    // Pass 1: compute each child's main-axis size (fixed/fit) and tally
    // grow weights.
    let mut main_sizes: Vec<f32> = Vec::with_capacity(n);
    let mut grow_weights: Vec<Option<f32>> = Vec::with_capacity(n);
    let mut fixed_main_total = 0.0_f32;
    let mut total_grow = 0.0_f32;

    for child in &stack.children {
        let margin = child.style.margin.scaled(scale);
        let margin_main = main_extent(
            stack.direction,
            Vec2::new(margin.horizontal(), margin.vertical()),
        );
        let main_sizing = match stack.direction {
            StackDirection::Row => child.style.width,
            StackDirection::Column => child.style.height,
        };

        let (inner_main, weight) = match main_sizing {
            Sizing::Fixed(v) => (v * scale, None),
            Sizing::FitContent => {
                let m = measure(child, content.size(), scale);
                (main_extent(stack.direction, m), None)
            }
            Sizing::Grow(w) => (0.0, Some(w.max(0.0))),
        };

        if let Some(w) = weight {
            total_grow += w;
        }
        grow_weights.push(weight);
        main_sizes.push(inner_main + margin_main);
        fixed_main_total += inner_main + margin_main;
    }

    let leftover = (content_main - fixed_main_total - total_gap).max(0.0);
    if total_grow > 0.0 {
        for (i, w) in grow_weights.iter().enumerate() {
            if let Some(w) = w {
                main_sizes[i] += leftover * (*w / total_grow);
            }
        }
    }

    // After distributing Grow, any remaining slack is positioned via the
    // stack's `main_align`. (If any child grew, slack is zero.)
    let used_main: f32 = main_sizes.iter().sum::<f32>() + total_gap;
    let extra = (content_main - used_main).max(0.0);
    let start_offset = align_offset(stack.main_align, extra);

    // Pass 2: place each child in its slot.
    let mut cursor = start_offset;
    let mut child_indices = Vec::with_capacity(n);
    for (i, child) in stack.children.iter().enumerate() {
        let slot_main = main_sizes[i];
        let slot = make_slot(stack.direction, content, cursor, slot_main, content_cross);
        cursor += slot_main + gap;
        child_indices.push(arrange_in_slot(child, slot, scale, out));
    }

    out[parent_idx as usize].children = child_indices;
}

fn arrange_grid(
    parent_idx: u32,
    grid: &Grid,
    content: Rect,
    scale: f32,
    out: &mut Vec<LayoutNode>,
) {
    if grid.cols == 0 || grid.rows == 0 || grid.children.is_empty() {
        return;
    }
    let gap = grid.gap * scale;
    let total_gap_x = gap.x * grid.cols.saturating_sub(1) as f32;
    let total_gap_y = gap.y * grid.rows.saturating_sub(1) as f32;
    let cell_w = ((content.width() - total_gap_x) / grid.cols as f32).max(0.0);
    let cell_h = ((content.height() - total_gap_y) / grid.rows as f32).max(0.0);
    let cells = grid.cols * grid.rows;

    let mut child_indices = Vec::with_capacity(grid.children.len().min(cells as usize));
    for (i, child) in grid.children.iter().enumerate() {
        if i as u32 >= cells {
            break;
        }
        let row = i as u32 / grid.cols;
        let col = i as u32 % grid.cols;
        let cell_origin =
            content.min + Vec2::new(col as f32 * (cell_w + gap.x), row as f32 * (cell_h + gap.y));
        let slot = Rect::from_min_size(cell_origin, Vec2::new(cell_w, cell_h));
        child_indices.push(arrange_in_slot(child, slot, scale, out));
    }
    out[parent_idx as usize].children = child_indices;
}

fn arrange_anchor(
    parent_idx: u32,
    group: &AnchorGroup,
    content: Rect,
    scale: f32,
    out: &mut Vec<LayoutNode>,
) {
    let size = content.size();
    let mut child_indices = Vec::with_capacity(group.children.len());
    for child in &group.children {
        let a = child.style.anchor;
        let min = content.min + size * a.min + a.offset_min * scale;
        let max = content.min + size * a.max + a.offset_max * scale;
        let target = Rect::new(min, max);
        child_indices.push(arrange_in_rect(child, target, scale, out));
    }
    out[parent_idx as usize].children = child_indices;
}

// ---------- internal: measurement ----------

fn measure(widget: &Widget, available: Vec2, scale: f32) -> Vec2 {
    match &widget.kind {
        WidgetKind::Leaf { intrinsic } => *intrinsic * scale,
        WidgetKind::Stack(stack) => measure_stack(&widget.style, stack, available, scale),
        WidgetKind::Grid(grid) => measure_grid(&widget.style, grid, available, scale),
        // Anchor parents derive their children's rects from the parent's size,
        // so they can't propose an intrinsic "fit" size; FitContent on an
        // anchor parent collapses to zero.
        WidgetKind::Anchor(_) => Vec2::ZERO,
    }
}

/// Outer footprint of a child (the slot it would consume in its parent),
/// including its own margin.
fn measure_outer(widget: &Widget, available: Vec2, scale: f32) -> Vec2 {
    let intrinsic = measure(widget, available, scale);
    let w = match widget.style.width {
        Sizing::Fixed(v) => v * scale,
        Sizing::FitContent => intrinsic.x,
        Sizing::Grow(_) => 0.0,
    };
    let h = match widget.style.height {
        Sizing::Fixed(v) => v * scale,
        Sizing::FitContent => intrinsic.y,
        Sizing::Grow(_) => 0.0,
    };
    let m = widget.style.margin.scaled(scale);
    Vec2::new(w + m.horizontal(), h + m.vertical())
}

fn measure_stack(parent_style: &LayoutStyle, stack: &Stack, available: Vec2, scale: f32) -> Vec2 {
    let mut main = 0.0_f32;
    let mut cross = 0.0_f32;
    let n = stack.children.len();
    for child in &stack.children {
        let s = measure_outer(child, available, scale);
        main += main_extent(stack.direction, s);
        cross = cross.max(cross_extent(stack.direction, s));
    }
    if n > 1 {
        main += stack.gap * scale * (n - 1) as f32;
    }
    let p = parent_style.padding.scaled(scale);
    match stack.direction {
        StackDirection::Row => Vec2::new(main + p.horizontal(), cross + p.vertical()),
        StackDirection::Column => Vec2::new(cross + p.horizontal(), main + p.vertical()),
    }
}

fn measure_grid(parent_style: &LayoutStyle, grid: &Grid, available: Vec2, scale: f32) -> Vec2 {
    if grid.cols == 0 || grid.rows == 0 {
        return Vec2::ZERO;
    }
    let mut cell_w = 0.0_f32;
    let mut cell_h = 0.0_f32;
    for child in &grid.children {
        let s = measure_outer(child, available, scale);
        cell_w = cell_w.max(s.x);
        cell_h = cell_h.max(s.y);
    }
    let gap = grid.gap * scale;
    let total = Vec2::new(
        cell_w * grid.cols as f32 + gap.x * grid.cols.saturating_sub(1) as f32,
        cell_h * grid.rows as f32 + gap.y * grid.rows.saturating_sub(1) as f32,
    );
    let p = parent_style.padding.scaled(scale);
    Vec2::new(total.x + p.horizontal(), total.y + p.vertical())
}

// ---------- internal: small helpers ----------

fn shrink(r: Rect, i: Insets) -> Rect {
    let min = r.min + Vec2::new(i.left, i.top);
    let max = r.max - Vec2::new(i.right, i.bottom);
    Rect::new(min, max)
}

fn align_offset(align: Align, extra: f32) -> f32 {
    match align {
        Align::Start => 0.0,
        Align::Center => extra * 0.5,
        Align::End => extra,
    }
}

fn resolve_axis(sizing: Sizing, available: f32, intrinsic: f32, scale: f32) -> f32 {
    match sizing {
        Sizing::Fixed(v) => (v * scale).min(available),
        Sizing::Grow(_) => available,
        Sizing::FitContent => intrinsic.min(available),
    }
}

fn main_extent(dir: StackDirection, v: Vec2) -> f32 {
    match dir {
        StackDirection::Row => v.x,
        StackDirection::Column => v.y,
    }
}

fn cross_extent(dir: StackDirection, v: Vec2) -> f32 {
    match dir {
        StackDirection::Row => v.y,
        StackDirection::Column => v.x,
    }
}

fn make_slot(dir: StackDirection, content: Rect, cursor: f32, main: f32, cross: f32) -> Rect {
    match dir {
        StackDirection::Row => {
            Rect::from_min_size(content.min + Vec2::new(cursor, 0.0), Vec2::new(main, cross))
        }
        StackDirection::Column => {
            Rect::from_min_size(content.min + Vec2::new(0.0, cursor), Vec2::new(cross, main))
        }
    }
}

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

    fn vp(w: f32, h: f32) -> Rect {
        Rect::from_min_size(Vec2::ZERO, Vec2::new(w, h))
    }

    /// `Grow(1.0)` on both axes — the common "fill the parent" style for
    /// container tests where intrinsic sizing would collapse the root.
    fn grow_both() -> LayoutStyle {
        LayoutStyle {
            width: Sizing::Grow(1.0),
            height: Sizing::Grow(1.0),
            ..Default::default()
        }
    }

    #[test]
    fn single_leaf_takes_intrinsic_size_at_origin() {
        let w = Widget::leaf(Vec2::new(40.0, 20.0)).with_id("a");
        let tree = layout(&w, vp(800.0, 600.0), 1.0);
        let n = tree.find(&"a".into()).unwrap();
        assert_eq!(
            n.rect,
            Rect::from_min_size(Vec2::ZERO, Vec2::new(40.0, 20.0))
        );
        assert_eq!(n.content_rect, n.rect);
        assert_eq!(tree.nodes().len(), 1);
    }

    #[test]
    fn dpi_scale_doubles_sizes() {
        let w = Widget::leaf(Vec2::new(40.0, 20.0));
        let tree = layout(&w, vp(800.0, 600.0), 2.0);
        let n = tree.root().unwrap();
        assert_eq!(n.rect.size(), Vec2::new(80.0, 40.0));
    }

    #[test]
    fn row_stack_places_fixed_children_with_gap() {
        let row = Widget::row()
            .with_id("row")
            .with_gap(4.0)
            .with_child(Widget::leaf(Vec2::new(30.0, 20.0)).with_id("a"))
            .with_child(Widget::leaf(Vec2::new(50.0, 20.0)).with_id("b"))
            .with_child(Widget::leaf(Vec2::new(10.0, 20.0)).with_id("c"));
        let tree = layout(&row, vp(200.0, 100.0), 1.0);
        let a = tree.find(&"a".into()).unwrap().rect;
        let b = tree.find(&"b".into()).unwrap().rect;
        let c = tree.find(&"c".into()).unwrap().rect;
        assert_eq!(a, Rect::from_min_size(Vec2::ZERO, Vec2::new(30.0, 20.0)));
        assert_eq!(
            b,
            Rect::from_min_size(Vec2::new(34.0, 0.0), Vec2::new(50.0, 20.0))
        );
        assert_eq!(
            c,
            Rect::from_min_size(Vec2::new(88.0, 0.0), Vec2::new(10.0, 20.0))
        );
    }

    #[test]
    fn row_stack_grow_fills_leftover_space() {
        // 200 wide; A=30 fixed, B=Grow, C=10 fixed → B gets 160 wide.
        let row =
            Widget::row()
                .with_id("row")
                .with_style(grow_both())
                .with_child(Widget::leaf(Vec2::new(30.0, 20.0)).with_id("a"))
                .with_child(Widget::leaf(Vec2::new(0.0, 20.0)).with_id("b").with_style(
                    LayoutStyle {
                        width: Sizing::Grow(1.0),
                        height: Sizing::Fixed(20.0),
                        ..Default::default()
                    },
                ))
                .with_child(Widget::leaf(Vec2::new(10.0, 20.0)).with_id("c"));
        let tree = layout(&row, vp(200.0, 100.0), 1.0);
        let b = tree.find(&"b".into()).unwrap().rect;
        assert_eq!(b.min.x, 30.0);
        assert_eq!(b.width(), 160.0);
        assert_eq!(tree.find(&"c".into()).unwrap().rect.min.x, 190.0);
    }

    #[test]
    fn row_stack_grow_weights_split_proportionally() {
        // 300 wide root; A=Grow(1), B=Grow(2) → A gets 100, B gets 200.
        let row = Widget::row()
            .with_style(grow_both())
            .with_child(Widget::default().with_id("a").with_style(LayoutStyle {
                width: Sizing::Grow(1.0),
                height: Sizing::Grow(1.0),
                ..Default::default()
            }))
            .with_child(Widget::default().with_id("b").with_style(LayoutStyle {
                width: Sizing::Grow(2.0),
                height: Sizing::Grow(1.0),
                ..Default::default()
            }));
        let tree = layout(&row, vp(300.0, 30.0), 1.0);
        let a = tree.find(&"a".into()).unwrap().rect;
        let b = tree.find(&"b".into()).unwrap().rect;
        assert_eq!(a.width(), 100.0);
        assert_eq!(b.width(), 200.0);
        assert_eq!(b.min.x, 100.0);
        // Cross axis Grow fills full height.
        assert_eq!(a.height(), 30.0);
        assert_eq!(b.height(), 30.0);
    }

    #[test]
    fn row_stack_main_align_center_splits_extra() {
        // Two 30-wide children with gap 0 → main extent 60; viewport 200 →
        // 140 extra, centered → 70 each side.
        let row = Widget::row()
            .with_main_align(Align::Center)
            .with_style(grow_both())
            .with_child(Widget::leaf(Vec2::new(30.0, 10.0)).with_id("a"))
            .with_child(Widget::leaf(Vec2::new(30.0, 10.0)).with_id("b"));
        let tree = layout(&row, vp(200.0, 20.0), 1.0);
        assert_eq!(tree.find(&"a".into()).unwrap().rect.min.x, 70.0);
        assert_eq!(tree.find(&"b".into()).unwrap().rect.min.x, 100.0);
    }

    #[test]
    fn row_stack_cross_align_end_docks_to_bottom() {
        // Child is 30x10 in a 100-wide row with 40 tall → align End → top=30.
        let row = Widget::row().with_style(grow_both()).with_child(
            Widget::leaf(Vec2::new(30.0, 10.0))
                .with_id("a")
                .with_style(LayoutStyle {
                    align_vertical: Align::End,
                    ..Default::default()
                }),
        );
        let tree = layout(&row, vp(100.0, 40.0), 1.0);
        let a = tree.find(&"a".into()).unwrap().rect;
        assert_eq!(a.min.y, 30.0);
        assert_eq!(a.max.y, 40.0);
    }

    #[test]
    fn column_stack_flows_top_to_bottom() {
        let col = Widget::column()
            .with_gap(2.0)
            .with_child(Widget::leaf(Vec2::new(20.0, 10.0)).with_id("a"))
            .with_child(Widget::leaf(Vec2::new(20.0, 30.0)).with_id("b"));
        let tree = layout(&col, vp(100.0, 100.0), 1.0);
        let a = tree.find(&"a".into()).unwrap().rect;
        let b = tree.find(&"b".into()).unwrap().rect;
        assert_eq!(a.min, Vec2::ZERO);
        assert_eq!(a.max.y, 10.0);
        assert_eq!(b.min.y, 12.0);
        assert_eq!(b.max.y, 42.0);
    }

    #[test]
    fn grid_two_by_three_makes_six_equal_cells() {
        // 100x60 content, 2 cols × 3 rows, no gap → cells 50x20.
        let grid = Widget::grid(2, 3)
            .with_style(grow_both())
            .with_children((0..6).map(|i| Widget::leaf(Vec2::ZERO).with_id(format!("c{i}"))));
        let tree = layout(&grid, vp(100.0, 60.0), 1.0);
        for i in 0..6 {
            let row = i / 2;
            let col = i % 2;
            let n = tree.find(&format!("c{i}").into()).unwrap();
            // Default FitContent of zero intrinsic ⇒ children collapse to
            // (col*50, row*20)–(col*50, row*20) at Start align inside the
            // cell. Verify the *cell origin* via the node's `rect.min`.
            assert_eq!(n.rect.min, Vec2::new(col as f32 * 50.0, row as f32 * 20.0));
        }
    }

    #[test]
    fn grid_gap_subtracts_from_cell_size() {
        let grid = Widget::grid(2, 2)
            .with_style(grow_both())
            .with_grid_gap(Vec2::new(10.0, 10.0))
            .with_children((0..4).map(|i| {
                Widget::default()
                    .with_id(format!("c{i}"))
                    .with_style(LayoutStyle {
                        width: Sizing::Grow(1.0),
                        height: Sizing::Grow(1.0),
                        ..Default::default()
                    })
            }));
        let tree = layout(&grid, vp(110.0, 110.0), 1.0);
        // (110 - 10 gap) / 2 = 50 per cell.
        for i in 0..4 {
            let n = tree.find(&format!("c{i}").into()).unwrap();
            assert_eq!(n.rect.size(), Vec2::new(50.0, 50.0));
        }
        // Second column starts at 60 (50 + 10 gap).
        assert_eq!(tree.find(&"c1".into()).unwrap().rect.min.x, 60.0);
        // Second row starts at 60.
        assert_eq!(tree.find(&"c2".into()).unwrap().rect.min.y, 60.0);
    }

    #[test]
    fn anchor_fill_makes_child_match_parent_content() {
        let parent = Widget::anchor()
            .with_id("p")
            .with_style(LayoutStyle {
                width: Sizing::Grow(1.0),
                height: Sizing::Grow(1.0),
                ..Default::default()
            })
            .with_child(Widget::leaf(Vec2::ZERO).with_id("child"));
        let tree = layout(&parent, vp(200.0, 100.0), 1.0);
        let child = tree.find(&"child".into()).unwrap();
        assert_eq!(
            child.rect,
            Rect::from_min_size(Vec2::ZERO, Vec2::new(200.0, 100.0))
        );
    }

    #[test]
    fn anchor_top_right_with_offsets_places_child_relative_to_corner() {
        // Pin the child's top-right at the parent's top-right, then push the
        // top-left corner 80 pixels left and 24 pixels down → 80×24 child in
        // the top-right corner.
        let parent = Widget::anchor()
            .with_style(LayoutStyle {
                width: Sizing::Grow(1.0),
                height: Sizing::Grow(1.0),
                ..Default::default()
            })
            .with_child(
                Widget::leaf(Vec2::ZERO)
                    .with_id("c")
                    .with_style(LayoutStyle {
                        anchor: Anchor::TOP_RIGHT
                            .with_offsets(Vec2::new(-80.0, 0.0), Vec2::new(0.0, 24.0)),
                        ..Default::default()
                    }),
            );
        let tree = layout(&parent, vp(300.0, 200.0), 1.0);
        let c = tree.find(&"c".into()).unwrap().rect;
        assert_eq!(c.min, Vec2::new(220.0, 0.0));
        assert_eq!(c.max, Vec2::new(300.0, 24.0));
    }

    #[test]
    fn anchor_dpi_scales_offsets() {
        let parent = Widget::anchor()
            .with_style(LayoutStyle {
                width: Sizing::Grow(1.0),
                height: Sizing::Grow(1.0),
                ..Default::default()
            })
            .with_child(
                Widget::leaf(Vec2::ZERO)
                    .with_id("c")
                    .with_style(LayoutStyle {
                        anchor: Anchor::TOP_LEFT.with_offsets(Vec2::ZERO, Vec2::new(40.0, 20.0)),
                        ..Default::default()
                    }),
            );
        let tree = layout(&parent, vp(400.0, 400.0), 2.0);
        let c = tree.find(&"c".into()).unwrap().rect;
        assert_eq!(c.min, Vec2::ZERO);
        assert_eq!(c.max, Vec2::new(80.0, 40.0));
    }

    #[test]
    fn padding_shrinks_content_rect_and_offsets_children() {
        let row = Widget::row()
            .with_id("row")
            .with_style(LayoutStyle {
                width: Sizing::Grow(1.0),
                height: Sizing::Grow(1.0),
                padding: Insets::all(10.0),
                ..Default::default()
            })
            .with_child(Widget::leaf(Vec2::new(50.0, 20.0)).with_id("a"));
        let tree = layout(&row, vp(200.0, 100.0), 1.0);
        let row_node = tree.find(&"row".into()).unwrap();
        assert_eq!(
            row_node.rect,
            Rect::from_min_size(Vec2::ZERO, Vec2::new(200.0, 100.0))
        );
        assert_eq!(
            row_node.content_rect,
            Rect::from_min_size(Vec2::splat(10.0), Vec2::new(180.0, 80.0))
        );
        let a = tree.find(&"a".into()).unwrap().rect;
        assert_eq!(a.min, Vec2::splat(10.0));
        assert_eq!(a.size(), Vec2::new(50.0, 20.0));
    }

    #[test]
    fn margin_reserves_space_outside_widget() {
        let row =
            Widget::row().with_child(Widget::leaf(Vec2::new(40.0, 20.0)).with_id("a").with_style(
                LayoutStyle {
                    margin: Insets::symmetric(5.0, 0.0),
                    ..Default::default()
                },
            ));
        let tree = layout(&row, vp(200.0, 100.0), 1.0);
        let a = tree.find(&"a".into()).unwrap().rect;
        // 5px left margin → child starts at 5, width 40.
        assert_eq!(a.min.x, 5.0);
        assert_eq!(a.max.x, 45.0);
    }

    #[test]
    fn fit_content_stack_sums_children_plus_padding() {
        // Two 30x10 fixed children, no gap, padding=8 → root 76 x 26.
        let row = Widget::row()
            .with_id("root")
            .with_style(LayoutStyle {
                padding: Insets::all(8.0),
                ..Default::default()
            })
            .with_child(Widget::leaf(Vec2::new(30.0, 10.0)))
            .with_child(Widget::leaf(Vec2::new(30.0, 10.0)));
        let tree = layout(&row, vp(1000.0, 1000.0), 1.0);
        let r = tree.find(&"root".into()).unwrap().rect;
        assert_eq!(r.size(), Vec2::new(76.0, 26.0));
    }

    #[test]
    fn layout_tree_round_trips_through_ron() {
        let w = Widget::row()
            .with_id("root")
            .with_gap(4.0)
            .with_child(Widget::leaf(Vec2::new(20.0, 10.0)).with_id("a"));
        let tree = layout(&w, vp(100.0, 50.0), 1.0);
        let text = ron::ser::to_string(&tree).unwrap();
        let decoded: LayoutTree = ron::de::from_str(&text).unwrap();
        assert_eq!(tree, decoded);
    }

    #[test]
    fn find_rejects_empty_id() {
        let w = Widget::leaf(Vec2::ONE);
        let tree = layout(&w, vp(10.0, 10.0), 1.0);
        assert!(tree.find(&WidgetId::default()).is_none());
    }

    #[test]
    fn children_of_iterates_direct_children_only() {
        let tree = layout(
            &Widget::row()
                .with_child(Widget::leaf(Vec2::new(10.0, 10.0)).with_id("a"))
                .with_child(
                    Widget::column()
                        .with_id("col")
                        .with_child(Widget::leaf(Vec2::new(10.0, 10.0)).with_id("inner")),
                ),
            vp(100.0, 100.0),
            1.0,
        );
        let ids: Vec<_> = tree
            .children_of(0)
            .map(|n| n.id.as_str().to_owned())
            .collect();
        assert_eq!(ids, vec!["a".to_string(), "col".to_string()]);
    }
}
