use std::collections::HashMap;
use crate::entity_state::{EntityState, EntityView};
use crate::types::{AssetTag, EntityId, EntityTypeId, PoolId};

// ---------------------------------------------------------------------------
// Tier 1 — Cosmetic theme types
// ---------------------------------------------------------------------------

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StyleToken {
    pub key: String,   // e.g. "color.primary"
    pub value: String, // e.g. "#ff0000"
}

impl StyleToken {
    pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
        Self { key: key.into(), value: value.into() }
    }
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)]
pub struct StyleTokens(pub Vec<StyleToken>);

impl StyleTokens {
    pub fn new() -> Self {
        Self(vec![])
    }

    pub fn with_token(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.0.push(StyleToken::new(key, value));
        self
    }

    pub fn get(&self, key: &str) -> Option<&str> {
        self.0.iter().find(|t| t.key == key).map(|t| t.value.as_str())
    }
}

// ---------------------------------------------------------------------------
// Slot types for Tier 2 generic composition
// ---------------------------------------------------------------------------

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Slot {
    Primary,    // one winner (Featured wins)
    Indicator,  // small badge overlays (HP bar, condition icons)
    Overlay,    // top-layer overlays (revealed/hidden indicator)
    Action,     // clickable action buttons
    Detail,     // expanded panel content
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct SlottedHint {
    pub slot: Slot,
    pub node: ViewNode,
    pub priority: i32,
}

impl SlottedHint {
    pub fn new(slot: Slot, node: ViewNode, priority: i32) -> Self {
        Self { slot, node, priority }
    }
}

// ---------------------------------------------------------------------------
// FieldRef — typed bindings to entity trait data
// ---------------------------------------------------------------------------

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum PoolField {
    Current,
    Max,
    Min,
    Ratio,  // current / max as 0.0–1.0 scaled to i64 * 1000 for integer math
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum FieldRef {
    FeaturedName,
    FeaturedDescription,
    FeaturedIcon,
    QuantifiedPool { pool: PoolId, field: PoolField },
    SpatialPosition,   // resolves to a debug string of the position
    OwnableOwner,      // resolves to seat id as string, or empty
    /// Index of this entity within the container it's contained in (was StackableIndex).
    ContainerIndex,
    /// Size of the container this entity is contained in (was StackableCount).
    ContainerCount,
    MorphableForm,
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub enum TextSource {
    Static(String),
    Field(FieldRef),
    Format { template: String, slots: Vec<(String, FieldRef)> },
}

impl TextSource {
    pub fn static_str(s: impl Into<String>) -> Self {
        Self::Static(s.into())
    }
}

// ---------------------------------------------------------------------------
// Condition — typed predicates for conditional nodes
// ---------------------------------------------------------------------------

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub enum Condition {
    PoolAbove    { pool: PoolId, threshold: i64 },
    PoolBelow    { pool: PoolId, threshold: i64 },
    PoolDepleted { pool: PoolId },
    PoolFull     { pool: PoolId },
    TraitPresent(u32),
    IsRevealed,
    OwnerIs(u64),
    Not(Box<Condition>),
    And(Vec<Condition>),
    Or(Vec<Condition>),
}

/// Alias for Condition — kept for external crate compatibility.
pub type ViewCondition = Condition;

// ---------------------------------------------------------------------------
// ViewNode — the declarative view tree
// ---------------------------------------------------------------------------

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
    Row,
    Column,
}


#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub enum ViewNode {
    // Layout
    Stack { direction: Direction, children: Vec<ViewNode> },
    Grid { cols: u32, children: Vec<ViewNode> },
    Tabs { labels: Vec<String>, children: Vec<ViewNode> },

    // Display
    Label { text: TextSource, style_token: Option<String> },
    Icon { asset: AssetTag, size: u32 },
    ProgressBar { value: FieldRef, max: FieldRef, style_token: Option<String> },
    Boxes { filled: u32, total: u32, style_token: Option<String> },
    Numeric { value: i64, label: Option<String> },

    // Actions
    Button { label: String, entity: EntityId },

    // Composition
    GenericTraitWidget { trait_bit: u32 },
    Conditional {
        condition: Condition,
        then_node: Box<ViewNode>,
        else_node: Option<Box<ViewNode>>,
    },

    // UC-29 Phase C — sheet composition

    /// A titled sheet section. Pure display sugar over
    /// `Stack(Label, body)` so plugin templates don't have to hand-roll
    /// the boilerplate.
    Section {
        title: String,
        body: Box<ViewNode>,
    },

    /// Render `item_template` once per child of the referenced
    /// container. Resolves children via `view.container.contents` on
    /// the entity named by `container`; the renderer looks up each
    /// child's `EntityView` from the surrounding map. When the
    /// container is empty (or absent), renders `empty_node` if set,
    /// otherwise nothing.
    ForEachChild {
        container: ContainerRef,
        item_template: Box<ViewNode>,
        empty_node: Option<Box<ViewNode>>,
    },

    /// A drop target wrapping a body. Phase E1 wires the drop handler
    /// to emit `propose_batch([Removed, Inserted])` when the held
    /// entity is released over the zone. In Phase C the variant exists
    /// in the IR; the renderer passes the body through transparently.
    DropZone {
        container_entity: EntityId,
        body: Box<ViewNode>,
    },

    // Empty
    Empty,
}

/// Reference to a container the rendering host knows about.
/// Constrained on purpose so the renderer can resolve refs locally
/// without a server round-trip (see UC-29 Phase C rationale in
/// `docs/superpowers/specs/2026-05-13-uc-29-interactive-character-sheet.md`).
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ContainerRef {
    /// The host entity's own `ContainerData`. Spelled `Self_` because
    /// `Self` is a Rust keyword.
    Self_,
    /// UC-29-Pre — a direct child of `host.container.contents` whose
    /// `Entity.tags` contains `tag`. Used to address nested sub-
    /// containers (slot patterns) without depending on display names
    /// or entity IDs.
    ///
    /// Resolution semantics:
    /// 1. Walk `host.container.contents`.
    /// 2. Look up each child's `EntityView` in the recipient's entities map.
    /// 3. Select the first whose tags contain `tag`.
    /// 4. The expansion is that child's `container.contents` (recursed once).
    ///
    /// "First match wins" — multiple children sharing the same slot
    /// tag is a plugin-modeling mistake, not a feature.
    ChildByTag(String),
}

impl ViewNode {
    pub fn label(text: impl Into<String>) -> Self {
        Self::Label { text: TextSource::Static(text.into()), style_token: None }
    }

    pub fn label_styled(text: impl Into<String>, style: impl Into<String>) -> Self {
        Self::Label { text: TextSource::Static(text.into()), style_token: Some(style.into()) }
    }

    pub fn column(children: Vec<ViewNode>) -> Self {
        Self::Stack { direction: Direction::Column, children }
    }

    pub fn row(children: Vec<ViewNode>) -> Self {
        Self::Stack { direction: Direction::Row, children }
    }

    pub fn is_empty(&self) -> bool {
        matches!(self, Self::Empty)
    }
}

// ---------------------------------------------------------------------------
// ViewTemplate — Tier 3 game-specific rendering
// ---------------------------------------------------------------------------

/// A template maps an entity to a ViewNode tree.
pub struct ViewTemplate {
    pub entity_type_id: EntityTypeId,
    pub name: String,
    pub render: Box<dyn Fn(&EntityView) -> ViewNode + Send + Sync>,
}

impl ViewTemplate {
    pub fn new(
        entity_type_id: EntityTypeId,
        name: impl Into<String>,
        render: impl Fn(&EntityView) -> ViewNode + Send + Sync + 'static,
    ) -> Self {
        Self {
            entity_type_id,
            name: name.into(),
            render: Box::new(render),
        }
    }

    pub fn render(&self, view: &EntityView) -> ViewNode {
        (self.render)(view)
    }
}

// ---------------------------------------------------------------------------
// ViewRegistry — maps entity type IDs to ViewTemplates
// ---------------------------------------------------------------------------

pub struct ViewRegistry {
    templates: HashMap<EntityTypeId, ViewTemplate>,
}

impl ViewRegistry {
    pub fn new() -> Self {
        Self { templates: HashMap::new() }
    }

    pub fn register(&mut self, template: ViewTemplate) {
        self.templates.insert(template.entity_type_id, template);
    }

    /// Render an entity view. Falls back to Tier 2 generic composition if no
    /// template is registered for this entity type.
    pub fn render(
        &self,
        view: &EntityView,
        store: &EntityState,
        generic: &GenericWidgetRegistry,
    ) -> ViewNode {
        let type_id = store.entity(view.id).map(|e| e.type_id);
        if let Some(tid) = type_id {
            if let Some(template) = self.templates.get(&tid) {
                return template.render(view);
            }
        }
        // Tier 2 fallback
        generic.compose_node(view)
    }

    pub fn has_template(&self, type_id: EntityTypeId) -> bool {
        self.templates.contains_key(&type_id)
    }

    pub fn template_count(&self) -> usize {
        self.templates.len()
    }
}

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

// ---------------------------------------------------------------------------
// GenericWidgetRegistry — Tier 2 per-trait widget producers
// ---------------------------------------------------------------------------

pub type TraitWidgetFn = Box<dyn Fn(&EntityView) -> Vec<SlottedHint> + Send + Sync>;

pub struct GenericWidgetRegistry {
    widgets: HashMap<u32, TraitWidgetFn>,  // trait bit → widget fn
}

impl GenericWidgetRegistry {
    pub fn new() -> Self {
        Self { widgets: HashMap::new() }
    }

    pub fn register_trait_widget(
        &mut self,
        trait_bit: u32,
        f: impl Fn(&EntityView) -> Vec<SlottedHint> + Send + Sync + 'static,
    ) {
        self.widgets.insert(trait_bit, Box::new(f));
    }

    /// Returns hints from ALL registered traits present on the entity.
    /// Sorted by (slot, priority descending).
    pub fn hints_for(&self, view: &EntityView) -> Vec<SlottedHint> {
        let mut hints: Vec<SlottedHint> = self.widgets
            .values()
            .flat_map(|f| f(view))
            .collect();

        // Sort by slot order, then priority descending within each slot
        hints.sort_by(|a, b| {
            a.slot.cmp(&b.slot)
                .then_with(|| b.priority.cmp(&a.priority))
        });
        hints
    }

    /// Compose hints into a ViewNode tree.
    /// Primary slot: highest-priority winner only.
    /// Indicator/Overlay/Action/Detail: all, in order.
    pub fn compose_node(&self, view: &EntityView) -> ViewNode {
        let hints = self.hints_for(view);

        let primary: Vec<&SlottedHint> = hints.iter()
            .filter(|h| h.slot == Slot::Primary)
            .take(1)
            .collect();
        let indicators: Vec<&SlottedHint> = hints.iter()
            .filter(|h| h.slot == Slot::Indicator)
            .collect();
        let overlays: Vec<&SlottedHint> = hints.iter()
            .filter(|h| h.slot == Slot::Overlay)
            .collect();
        let actions: Vec<&SlottedHint> = hints.iter()
            .filter(|h| h.slot == Slot::Action)
            .collect();
        let details: Vec<&SlottedHint> = hints.iter()
            .filter(|h| h.slot == Slot::Detail)
            .collect();

        let mut sections: Vec<ViewNode> = vec![];

        for h in primary {
            sections.push(h.node.clone());
        }

        if !indicators.is_empty() {
            let indicator_row = ViewNode::row(
                indicators.iter().map(|h| h.node.clone()).collect()
            );
            sections.push(indicator_row);
        }

        for h in overlays {
            sections.push(h.node.clone());
        }

        if !actions.is_empty() {
            let action_row = ViewNode::row(
                actions.iter().map(|h| h.node.clone()).collect()
            );
            sections.push(action_row);
        }

        for h in details {
            sections.push(h.node.clone());
        }

        if sections.is_empty() {
            ViewNode::Empty
        } else {
            ViewNode::column(sections)
        }
    }

    pub fn widget_count(&self) -> usize {
        self.widgets.len()
    }
}

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

// ---------------------------------------------------------------------------
// Scene contributors — non-view plugins providing visual overlays
// ---------------------------------------------------------------------------

/// The layer within the scene where this overlay renders.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SceneLayer {
    /// Floating numbers above entities (damage, healing)
    DamageNumbers,
    /// Environmental effects covering the board
    Weather,
    /// Particle or visual effects tied to entities
    Effects,
    /// Heads-up display elements
    HUD,
}

/// A scene overlay node contributed by a specific entity.
///
/// Added as a `ViewNode::SceneOverlay` variant so the renderer knows which
/// entity owns this overlay and at which scene layer to draw it.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub struct SceneOverlay {
    pub contributor_entity: crate::types::EntityId,
    pub layer: SceneLayer,
    pub node: ViewNode,
}

/// A boxed closure that produces an optional overlay `ViewNode` for one
/// entity-view. Introduced by 0400 to satisfy clippy `type_complexity`.
pub type SceneOverlayProducer = Box<dyn Fn(&EntityView) -> Option<ViewNode> + Send + Sync>;

/// Registry mapping contributor entity IDs to overlay-producing closures.
///
/// At render time the board renderer calls `overlays_for` with each visible
/// entity's `EntityView` and collects the resulting overlays.
pub struct SceneContributorRegistry {
    contributors: Vec<(crate::types::EntityId, SceneOverlayProducer)>,
}

impl SceneContributorRegistry {
    pub fn new() -> Self {
        Self { contributors: vec![] }
    }

    /// Register a contributor. The closure receives the entity's view and
    /// returns `Some(ViewNode)` if there is an overlay to show, `None` if not.
    pub fn register(
        &mut self,
        entity: crate::types::EntityId,
        f: impl Fn(&EntityView) -> Option<ViewNode> + Send + Sync + 'static,
    ) {
        self.contributors.push((entity, Box::new(f)));
    }

    /// Collect all overlays produced for the given entity view.
    pub fn overlays_for(&self, view: &EntityView) -> Vec<SceneOverlay> {
        self.contributors
            .iter()
            .filter_map(|(entity_id, f)| {
                f(view).map(|node| SceneOverlay {
                    contributor_entity: *entity_id,
                    layer: SceneLayer::Effects, // default; callers may wrap with specific layer
                    node,
                })
            })
            .collect()
    }

    /// Collect overlays with an explicit layer supplied by the caller.
    pub fn overlays_for_with_layer(
        &self,
        view: &EntityView,
        layer: SceneLayer,
    ) -> Vec<SceneOverlay> {
        self.contributors
            .iter()
            .filter_map(|(entity_id, f)| {
                f(view).map(|node| SceneOverlay {
                    contributor_entity: *entity_id,
                    layer,
                    node,
                })
            })
            .collect()
    }

    pub fn contributor_count(&self) -> usize {
        self.contributors.len()
    }
}

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

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::entity_state::EntityState;
    use crate::trait_data::*;
    use crate::types::{EntityTypeId, SeatId, PoolId};

    const HERO_TYPE: EntityTypeId = EntityTypeId::from_raw(100);

    fn make_hero_view(id: u64) -> EntityView {
        use std::sync::Arc;
        EntityView {
            id: crate::types::EntityId::from_raw(id),
            named: Some(Arc::new(NamedData::new("Thorin").with_icon("hero:thorin"))),
            featured: Some(FeaturedData::new()),
            ownable: Some(OwnableData::owned_by(SeatId::from_raw(1))),
            quantified: Some(Arc::new(QuantifiedData::new().with_pool(
                ResourcePool::depletable(
                    PoolId::new("test:counter", "Counter"), 18, 20
                )
            ))),
            activatable: Some(Arc::new(ActivatableData::new("combat", "Swing Sword"))),
            ..EntityView::default()
        }
    }

    // ---- StyleTokens ----

    #[test]
    fn style_tokens_get_by_key() {
        let tokens = StyleTokens::new()
            .with_token("color.primary", "#ff0000")
            .with_token("color.secondary", "#00ff00");
        assert_eq!(tokens.get("color.primary"), Some("#ff0000"));
        assert_eq!(tokens.get("color.secondary"), Some("#00ff00"));
        assert_eq!(tokens.get("color.missing"), None);
    }

    // ---- ViewNode construction ----

    #[test]
    fn view_node_label_and_column() {
        let node = ViewNode::column(vec![
            ViewNode::label("Name"),
            ViewNode::label("Counter: 18/20"),
        ]);
        match node {
            ViewNode::Stack { direction, children } => {
                assert_eq!(direction, Direction::Column);
                assert_eq!(children.len(), 2);
            }
            _ => panic!("expected Stack"),
        }
    }

    #[test]
    fn view_node_progress_bar() {
        let counter_pool = PoolId::new("test:counter", "Counter");
        let node = ViewNode::ProgressBar {
            value: FieldRef::QuantifiedPool { pool: counter_pool.clone(), field: PoolField::Current },
            max: FieldRef::QuantifiedPool { pool: counter_pool, field: PoolField::Max },
            style_token: None,
        };
        assert!(matches!(node, ViewNode::ProgressBar { .. }));
    }

    #[test]
    fn view_node_boxes() {
        let node = ViewNode::Boxes { filled: 2, total: 4, style_token: None };
        match node {
            ViewNode::Boxes { filled, total, .. } => {
                assert_eq!(filled, 2);
                assert_eq!(total, 4);
            }
            _ => panic!("expected Boxes"),
        }
    }

    #[test]
    fn view_node_conditional() {
        let node = ViewNode::Conditional {
            condition: ViewCondition::IsRevealed,
            then_node: Box::new(ViewNode::label("Face Up")),
            else_node: Some(Box::new(ViewNode::label("Face Down"))),
        };
        match node {
            ViewNode::Conditional { condition, .. } => {
                assert!(matches!(condition, ViewCondition::IsRevealed));
            }
            _ => panic!("expected Conditional"),
        }
    }

    // ---- ViewTemplate ----

    #[test]
    fn view_template_renders_hero_sheet() {
        let template = ViewTemplate::new(HERO_TYPE, "Hero Sheet", |view| {
            let name = view.named.as_ref()
                .map(|n| n.display_name.clone())
                .unwrap_or_else(|| "Unknown".into());
            let counter_pool = PoolId::new("test:counter", "Counter");
            let hp = ViewNode::ProgressBar {
                value: FieldRef::QuantifiedPool { pool: counter_pool.clone(), field: PoolField::Current },
                max: FieldRef::QuantifiedPool { pool: counter_pool, field: PoolField::Max },
                style_token: Some("color.hp".into()),
            };

            ViewNode::column(vec![
                ViewNode::label(name),
                hp,
                ViewNode::row(vec![
                    ViewNode::Button {
                        label: "Attack".into(),
                        entity: view.id,
                    }
                ]),
            ])
        });

        let view = make_hero_view(1);
        let node = template.render(&view);
        match &node {
            ViewNode::Stack { direction, children } => {
                assert_eq!(*direction, Direction::Column);
                assert_eq!(children.len(), 3);
                // First child: label with name
                assert!(matches!(&children[0], ViewNode::Label { text: TextSource::Static(ref s), .. } if s == "Thorin"));
                // Second child: progress bar for HP
                assert!(matches!(&children[1], ViewNode::ProgressBar { .. }));
            }
            _ => panic!("expected Stack node, got {:?}", node),
        }
    }

    #[test]
    fn view_template_registered_and_rendered_via_registry() {
        let mut store = EntityState::new();
        let id = store.spawn(HERO_TYPE)
            .named(NamedData::new("Hero"))
            .featured(FeaturedData::new())
            .build();

        let mut registry = ViewRegistry::new();
        let generic = GenericWidgetRegistry::new();

        registry.register(ViewTemplate::new(HERO_TYPE, "Hero Sheet", |view| {
            ViewNode::label(
                view.named.as_ref()
                    .map(|n| n.display_name.as_str())
                    .unwrap_or("?")
            )
        }));

        let view = EntityView {
            id,
            named: Some(std::sync::Arc::new(NamedData::new("Hero"))),
            featured: Some(FeaturedData::new()),
            ..EntityView::default()
        };

        let node = registry.render(&view, &store, &generic);
        assert!(matches!(node, ViewNode::Label { text: TextSource::Static(ref s), .. } if s == "Hero"));
    }

    #[test]
    fn view_registry_falls_back_to_generic_when_no_template() {
        let mut store = EntityState::new();
        let id = store.spawn(EntityTypeId::from_raw(999)).build();

        let registry = ViewRegistry::new();
        let mut generic = GenericWidgetRegistry::new();
        generic.register_trait_widget(15 /* NAMED bit */, |view| {
            if let Some(n) = &view.named {
                vec![SlottedHint::new(
                    Slot::Primary,
                    ViewNode::label(n.display_name.clone()),
                    100,
                )]
            } else {
                vec![]
            }
        });

        let view = EntityView {
            id,
            named: Some(std::sync::Arc::new(NamedData::new("Generic Entity"))),
            featured: Some(FeaturedData::new()),
            ..EntityView::default()
        };

        let node = registry.render(&view, &store, &generic);
        // Should be generic composition: Stack(Column) with Primary section
        assert!(!node.is_empty(), "should produce non-empty node from generic");
    }

    // ---- GenericWidgetRegistry ----

    #[test]
    fn generic_registry_collects_hints_from_multiple_traits() {
        let mut registry = GenericWidgetRegistry::new();

        registry.register_trait_widget(15 /* NAMED */, |view| {
            if let Some(n) = &view.named {
                vec![SlottedHint::new(Slot::Primary, ViewNode::label(&n.display_name), 100)]
            } else {
                vec![]
            }
        });

        registry.register_trait_widget(9 /* QUANTIFIED */, |view| {
            if let Some(q) = &view.quantified {
                q.pools.iter().map(|p| SlottedHint::new(
                    Slot::Indicator,
                    ViewNode::ProgressBar {
                        value: FieldRef::QuantifiedPool { pool: p.id.clone(), field: PoolField::Current },
                        max: FieldRef::QuantifiedPool { pool: p.id.clone(), field: PoolField::Max },
                        style_token: None,
                    },
                    50,
                )).collect()
            } else {
                vec![]
            }
        });

        let view = make_hero_view(1);
        let hints = registry.hints_for(&view);

        assert!(!hints.is_empty(), "should have hints from both trait widgets");
        let primary_count = hints.iter().filter(|h| h.slot == Slot::Primary).count();
        let indicator_count = hints.iter().filter(|h| h.slot == Slot::Indicator).count();
        assert_eq!(primary_count, 1, "one primary hint from featured");
        assert_eq!(indicator_count, 1, "one indicator from quantified Counter pool");
    }

    #[test]
    fn generic_registry_compose_node_produces_column() {
        let mut registry = GenericWidgetRegistry::new();

        registry.register_trait_widget(15 /* NAMED */, |view| {
            if let Some(n) = &view.named {
                vec![SlottedHint::new(Slot::Primary, ViewNode::label(&n.display_name), 100)]
            } else {
                vec![]
            }
        });

        registry.register_trait_widget(10 /* ACTIVATABLE */, |view| {
            if view.activatable.is_some() {
                vec![SlottedHint::new(
                    Slot::Action,
                    ViewNode::Button {
                        label: "Use".into(),
                        entity: view.id,
                    },
                    50,
                )]
            } else {
                vec![]
            }
        });

        let view = make_hero_view(1);
        let node = registry.compose_node(&view);

        match &node {
            ViewNode::Stack { direction: Direction::Column, children } => {
                assert!(children.len() >= 2, "should have primary + action sections");
            }
            _ => panic!("expected column stack, got {:?}", node),
        }
    }

    #[test]
    fn generic_registry_empty_returns_empty_node() {
        let registry = GenericWidgetRegistry::new();
        let view = EntityView { id: crate::types::EntityId::from_raw(0), ..EntityView::default() };
        let node = registry.compose_node(&view);
        assert!(matches!(node, ViewNode::Empty));
    }

    #[test]
    fn hint_slot_ordering_primary_before_indicator() {
        assert!(Slot::Primary < Slot::Indicator);
        assert!(Slot::Indicator < Slot::Overlay);
        assert!(Slot::Overlay < Slot::Action);
        assert!(Slot::Action < Slot::Detail);
    }

    #[test]
    fn tabs_node_has_matching_labels_and_children() {
        let node = ViewNode::Tabs {
            labels: vec!["Stats".into(), "Inventory".into()],
            children: vec![
                ViewNode::label("STR 16"),
                ViewNode::label("Sword x1"),
            ],
        };
        match node {
            ViewNode::Tabs { labels, children } => {
                assert_eq!(labels.len(), 2);
                assert_eq!(children.len(), 2);
            }
            _ => panic!("expected Tabs"),
        }
    }

    #[test]
    fn ttrpg_hero_view_template_end_to_end() {
        // Full end-to-end test: hero entity → template → verify ViewNode tree structure.
        const HERO_T: EntityTypeId = EntityTypeId::from_raw(200);

        let mut store = EntityState::new();
        let hero_id = store.spawn(HERO_T)
            .named(NamedData::new("Thorin"))
            .featured(FeaturedData::new())
            .ownable(OwnableData::owned_by(SeatId::from_raw(1)))
            .quantified(QuantifiedData::new().with_pool(
                ResourcePool::depletable(PoolId::new("test:counter", "Counter"), 18, 20)
            ))
            .activatable(ActivatableData::new("combat", "Swing Sword"))
            .build();

        let mut view_registry = ViewRegistry::new();
        let generic_registry = GenericWidgetRegistry::new();

        // Register a TTRPG character sheet template
        view_registry.register(ViewTemplate::new(HERO_T, "TTRPG Character Sheet", |view| {
            let name = view.named.as_ref()
                .map(|n| n.display_name.clone())
                .unwrap_or_default();

            let counter_pool = PoolId::new("test:counter", "Counter");
            let hp_node = ViewNode::ProgressBar {
                value: FieldRef::QuantifiedPool { pool: counter_pool.clone(), field: PoolField::Current },
                max: FieldRef::QuantifiedPool { pool: counter_pool, field: PoolField::Max },
                style_token: Some("color.hp".into()),
            };

            let actions: Vec<ViewNode> = view.activatable.iter()
                .map(|a| ViewNode::Button {
                    label: a.description.clone(),
                    entity: view.id,
                })
                .collect();

            ViewNode::Tabs {
                labels: vec!["Main".into(), "Actions".into()],
                children: vec![
                    ViewNode::column(vec![ViewNode::label(name), hp_node]),
                    ViewNode::column(actions),
                ],
            }
        }));

        let view = EntityView {
            id: hero_id,
            named: Some(std::sync::Arc::new(NamedData::new("Thorin"))),
            featured: Some(FeaturedData::new()),
            ownable: Some(OwnableData::owned_by(SeatId::from_raw(1))),
            quantified: Some(std::sync::Arc::new(QuantifiedData::new().with_pool(
                ResourcePool::depletable(PoolId::new("test:counter", "Counter"), 18, 20)
            ))),
            activatable: Some(std::sync::Arc::new(ActivatableData::new("combat", "Swing Sword"))),
            ..EntityView::default()
        };

        let rendered = view_registry.render(&view, &store, &generic_registry);

        // Verify the tree structure
        match &rendered {
            ViewNode::Tabs { labels, children } => {
                assert_eq!(labels, &["Main", "Actions"]);
                assert_eq!(children.len(), 2);

                // First tab: column with name label + HP bar
                match &children[0] {
                    ViewNode::Stack { direction: Direction::Column, children: tab_children } => {
                        assert!(tab_children.len() >= 2);
                        assert!(matches!(&tab_children[0], ViewNode::Label { text: TextSource::Static(ref s), .. } if s == "Thorin"));
                        assert!(matches!(&tab_children[1], ViewNode::ProgressBar { .. }));
                    }
                    _ => panic!("first tab should be a column"),
                }

                // Second tab: column with action button
                match &children[1] {
                    ViewNode::Stack { direction: Direction::Column, children: action_children } => {
                        assert_eq!(action_children.len(), 1);
                        assert!(matches!(
                            &action_children[0],
                            ViewNode::Button { label, .. } if label == "Swing Sword"
                        ));
                    }
                    _ => panic!("second tab should be a column"),
                }
            }
            _ => panic!("expected Tabs node, got {:?}", rendered),
        }
    }

    // ---- SceneContributorRegistry ----

    #[test]
    fn scene_contributor_registry_starts_empty() {
        let reg = SceneContributorRegistry::new();
        assert_eq!(reg.contributor_count(), 0);
    }

    #[test]
    fn scene_contributor_no_match_returns_empty() {
        let reg = SceneContributorRegistry::new();
        let view = EntityView { id: crate::types::EntityId::from_raw(1), ..Default::default() };
        let overlays = reg.overlays_for(&view);
        assert!(overlays.is_empty());
    }

    #[test]
    fn scene_contributor_registered_fn_called_per_view() {
        use crate::types::EntityId;

        let mut reg = SceneContributorRegistry::new();
        reg.register(EntityId::from_raw(10), |view| {
            view.named.as_ref().map(|n| ViewNode::label(format!("Counter: {}", n.display_name)))
        });

        // Entity WITH named data — should produce an overlay
        let view_with_featured = EntityView {
            id: EntityId::from_raw(1),
            named: Some(std::sync::Arc::new(NamedData::new("Thorin"))),
            featured: Some(FeaturedData::new()),
            ..Default::default()
        };
        let overlays = reg.overlays_for(&view_with_featured);
        assert_eq!(overlays.len(), 1);
        assert_eq!(overlays[0].contributor_entity, EntityId::from_raw(10));
        match &overlays[0].node {
            ViewNode::Label { text: TextSource::Static(s), .. } => assert!(s.contains("Thorin")),
            _ => panic!("expected label"),
        }

        // Entity WITHOUT featured data — None, so no overlay
        let view_without = EntityView { id: EntityId::from_raw(2), ..Default::default() };
        let no_overlays = reg.overlays_for(&view_without);
        assert!(no_overlays.is_empty());
    }

    #[test]
    fn scene_contributor_with_explicit_layer() {
        use crate::types::EntityId;

        let mut reg = SceneContributorRegistry::new();
        reg.register(EntityId::from_raw(5), |_view| {
            Some(ViewNode::label("rain"))
        });

        let view = EntityView { id: EntityId::from_raw(1), ..Default::default() };
        let overlays = reg.overlays_for_with_layer(&view, SceneLayer::Weather);
        assert_eq!(overlays.len(), 1);
        assert_eq!(overlays[0].layer, SceneLayer::Weather);
    }

    // -----------------------------------------------------------------------
    // UC-29 Phase C — ViewNode IR additions
    // -----------------------------------------------------------------------

    #[test]
    fn uc_29_c_section_constructs_and_matches() {
        let node = ViewNode::Section {
            title: "Equipment".into(),
            body: Box::new(ViewNode::label("(empty)")),
        };
        match node {
            ViewNode::Section { title, body } => {
                assert_eq!(title, "Equipment");
                assert!(matches!(*body, ViewNode::Label { .. }));
            }
            other => panic!("expected Section, got {other:?}"),
        }
    }

    #[test]
    fn uc_29_c_for_each_child_constructs_and_matches() {
        let node = ViewNode::ForEachChild {
            container: ContainerRef::Self_,
            item_template: Box::new(ViewNode::label("item")),
            empty_node: Some(Box::new(ViewNode::label("(empty)"))),
        };
        match node {
            ViewNode::ForEachChild { container, item_template, empty_node } => {
                assert_eq!(container, ContainerRef::Self_);
                assert!(matches!(*item_template, ViewNode::Label { .. }));
                assert!(empty_node.is_some());
            }
            other => panic!("expected ForEachChild, got {other:?}"),
        }
    }

    #[test]
    fn uc_29_c_for_each_child_empty_node_optional() {
        let node = ViewNode::ForEachChild {
            container: ContainerRef::Self_,
            item_template: Box::new(ViewNode::label("item")),
            empty_node: None,
        };
        match node {
            ViewNode::ForEachChild { empty_node, .. } => assert!(empty_node.is_none()),
            other => panic!("expected ForEachChild, got {other:?}"),
        }
    }

    #[test]
    fn uc_29_c_drop_zone_constructs_and_matches() {
        let backpack = EntityId::from_raw(42);
        let node = ViewNode::DropZone {
            container_entity: backpack,
            body: Box::new(ViewNode::label("Backpack")),
        };
        match node {
            ViewNode::DropZone { container_entity, body } => {
                assert_eq!(container_entity, backpack);
                assert!(matches!(*body, ViewNode::Label { .. }));
            }
            other => panic!("expected DropZone, got {other:?}"),
        }
    }

    // -----------------------------------------------------------------------
    // UC-29-Pre — ContainerRef::ChildByTag
    // -----------------------------------------------------------------------

    #[test]
    fn uc_29_pre_container_ref_child_by_tag_constructs() {
        let r = ContainerRef::ChildByTag("dnd5e:slot:inventory".into());
        match r {
            ContainerRef::ChildByTag(tag) => {
                assert_eq!(tag, "dnd5e:slot:inventory");
            }
            other => panic!("expected ChildByTag, got {other:?}"),
        }
    }

    #[test]
    fn uc_29_pre_container_ref_self_still_exists() {
        // Regression guard: adding ChildByTag must not remove Self_.
        let r = ContainerRef::Self_;
        assert_eq!(r, ContainerRef::Self_);
    }
}
