"""
DAG-Based Structure Generator for Google Chat Cards

Uses the ModuleWrapper's NetworkX DAG to generate random but VALID
card structures. Unlike hardcoded generators, this guarantees:

1. All component nesting is valid (parent can actually contain child)
2. Structures can be generated from any starting component
3. DSL notation is automatically produced
4. Structures are validated against the real component hierarchy

Usage:
    from gchat.testing.dag_structure_generator import DAGStructureGenerator

    gen = DAGStructureGenerator()

    # Generate random valid structure
    result = gen.generate_random_structure(root="Section", max_depth=3)
    print(result["dsl"])  # §[δ, Ƀ[ᵬ×2]]
    print(result["components"])  # ['Section', 'DecoratedText', 'ButtonList', 'Button']

    # Generate card JSON from structure
    card = gen.structure_to_card(result)

    # Generate multiple random cards
    cards = gen.generate_random_cards(count=5)
"""

import random
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Set, Tuple

from config.enhanced_logging import setup_logger

logger = setup_logger()


# =============================================================================
# PARAMETER SCHEMA EXTRACTION
# =============================================================================


@dataclass
class FieldInfo:
    """Information about a component field/parameter."""

    name: str
    field_type: str
    has_default: bool
    default_value: Any
    is_required: bool
    is_optional: bool  # Optional[X] type
    generated_value: Any = None
    used_default: bool = True


@dataclass
class ComponentSchema:
    """Schema for a component including all its parameters."""

    name: str
    fields: Dict[str, FieldInfo] = field(default_factory=dict)
    json_key: str = ""  # e.g., "decoratedText" for DecoratedText


def extract_component_schema(component_class: Any) -> Optional[ComponentSchema]:
    """
    Extract field schema from a component class.

    Args:
        component_class: The dataclass component (e.g., DecoratedText)

    Returns:
        ComponentSchema with all field information
    """
    from dataclasses import MISSING

    if not hasattr(component_class, "__dataclass_fields__"):
        return None

    name = component_class.__name__
    schema = ComponentSchema(name=name)

    # Determine JSON key (camelCase of class name)
    # DecoratedText -> decoratedText
    json_key = name[0].lower() + name[1:]
    schema.json_key = json_key

    for field_name, field_obj in component_class.__dataclass_fields__.items():
        # Skip private fields
        if field_name.startswith("_"):
            continue

        # Determine type
        type_str = getattr(field_obj.type, "__name__", str(field_obj.type))
        is_optional = "Optional" in str(field_obj.type) or type_str == "Optional"

        # Determine default
        if field_obj.default is not MISSING:
            has_default = True
            default_value = field_obj.default
        elif field_obj.default_factory is not MISSING:
            has_default = True
            default_value = "factory()"
        else:
            has_default = False
            default_value = None

        is_required = not has_default and not is_optional

        schema.fields[field_name] = FieldInfo(
            name=field_name,
            field_type=type_str,
            has_default=has_default,
            default_value=default_value,
            is_required=is_required,
            is_optional=is_optional,
        )

    return schema


# =============================================================================
# PARAMETER VALUE GENERATORS
# =============================================================================

# Import material icons for rich test data
try:
    from gchat.material_icons import (
        MATERIAL_ICONS,
        SEMANTIC_ICONS,
        create_material_icon,
        is_valid_icon,
    )

    _MATERIAL_ICONS_AVAILABLE = True
except ImportError:
    MATERIAL_ICONS = frozenset()
    SEMANTIC_ICONS = {}
    _MATERIAL_ICONS_AVAILABLE = False

    def is_valid_icon(name: str) -> bool:
        return False

    def create_material_icon(name: str, fill: bool = False) -> dict:
        return {"materialIcon": {"name": name}}


# Curated list of useful material icons for testing
# These are common, visually distinct icons that render well
CURATED_MATERIAL_ICONS = [
    "star",
    "favorite",
    "check_circle",
    "home",
    "settings",
    "person",
    "email",
    "phone",
    "calendar_today",
    "schedule",
    "notifications",
    "shopping_cart",
    "search",
    "edit",
    "delete",
    "add",
    "remove",
    "arrow_forward",
    "arrow_back",
    "refresh",
    "cloud",
    "folder",
    "attach_file",
    "link",
    "share",
    "thumb_up",
    "thumb_down",
    "bookmark",
    "flag",
    "info",
    "warning",
    "error",
    "help",
    "lightbulb",
    "verified",
]

# Sample values by type for generating test data
SAMPLE_VALUES_BY_TYPE = {
    "str": [
        "Test text",
        "Sample content",
        "Hello world",
        "Generated value",
        "Lorem ipsum",
        "Quick test",
        "Status update",
        "Action required",
        "Review pending",
    ],
    "bool": [True, False],
    "int": [0, 1, 5, 10, 42, 100],
    "float": [0.0, 0.5, 1.0, 3.14],
    "HorizontalAlignment": ["START", "CENTER", "END"],
    "VerticalAlignment": ["TOP", "CENTER", "BOTTOM"],
    "ImageType": ["SQUARE", "CIRCLE"],
    "SelectionType": ["CHECK_BOX", "RADIO_BUTTON", "SWITCH", "DROPDOWN"],
    "BorderType": ["NO_BORDER", "STROKE"],
    "LoadIndicator": ["SPINNER", "NONE"],
    "ResponseType": ["TYPE_UNSPECIFIED", "NEW_MESSAGE", "UPDATE_MESSAGE"],
    "OpenAs": ["FULL_SIZE", "OVERLAY"],
    "OnClose": ["NOTHING", "RELOAD"],
}

# Known icons (legacy format for knownIcon field)
KNOWN_ICONS = [
    "STAR",
    "BOOKMARK",
    "DESCRIPTION",
    "EMAIL",
    "CLOCK",
    "CONFIRMATION_NUMBER",
    "NOTIFICATIONS",
    "PERSON",
    "PHONE",
    "FLIGHT_ARRIVAL",
    "FLIGHT_DEPARTURE",
    "HOTEL",
    "INVITE",
]

# Sample images for testing (define here for use in COMPONENT_DEFAULTS)
PARAM_SAMPLE_IMAGES = [
    "https://picsum.photos/id/1/200/200",
    "https://picsum.photos/id/20/200/200",
    "https://picsum.photos/id/42/200/200",
    "https://picsum.photos/id/60/200/200",
]

# Component-specific default values (based on typical usage)
COMPONENT_DEFAULTS = {
    "DecoratedText": {
        "text": ["Status update", "Item details", "Quick info", "Notification"],
        "top_label": ["Label", "Category", "Type", "Status"],
        "bottom_label": ["Updated recently", "Click for details", "More info"],
        "wrap_text": [True],
    },
    "TextParagraph": {
        "text": [
            "This is a paragraph of text content.",
            "Welcome to the generated card test.",
            "Sample paragraph for testing purposes.",
        ],
    },
    "Button": {
        "text": [
            "Submit",
            "Cancel",
            "Confirm",
            "Next",
            "Back",
            "View",
            "Edit",
            "Delete",
        ],
    },
    "Image": {
        "image_url": PARAM_SAMPLE_IMAGES,
        "alt_text": ["Test image", "Sample photo", "Generated image"],
    },
    "SelectionInput": {
        "label": ["Select option", "Choose item", "Pick one"],
        "name": ["selection_1", "dropdown_1", "choice_1"],
    },
    "DateTimePicker": {
        "label": ["Select date", "Choose time", "Pick date/time"],
        "name": ["datetime_1", "date_picker_1"],
    },
    "Grid": {
        "title": ["Grid View", "Items", "Gallery", "Options"],
        "column_count": [2, 3, 4],
    },
    "Chip": {
        "label": ["Tag 1", "Category", "Filter", "Option"],
    },
}


def generate_value_for_field(
    field_info: FieldInfo,
    component_name: Optional[str] = None,
    use_material_icons: bool = True,
) -> Tuple[Any, bool]:
    """
    Generate a test value for a field.

    Args:
        field_info: Field information
        component_name: Name of the component (for component-specific defaults)
        use_material_icons: Whether to use material design icons

    Returns:
        Tuple of (generated_value, used_default)
    """
    field_type = field_info.field_type
    field_name = field_info.name

    # Check for component-specific defaults first
    if component_name and component_name in COMPONENT_DEFAULTS:
        comp_defaults = COMPONENT_DEFAULTS[component_name]
        if field_name in comp_defaults:
            return random.choice(comp_defaults[field_name]), False

    # Decide whether to use default (30% chance if available)
    if field_info.has_default and random.random() < 0.3:
        return field_info.default_value, True

    # Generate based on type
    if field_type in SAMPLE_VALUES_BY_TYPE:
        return random.choice(SAMPLE_VALUES_BY_TYPE[field_type]), False

    # String fields
    if "str" in field_type.lower():
        return random.choice(SAMPLE_VALUES_BY_TYPE["str"]), False

    # Boolean fields
    if "bool" in field_type.lower():
        return random.choice([True, False]), False

    # Integer fields
    if "int" in field_type.lower():
        return random.randint(1, 10), False

    # Icon fields - use material icons if available
    if "Icon" in field_type or "icon" in field_name.lower():
        if use_material_icons and _MATERIAL_ICONS_AVAILABLE:
            # Use materialIcon format with curated icons via helper
            icon_name = random.choice(CURATED_MATERIAL_ICONS)
            # create_material_icon returns {"materialIcon": {"name": "..."}}
            return create_material_icon(icon_name), False
        else:
            # Fallback to knownIcon format
            return {"knownIcon": random.choice(KNOWN_ICONS)}, False

    # List fields
    if "List" in field_type:
        return [], False  # Empty list as default

    # Optional fields
    if field_info.is_optional:
        # 50% chance to provide value for optional fields
        if random.random() < 0.5:
            return None, True

    # Default: use the default value if available
    if field_info.has_default:
        return field_info.default_value, True

    return None, True


def generate_component_test_values(
    component_name: str,
    schema: Optional[ComponentSchema] = None,
    fill_all: bool = False,
) -> Dict[str, Any]:
    """
    Generate a complete set of test values for a component.

    Args:
        component_name: Name of the component
        schema: Optional pre-extracted schema
        fill_all: If True, fill all fields; otherwise use probabilistic filling

    Returns:
        Dict of field name to generated value
    """
    if schema is None:
        return {}

    values = {}
    for field_name, field_info in schema.fields.items():
        if fill_all or field_info.is_required or random.random() < 0.5:
            value, _ = generate_value_for_field(field_info, component_name)
            if value is not None:
                values[field_name] = value

    return values


# =============================================================================
# CONFIGURATION
# =============================================================================


@dataclass
class DAGGeneratorConfig:
    """Configuration for DAG-based structure generation."""

    # Structure constraints
    max_depth: int = 3
    max_children_per_node: int = 4
    min_children_per_node: int = 1

    # Component preferences (boost probability)
    preferred_components: List[str] = field(
        default_factory=lambda: [
            "DecoratedText",
            "ButtonList",
            "Button",
            "TextParagraph",
        ]
    )

    # Components to avoid (reduce probability)
    avoid_components: List[str] = field(
        default_factory=lambda: [
            "Carousel",
            "CarouselCard",
            "NestedWidget",  # Complex carousel
            "Columns",
            "Column",  # Complex multi-column
            "CollapseControl",  # Requires special handling
        ]
    )

    # Required components (must appear somewhere in structure)
    required_components: List[str] = field(default_factory=list)

    # Probability weights
    prefer_weight: float = 2.0  # Multiplier for preferred components
    avoid_weight: float = 0.2  # Multiplier for avoided components

    # Content generation
    include_content: bool = True
    webhook_url: Optional[str] = None
    test_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])


@dataclass
class GeneratedStructure:
    """Result of structure generation."""

    root: str
    components: List[str]
    dsl: str
    tree: Dict[str, Any]
    depth: int
    is_valid: bool
    validation_issues: List[str] = field(default_factory=list)


# =============================================================================
# WIDGET HIERARCHY (Canonical containment rules)
# =============================================================================

# This defines what containers can hold what children
# This is the SSoT for valid card structures
# Matches CUSTOM_CHAT_API_RELATIONSHIPS from card_framework_wrapper.py
WIDGET_HIERARCHY = {
    # Card-level containers
    "Card": ["Section", "CardHeader", "CardFixedFooter", "Carousel"],
    "CardHeader": [],  # Just displays title/subtitle
    "CardFixedFooter": ["Button"],  # primaryButton, secondaryButton
    # Section is the main container for widgets
    "Section": [
        "DecoratedText",
        "TextParagraph",
        "Image",
        "Divider",
        "ButtonList",
        "Grid",
        "Columns",
        "SelectionInput",
        "DateTimePicker",
        "ChipList",
        "CollapseControl",
    ],
    # CollapseControl for expandable sections
    "CollapseControl": ["Button"],  # expandButton, collapseButton
    # Widget internals
    "DecoratedText": ["Icon", "Button", "SwitchControl"],
    "ButtonList": ["Button"],
    "Button": ["Icon"],
    "Grid": ["GridItem"],
    "GridItem": ["Image"],
    "Columns": ["Column"],
    "Column": ["DecoratedText", "TextParagraph", "Image", "ButtonList"],
    "ChipList": ["Chip"],
    "Chip": ["Icon"],
    # Carousel (Custom Google Chat API component)
    # Note: NestedWidget only supports textParagraph, buttonList, image (NOT decoratedText!)
    "Carousel": ["CarouselCard"],
    "CarouselCard": ["NestedWidget"],
    "NestedWidget": [
        "TextParagraph",
        "ButtonList",
        "Image",
    ],  # Limited set per API docs
}

# Leaf components (no children)
LEAF_COMPONENTS = {
    "TextParagraph",
    "Image",
    "Divider",
    "Icon",
    "SwitchControl",
    "SelectionInput",
    "DateTimePicker",
    "Widget",
    "CardHeader",
}

# Internal types that are NOT renderable widgets (exclude from generation)
# These come from type annotations but aren't actual widget containers
INTERNAL_TYPES = {
    # Enums and types
    "Type",
    "Color",
    "OnClick",
    "Action",
    "OverflowMenu",
    "HorizontalAlignment",
    "VerticalAlignment",
    "HorizontalSizeStyle",
    "ControlType",
    "DisplayStyle",
    "LoadIndicator",
    "MatchedUrl",
    "ResponseType",
    "Interaction",
    "BorderType",
    "ImageType",
    "GridItemLayout",
    "Layout",
    "DividerStyle",
    "WrapStyle",
    "OpenAs",
    "OnClose",
    "UpdatedWidget",
    "SelectionType",
    "PlatformDataSource",
    "CommonDataSource",
    # Internal structures
    "AttachmentDataRef",
    "CustomEmojiPayload",
    "SuggestionItem",
    "CollapseControl",
    "ImageComponent",
}


# =============================================================================
# SAMPLE CONTENT POOLS
# =============================================================================

SAMPLE_TEXTS = [
    "Status update available",
    "Task completed successfully",
    "New notification received",
    "Action required",
    "Review pending items",
    "System check passed",
]

SAMPLE_BUTTON_LABELS = [
    "View Details",
    "Confirm",
    "Cancel",
    "Submit",
    "Next",
    "Previous",
    "Approve",
    "Reject",
]

SAMPLE_ICONS = [
    "STAR",
    "BOOKMARK",
    "DESCRIPTION",
    "EMAIL",
    "CLOCK",
    "CONFIRMATION_NUMBER",
    "NOTIFICATIONS",
]

SAMPLE_IMAGES = [
    "https://picsum.photos/id/1/200/200",
    "https://picsum.photos/id/20/200/200",
    "https://picsum.photos/id/42/200/200",
]


# =============================================================================
# DAG STRUCTURE GENERATOR
# =============================================================================


@dataclass
class ParameterizedNode:
    """A node in the structure tree with parameter values."""

    name: str
    symbol: str
    depth: int
    children: List["ParameterizedNode"] = field(default_factory=list)
    schema: Optional[ComponentSchema] = None
    parameters: Dict[str, FieldInfo] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary representation."""
        return {
            "name": self.name,
            "symbol": self.symbol,
            "depth": self.depth,
            "children": [c.to_dict() for c in self.children],
            "parameters": (
                {
                    name: {
                        "type": f.field_type,
                        "value": f.generated_value,
                        "used_default": f.used_default,
                        "is_required": f.is_required,
                    }
                    for name, f in self.parameters.items()
                }
                if self.parameters
                else {}
            ),
        }


@dataclass
class ParameterizedStructure:
    """Generated structure with full parameter information."""

    root: str
    components: List[str]
    dsl: str
    tree: ParameterizedNode
    depth: int
    is_valid: bool
    validation_issues: List[str] = field(default_factory=list)
    total_parameters: int = 0
    parameters_with_values: int = 0
    parameters_using_defaults: int = 0


class DAGStructureGenerator:
    """
    Generates random valid card structures using the ModuleWrapper's DAG.

    Key features:
    - Uses actual component containment rules from the DAG
    - Generates valid DSL notation
    - Can target specific component combinations
    - Validates all generated structures
    - Can generate parameter values for each component field
    """

    def __init__(self, config: Optional[DAGGeneratorConfig] = None):
        self.config = config or DAGGeneratorConfig()
        self._wrapper = None
        self._graph_built = False
        self._schema_cache: Dict[str, ComponentSchema] = {}

    def _get_wrapper(self):
        """Get ModuleWrapper singleton with graph built."""
        if self._wrapper is None:
            from gchat.card_framework_wrapper import get_card_framework_wrapper

            self._wrapper = get_card_framework_wrapper()

        # Ensure graph is built with widget hierarchy
        if not self._graph_built:
            self._wrapper.build_relationship_graph()
            self._wrapper.add_relationships_to_graph(
                WIDGET_HIERARCHY, edge_type="widget_contains"
            )
            self._graph_built = True

        return self._wrapper

    # =========================================================================
    # STRUCTURE GENERATION
    # =========================================================================

    def get_valid_children(self, component: str) -> List[str]:
        """Get list of components that can be direct children of the given component."""
        # Use ONLY the canonical hierarchy - don't include DAG type annotations
        # The DAG includes internal types (OnClick, Color, etc.) that aren't widgets
        if component in WIDGET_HIERARCHY:
            children = WIDGET_HIERARCHY[component]
        else:
            # Fallback to DAG for components not in our hierarchy
            wrapper = self._get_wrapper()
            children = wrapper.get_children(component)

        # Filter out internal types that aren't renderable widgets
        return [c for c in children if c not in INTERNAL_TYPES]

    def is_leaf(self, component: str) -> bool:
        """Check if component is a leaf (has no valid children)."""
        if component in LEAF_COMPONENTS:
            return True
        return len(self.get_valid_children(component)) == 0

    def get_component_schema(self, component_name: str) -> Optional[ComponentSchema]:
        """
        Get the parameter schema for a component.

        Args:
            component_name: Name of the component (e.g., "DecoratedText")

        Returns:
            ComponentSchema with field information, or None if not found
        """
        # Check cache first
        if component_name in self._schema_cache:
            return self._schema_cache[component_name]

        wrapper = self._get_wrapper()

        # Find the component class
        for path, comp in wrapper.components.items():
            if comp.name == component_name:
                cls = comp.obj
                if cls:
                    schema = extract_component_schema(cls)
                    if schema:
                        self._schema_cache[component_name] = schema
                        return schema
                break

        return None

    def generate_parameters_for_component(
        self,
        component_name: str,
        fill_required: bool = True,
        fill_optional_probability: float = 0.3,
    ) -> Dict[str, FieldInfo]:
        """
        Generate parameter values for a component.

        Args:
            component_name: Name of the component
            fill_required: Always fill required fields
            fill_optional_probability: Probability of filling optional fields

        Returns:
            Dict of field name to FieldInfo with generated values
        """
        schema = self.get_component_schema(component_name)
        if not schema:
            return {}

        parameters = {}
        for field_name, field_info in schema.fields.items():
            # Copy the field info
            param = FieldInfo(
                name=field_info.name,
                field_type=field_info.field_type,
                has_default=field_info.has_default,
                default_value=field_info.default_value,
                is_required=field_info.is_required,
                is_optional=field_info.is_optional,
            )

            # Decide whether to generate a value
            should_fill = (fill_required and param.is_required) or (
                param.is_optional and random.random() < fill_optional_probability
            )

            if should_fill:
                value, used_default = generate_value_for_field(param, component_name)
                param.generated_value = value
                param.used_default = used_default
            else:
                param.generated_value = param.default_value
                param.used_default = True

            parameters[field_name] = param

        return parameters

    def select_children(
        self,
        parent: str,
        available: List[str],
        required: Optional[Set[str]] = None,
    ) -> List[str]:
        """
        Select children for a parent component using weighted random selection.

        Args:
            parent: Parent component name
            available: List of valid child components
            required: Components that must be included if possible

        Returns:
            List of selected child component names
        """
        if not available:
            return []

        required = required or set()
        config = self.config

        # Build weighted list
        weighted = []
        for comp in available:
            weight = 1.0

            # Apply preference weights
            if comp in config.preferred_components:
                weight *= config.prefer_weight
            if comp in config.avoid_components:
                weight *= config.avoid_weight
            if comp in required:
                weight *= 3.0  # Strong boost for required

            weighted.append((comp, weight))

        # Determine number of children
        num_children = random.randint(
            config.min_children_per_node,
            min(config.max_children_per_node, len(available)),
        )

        # Weighted selection without replacement
        selected = []
        remaining = weighted.copy()

        # First, try to include required components
        for comp in list(required):
            matching = [w for w in remaining if w[0] == comp]
            if matching:
                selected.append(matching[0][0])
                remaining = [w for w in remaining if w[0] != comp]
                required.discard(comp)

        # Then fill remaining slots with weighted random
        while len(selected) < num_children and remaining:
            total = sum(w for _, w in remaining)
            if total == 0:
                break

            r = random.uniform(0, total)
            cumulative = 0
            for i, (comp, weight) in enumerate(remaining):
                cumulative += weight
                if r <= cumulative:
                    selected.append(comp)
                    remaining.pop(i)
                    break

        return selected

    def generate_tree(
        self,
        root: str,
        depth: int = 0,
        required: Optional[Set[str]] = None,
    ) -> Dict[str, Any]:
        """
        Recursively generate a valid component tree.

        Args:
            root: Root component to start from
            depth: Current depth (for limiting recursion)
            required: Components that should appear somewhere in the tree

        Returns:
            Tree dict with 'name', 'children', 'symbol', 'depth'
        """
        wrapper = self._get_wrapper()
        required = required or set()

        # Get symbol for this component
        symbol = wrapper.symbol_mapping.get(root, root[0])

        node = {
            "name": root,
            "symbol": symbol,
            "depth": depth,
            "children": [],
        }

        # Check if we should stop
        if depth >= self.config.max_depth or self.is_leaf(root):
            return node

        # Get valid children
        valid_children = self.get_valid_children(root)
        if not valid_children:
            return node

        # Select children (pass down required components)
        selected = self.select_children(root, valid_children, required)

        # Recursively generate child subtrees
        remaining_required = required.copy()
        for child_name in selected:
            remaining_required.discard(child_name)
            child_node = self.generate_tree(
                child_name,
                depth + 1,
                remaining_required,
            )
            node["children"].append(child_node)

        return node

    def generate_parameterized_tree(
        self,
        root: str,
        depth: int = 0,
        required: Optional[Set[str]] = None,
        fill_optional_probability: float = 0.3,
    ) -> ParameterizedNode:
        """
        Generate a tree with parameter values for each component.

        Args:
            root: Root component to start from
            depth: Current depth
            required: Required components
            fill_optional_probability: Probability of filling optional fields

        Returns:
            ParameterizedNode with parameters filled in
        """
        wrapper = self._get_wrapper()
        required = required or set()

        # Get symbol
        symbol = wrapper.symbol_mapping.get(root, root[0])

        # Get schema and generate parameters
        schema = self.get_component_schema(root)
        parameters = self.generate_parameters_for_component(
            root,
            fill_required=True,
            fill_optional_probability=fill_optional_probability,
        )

        node = ParameterizedNode(
            name=root,
            symbol=symbol,
            depth=depth,
            children=[],
            schema=schema,
            parameters=parameters,
        )

        # Check if we should stop
        if depth >= self.config.max_depth or self.is_leaf(root):
            return node

        # Get valid children
        valid_children = self.get_valid_children(root)
        if not valid_children:
            return node

        # Select children
        selected = self.select_children(root, valid_children, required)

        # Recursively generate child subtrees
        remaining_required = required.copy()
        for child_name in selected:
            remaining_required.discard(child_name)
            child_node = self.generate_parameterized_tree(
                child_name,
                depth + 1,
                remaining_required,
                fill_optional_probability,
            )
            node.children.append(child_node)

        return node

    def generate_parameterized_structure(
        self,
        root: str = "Section",
        required_components: Optional[List[str]] = None,
        fill_optional_probability: float = 0.3,
    ) -> ParameterizedStructure:
        """
        Generate a structure with full parameter information.

        Args:
            root: Root component
            required_components: Required components
            fill_optional_probability: Probability of filling optional fields

        Returns:
            ParameterizedStructure with parameter details
        """
        required = set(required_components or [])

        # Generate parameterized tree
        tree = self.generate_parameterized_tree(
            root,
            depth=0,
            required=required,
            fill_optional_probability=fill_optional_probability,
        )

        # Convert to DSL (using dict format for compatibility)
        def tree_to_dict(node: ParameterizedNode) -> Dict[str, Any]:
            return {
                "name": node.name,
                "symbol": node.symbol,
                "depth": node.depth,
                "children": [tree_to_dict(c) for c in node.children],
            }

        dsl = self.tree_to_dsl(tree_to_dict(tree))

        # Collect component list and parameter stats
        components = []
        total_params = 0
        params_with_values = 0
        params_using_defaults = 0

        def collect_stats(node: ParameterizedNode):
            nonlocal total_params, params_with_values, params_using_defaults
            components.append(node.name)
            for field_info in node.parameters.values():
                total_params += 1
                if field_info.generated_value is not None:
                    params_with_values += 1
                if field_info.used_default:
                    params_using_defaults += 1
            for child in node.children:
                collect_stats(child)

        collect_stats(tree)

        # Calculate depth
        def calc_depth(node: ParameterizedNode) -> int:
            if not node.children:
                return 1
            return 1 + max(calc_depth(c) for c in node.children)

        depth = calc_depth(tree)

        return ParameterizedStructure(
            root=root,
            components=components,
            dsl=dsl,
            tree=tree,
            depth=depth,
            is_valid=True,
            total_parameters=total_params,
            parameters_with_values=params_with_values,
            parameters_using_defaults=params_using_defaults,
        )

    def tree_to_dsl(self, tree: Dict[str, Any]) -> str:
        """
        Convert a component tree to DSL notation.

        Args:
            tree: Tree dict from generate_tree()

        Returns:
            DSL string like "§[δ, Ƀ[ᵬ×2]]"
        """
        symbol = tree["symbol"]
        children = tree.get("children", [])

        if not children:
            return symbol

        # Group consecutive identical children for ×N notation
        groups = []
        current_group = None
        current_count = 0

        for child in children:
            child_dsl = self.tree_to_dsl(child)
            if child_dsl == current_group:
                current_count += 1
            else:
                if current_group is not None:
                    groups.append((current_group, current_count))
                current_group = child_dsl
                current_count = 1

        if current_group is not None:
            groups.append((current_group, current_count))

        # Build DSL parts
        parts = []
        for dsl, count in groups:
            if count > 1:
                parts.append(f"{dsl}×{count}")
            else:
                parts.append(dsl)

        return f"{symbol}[{', '.join(parts)}]"

    def tree_to_component_list(self, tree: Dict[str, Any]) -> List[str]:
        """Extract flat list of all components in tree."""
        components = [tree["name"]]
        for child in tree.get("children", []):
            components.extend(self.tree_to_component_list(child))
        return components

    def calculate_depth(self, tree: Dict[str, Any]) -> int:
        """Calculate maximum depth of tree."""
        children = tree.get("children", [])
        if not children:
            return 1
        return 1 + max(self.calculate_depth(c) for c in children)

    def generate_random_structure(
        self,
        root: str = "Section",
        required_components: Optional[List[str]] = None,
    ) -> GeneratedStructure:
        """
        Generate a random valid structure starting from root.

        Args:
            root: Root component (default: Section)
            required_components: Components that must appear in structure

        Returns:
            GeneratedStructure with tree, DSL, components, validation
        """
        wrapper = self._get_wrapper()
        required = set(required_components or self.config.required_components)

        # Generate tree
        tree = self.generate_tree(root, depth=0, required=required)

        # Convert to DSL
        dsl = self.tree_to_dsl(tree)

        # Get component list
        components = self.tree_to_component_list(tree)

        # Calculate depth
        depth = self.calculate_depth(tree)

        # Validate using the DAG
        is_valid = True
        issues = []

        # Check that required components are present
        for req in required:
            if req not in components:
                is_valid = False
                issues.append(f"Required component '{req}' not in structure")

        # Validate containment chain for each path
        def validate_paths(node: Dict, path: List[str] = None):
            nonlocal is_valid, issues
            path = path or []
            current_path = path + [node["name"]]

            for child in node.get("children", []):
                # Check if parent can contain child
                if not wrapper.can_contain(
                    node["name"], child["name"], direct_only=True
                ):
                    # Also check our hierarchy
                    hierarchy_children = WIDGET_HIERARCHY.get(node["name"], [])
                    if child["name"] not in hierarchy_children:
                        is_valid = False
                        issues.append(f"{node['name']} cannot contain {child['name']}")
                validate_paths(child, current_path)

        validate_paths(tree)

        return GeneratedStructure(
            root=root,
            components=components,
            dsl=dsl,
            tree=tree,
            depth=depth,
            is_valid=is_valid,
            validation_issues=issues,
        )

    # =========================================================================
    # CARD JSON GENERATION
    # =========================================================================

    def _build_carousel_card(
        self,
        carousel_card_tree: Dict[str, Any],
        widget_index: int,
    ) -> Tuple[Dict[str, Any], int]:
        """Build a single CarouselCard from tree."""
        card_content = {
            "widgets": [],
            "footerWidgets": [],
        }

        # Process children (NestedWidget contains the actual widgets)
        for child in carousel_card_tree.get("children", []):
            if child["name"] == "NestedWidget":
                # NestedWidget children become card widgets (NOT wrapped in nestedWidget)
                for nested_child in child.get("children", []):
                    child_widgets, widget_index = self.tree_to_widgets(
                        nested_child, widget_index
                    )
                    card_content["widgets"].extend(child_widgets)

        # Add a default widget if empty
        if not card_content["widgets"]:
            card_content["widgets"].append(
                {"textParagraph": {"text": "Carousel card content"}}
            )

        return card_content, widget_index

    def _generate_content(self, component: str, index: int = 0) -> Dict[str, Any]:
        """Generate sample content for a component."""
        if component == "DecoratedText":
            return {
                "decoratedText": {
                    "startIcon": {"knownIcon": random.choice(SAMPLE_ICONS)},
                    "text": random.choice(SAMPLE_TEXTS),
                    "wrapText": True,
                }
            }
        elif component == "TextParagraph":
            return {
                "textParagraph": {
                    "text": random.choice(SAMPLE_TEXTS),
                }
            }
        elif component == "Button":
            btn = {"text": random.choice(SAMPLE_BUTTON_LABELS)}
            if self.config.webhook_url:
                btn["onClick"] = {
                    "openLink": {
                        "url": f"{self.config.webhook_url}?btn={index}&test={self.config.test_id}"
                    }
                }
            return btn
        elif component == "ButtonList":
            return {"buttonList": {"buttons": []}}
        elif component == "Image":
            return {
                "image": {
                    "imageUrl": random.choice(SAMPLE_IMAGES),
                    "altText": "Generated test image",
                }
            }
        elif component == "Divider":
            return {"divider": {}}
        elif component == "Icon":
            return {"knownIcon": random.choice(SAMPLE_ICONS)}
        elif component == "Grid":
            return {
                "grid": {
                    "title": "Test Grid",
                    "columnCount": 2,
                    "items": [],
                }
            }
        elif component == "GridItem":
            return {
                "title": f"Item {index + 1}",
                "subtitle": "Test item",
            }
        elif component == "SelectionInput":
            return {
                "selectionInput": {
                    "name": f"select_{index}",
                    "label": "Select option",
                    "type": "DROPDOWN",
                    "items": [
                        {"text": "Option A", "value": "a", "selected": True},
                        {"text": "Option B", "value": "b"},
                    ],
                }
            }
        elif component == "DateTimePicker":
            return {
                "dateTimePicker": {
                    "name": f"datetime_{index}",
                    "label": "Select date",
                    "type": "DATE_AND_TIME",
                }
            }
        else:
            # Generic fallback
            return {"text": f"{component} content"}

    def _generate_content_from_params(
        self,
        node: ParameterizedNode,
        index: int = 0,
    ) -> Dict[str, Any]:
        """
        Generate widget content using the node's parameter values.

        This uses the generated/default values from the ParameterizedNode
        instead of hardcoded sample values.

        Args:
            node: ParameterizedNode with parameter values
            index: Widget index for unique IDs

        Returns:
            Widget content dict
        """
        component = node.name
        params = node.parameters

        def get_param(name: str, default: Any = None) -> Any:
            """Get parameter value, using generated value if available."""
            if name in params:
                val = params[name].generated_value
                return val if val is not None else default
            return default

        if component == "DecoratedText":
            content = {
                "decoratedText": {
                    "text": get_param("text", random.choice(SAMPLE_TEXTS)),
                    "wrapText": get_param("wrap_text", True),
                }
            }
            # Add optional fields if values were generated
            if get_param("top_label"):
                content["decoratedText"]["topLabel"] = get_param("top_label")
            if get_param("bottom_label"):
                content["decoratedText"]["bottomLabel"] = get_param("bottom_label")
            # Add icon - prefer generated materialIcon
            icon_val = get_param("start_icon") or get_param("icon")
            if icon_val and isinstance(icon_val, dict):
                content["decoratedText"]["startIcon"] = icon_val
            else:
                # Use a material icon from our curated list via helper
                content["decoratedText"]["startIcon"] = create_material_icon(
                    random.choice(CURATED_MATERIAL_ICONS)
                )
            return content

        elif component == "TextParagraph":
            return {
                "textParagraph": {
                    "text": get_param("text", random.choice(SAMPLE_TEXTS)),
                }
            }

        elif component == "Button":
            btn = {"text": get_param("text", random.choice(SAMPLE_BUTTON_LABELS))}
            # Add icon if generated
            icon_val = get_param("icon")
            if icon_val and isinstance(icon_val, dict):
                btn["icon"] = icon_val
            if self.config.webhook_url:
                btn["onClick"] = {
                    "openLink": {
                        "url": f"{self.config.webhook_url}?btn={index}&test={self.config.test_id}"
                    }
                }
            return btn

        elif component == "ButtonList":
            return {"buttonList": {"buttons": []}}

        elif component == "Image":
            return {
                "image": {
                    "imageUrl": get_param("image_url", random.choice(SAMPLE_IMAGES)),
                    "altText": get_param("alt_text", "Generated test image"),
                }
            }

        elif component == "Divider":
            return {"divider": {}}

        elif component == "Icon":
            # Return icon dict - prefer material icon format
            icon_val = get_param("material_icon") or get_param("known_icon")
            if icon_val and isinstance(icon_val, dict):
                return icon_val
            # Use material icon via helper
            return create_material_icon(random.choice(CURATED_MATERIAL_ICONS))

        elif component == "Grid":
            return {
                "grid": {
                    "title": get_param("title", "Test Grid"),
                    "columnCount": get_param("column_count", 2),
                    "items": [],
                }
            }

        elif component == "GridItem":
            return {
                "title": get_param("title", f"Item {index + 1}"),
                "subtitle": get_param("subtitle", "Test item"),
            }

        elif component == "SelectionInput":
            return {
                "selectionInput": {
                    "name": get_param("name", f"select_{index}"),
                    "label": get_param("label", "Select option"),
                    "type": get_param("type", "DROPDOWN"),
                    "items": [
                        {"text": "Option A", "value": "a", "selected": True},
                        {"text": "Option B", "value": "b"},
                    ],
                }
            }

        elif component == "DateTimePicker":
            return {
                "dateTimePicker": {
                    "name": get_param("name", f"datetime_{index}"),
                    "label": get_param("label", "Select date"),
                    "type": get_param("type", "DATE_AND_TIME"),
                }
            }

        elif component == "Chip":
            chip = {
                "label": get_param("label", f"Chip {index + 1}"),
                "enabled": get_param("enabled", True),
            }
            icon_val = get_param("icon")
            if icon_val and isinstance(icon_val, dict):
                chip["icon"] = icon_val
            return chip

        else:
            # Generic fallback
            return {"text": f"{component} content"}

    def parameterized_tree_to_widgets(
        self,
        node: ParameterizedNode,
        widget_index: int = 0,
    ) -> Tuple[List[Dict[str, Any]], int]:
        """
        Convert a ParameterizedNode tree to widget JSON using generated parameter values.

        This is the parameterized version of tree_to_widgets that uses
        the actual generated values from each node's parameters dict.

        Args:
            node: ParameterizedNode with parameter values
            widget_index: Running index for unique IDs

        Returns:
            Tuple of (widgets list, updated index)
        """
        widgets = []
        component = node.name
        children = node.children

        # Generate content using parameterized values
        content = self._generate_content_from_params(node, widget_index)
        widget_index += 1

        # Handle special cases for nesting
        if component == "Section":
            section_widgets = []
            for child in children:
                child_widgets, widget_index = self.parameterized_tree_to_widgets(
                    child, widget_index
                )
                section_widgets.extend(child_widgets)
            return section_widgets, widget_index

        elif component == "ButtonList":
            buttons = []
            for child in children:
                if child.name == "Button":
                    btn_content = self._generate_content_from_params(
                        child, widget_index
                    )
                    widget_index += 1
                    # Handle Button's children (Icon)
                    for btn_child in child.children:
                        if btn_child.name == "Icon":
                            btn_content["icon"] = self._generate_content_from_params(
                                btn_child
                            )
                    buttons.append(btn_content)
            content["buttonList"]["buttons"] = buttons
            widgets.append(content)

        elif component == "DecoratedText":
            # DecoratedText has a oneof for control: either button OR switchControl (not both)
            control_set = False
            for child in children:
                if child.name == "Icon":
                    content["decoratedText"]["startIcon"] = (
                        self._generate_content_from_params(child)
                    )
                elif child.name == "Button" and not control_set:
                    content["decoratedText"]["button"] = (
                        self._generate_content_from_params(child, widget_index)
                    )
                    widget_index += 1
                    control_set = True  # Can't add switchControl now
                elif child.name == "SwitchControl" and not control_set:
                    content["decoratedText"]["switchControl"] = {
                        "name": f"switch_{widget_index}",
                        "selected": random.choice([True, False]),
                    }
                    widget_index += 1
                    control_set = True  # Can't add button now
            widgets.append(content)

        elif component == "Grid":
            items = []
            for child in children:
                if child.name == "GridItem":
                    item = self._generate_content_from_params(child, widget_index)
                    widget_index += 1
                    for item_child in child.children:
                        if item_child.name == "Image":
                            item["image"] = {
                                "imageUri": random.choice(SAMPLE_IMAGES),
                                "altText": "Grid image",
                            }
                    items.append(item)
            content["grid"]["items"] = items
            widgets.append(content)

        elif component == "ChipList":
            chips = []
            for child in children:
                if child.name == "Chip":
                    chip = self._generate_content_from_params(child, widget_index)
                    widget_index += 1
                    chips.append(chip)
            widgets.append(
                {
                    "chipList": {
                        "chips": chips if chips else [{"label": "Default Chip"}],
                    }
                }
            )

        elif component == "Carousel":
            carousel_cards = []
            for child in children:
                if child.name == "CarouselCard":
                    card_content, widget_index = (
                        self._build_parameterized_carousel_card(child, widget_index)
                    )
                    carousel_cards.append(card_content)

            if not carousel_cards:
                carousel_cards.append(
                    {
                        "widgets": [{"textParagraph": {"text": "Carousel item"}}],
                        "footerWidgets": [],
                    }
                )

            widgets.append(
                {
                    "carousel": {
                        "carouselCards": carousel_cards,
                    }
                }
            )

        elif component == "NestedWidget":
            for child in children:
                child_widgets, widget_index = self.parameterized_tree_to_widgets(
                    child, widget_index
                )
                widgets.extend(child_widgets)

        elif component == "Columns":
            columns = []
            for child in children:
                if child.name == "Column":
                    col_widgets = []
                    for col_child in child.children:
                        child_widgets, widget_index = (
                            self.parameterized_tree_to_widgets(col_child, widget_index)
                        )
                        col_widgets.extend(child_widgets)

                    columns.append(
                        {
                            "horizontalSizeStyle": "FILL_AVAILABLE_SPACE",
                            "horizontalAlignment": "CENTER",
                            "verticalAlignment": "CENTER",
                            "widgets": (
                                col_widgets
                                if col_widgets
                                else [{"textParagraph": {"text": "Column content"}}]
                            ),
                        }
                    )

            if columns:
                widgets.append(
                    {
                        "columns": {
                            "columnItems": columns,
                        }
                    }
                )

        else:
            # Leaf widgets
            widgets.append(content)

        return widgets, widget_index

    def _build_parameterized_carousel_card(
        self,
        carousel_card_node: ParameterizedNode,
        widget_index: int,
    ) -> Tuple[Dict[str, Any], int]:
        """Build a CarouselCard from ParameterizedNode."""
        card_content = {
            "widgets": [],
            "footerWidgets": [],
        }

        for child in carousel_card_node.children:
            if child.name == "NestedWidget":
                for nested_child in child.children:
                    child_widgets, widget_index = self.parameterized_tree_to_widgets(
                        nested_child, widget_index
                    )
                    card_content["widgets"].extend(child_widgets)

        if not card_content["widgets"]:
            card_content["widgets"].append(
                {"textParagraph": {"text": "Carousel card content"}}
            )

        return card_content, widget_index

    def parameterized_structure_to_card(
        self,
        structure: ParameterizedStructure,
        title: str = "Generated Card",
        subtitle: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Convert a ParameterizedStructure to complete card JSON.

        This uses the generated parameter values from each node
        instead of hardcoded sample values.

        Args:
            structure: ParameterizedStructure from generate_parameterized_structure()
            title: Card header title
            subtitle: Card header subtitle (auto-generated if None)

        Returns:
            Complete message payload with cards_v2
        """
        # Convert tree to widgets using parameterized values
        widgets, _ = self.parameterized_tree_to_widgets(structure.tree)

        # Build card structure
        card_dict = {
            "card_id": f"dag-param-{self.config.test_id}",
            "card": {
                "header": {
                    "title": title,
                    "subtitle": subtitle or f"DSL: {structure.dsl}",
                },
                "sections": [
                    {
                        "widgets": widgets,
                    }
                ],
            },
        }

        # Build message using Message component
        message = self._build_message_payload(card_dict)

        # Add metadata including parameter stats
        message["_dag_gen_meta"] = {
            "test_id": self.config.test_id,
            "dsl": structure.dsl,
            "components": structure.components,
            "depth": structure.depth,
            "is_valid": structure.is_valid,
            "timestamp": datetime.now().isoformat(),
            "parameter_stats": {
                "total": structure.total_parameters,
                "with_values": structure.parameters_with_values,
                "using_defaults": structure.parameters_using_defaults,
            },
        }

        return message

    def generate_parameterized_card(
        self,
        root: str = "Section",
        required_components: Optional[List[str]] = None,
        fill_optional_probability: float = 0.5,
    ) -> Dict[str, Any]:
        """
        Generate a card with parameterized component values.

        This is like generate_random_card but uses the full parameter
        generation system to provide meaningful test values.

        Args:
            root: Root component
            required_components: Components that must appear
            fill_optional_probability: Probability of filling optional fields

        Returns:
            Complete card JSON with generated parameter values
        """
        # Generate new test ID for each card
        self.config.test_id = str(uuid.uuid4())[:8]

        structure = self.generate_parameterized_structure(
            root,
            required_components,
            fill_optional_probability,
        )

        return self.parameterized_structure_to_card(structure)

    def tree_to_widgets(
        self,
        tree: Dict[str, Any],
        widget_index: int = 0,
    ) -> Tuple[List[Dict[str, Any]], int]:
        """
        Convert a component tree to widget JSON.

        Args:
            tree: Component tree
            widget_index: Running index for unique IDs

        Returns:
            Tuple of (widgets list, updated index)
        """
        widgets = []
        component = tree["name"]
        children = tree.get("children", [])

        # Generate base content
        content = self._generate_content(component, widget_index)
        widget_index += 1

        # Handle special cases for nesting
        if component == "Section":
            # Section contains widgets directly
            section_widgets = []
            for child in children:
                child_widgets, widget_index = self.tree_to_widgets(child, widget_index)
                section_widgets.extend(child_widgets)
            return section_widgets, widget_index

        elif component == "ButtonList":
            # ButtonList wraps buttons
            buttons = []
            for child in children:
                if child["name"] == "Button":
                    btn_content = self._generate_content("Button", widget_index)
                    widget_index += 1
                    # Handle Button's children (Icon)
                    for btn_child in child.get("children", []):
                        if btn_child["name"] == "Icon":
                            btn_content["icon"] = self._generate_content("Icon")
                    buttons.append(btn_content)
            content["buttonList"]["buttons"] = buttons
            widgets.append(content)

        elif component == "DecoratedText":
            # DecoratedText has a oneof for control: either button OR switchControl (not both)
            control_set = False
            for child in children:
                if child["name"] == "Icon":
                    content["decoratedText"]["startIcon"] = self._generate_content(
                        "Icon"
                    )
                elif child["name"] == "Button" and not control_set:
                    content["decoratedText"]["button"] = self._generate_content(
                        "Button", widget_index
                    )
                    widget_index += 1
                    control_set = True
                elif child["name"] == "SwitchControl" and not control_set:
                    content["decoratedText"]["switchControl"] = {
                        "name": f"switch_{widget_index}",
                        "selected": random.choice([True, False]),
                    }
                    widget_index += 1
                    control_set = True
            widgets.append(content)

        elif component == "Grid":
            # Grid contains GridItems
            items = []
            for child in children:
                if child["name"] == "GridItem":
                    item = self._generate_content("GridItem", widget_index)
                    widget_index += 1
                    # GridItem can have Image
                    for item_child in child.get("children", []):
                        if item_child["name"] == "Image":
                            item["image"] = {
                                "imageUri": random.choice(SAMPLE_IMAGES),
                                "altText": "Grid image",
                            }
                    items.append(item)
            content["grid"]["items"] = items
            widgets.append(content)

        elif component == "Carousel":
            # Carousel contains CarouselCards - outputs a carousel widget
            carousel_cards = []
            for child in children:
                if child["name"] == "CarouselCard":
                    card_content, widget_index = self._build_carousel_card(
                        child, widget_index
                    )
                    carousel_cards.append(card_content)

            # Add at least one card if empty
            if not carousel_cards:
                carousel_cards.append(
                    {
                        "widgets": [{"textParagraph": {"text": "Carousel item"}}],
                        "footerWidgets": [],
                    }
                )

            widgets.append(
                {
                    "carousel": {
                        "carouselCards": carousel_cards,
                    }
                }
            )

        elif component == "CarouselCard":
            # Standalone CarouselCard (shouldn't happen, but handle it)
            card_content, widget_index = self._build_carousel_card(tree, widget_index)
            widgets.append(
                {
                    "carousel": {
                        "carouselCards": [card_content],
                    }
                }
            )

        elif component == "NestedWidget":
            # NestedWidget just passes through its children (no wrapping)
            for child in children:
                child_widgets, widget_index = self.tree_to_widgets(child, widget_index)
                widgets.extend(child_widgets)

        elif component == "CollapseControl":
            # CollapseControl has expand/collapse buttons
            buttons = []
            for child in children:
                if child["name"] == "Button":
                    btn = self._generate_content("Button", widget_index)
                    widget_index += 1
                    buttons.append(btn)
            if buttons:
                widgets.append(
                    {
                        "collapseControl": {
                            "expandButton": buttons[0] if len(buttons) > 0 else None,
                            "collapseButton": buttons[1] if len(buttons) > 1 else None,
                        }
                    }
                )

        elif component == "Columns":
            # Columns widget contains Column items
            columns = []
            for child in children:
                if child["name"] == "Column":
                    # Column contains widgets
                    col_widgets = []
                    for col_child in child.get("children", []):
                        child_widgets, widget_index = self.tree_to_widgets(
                            col_child, widget_index
                        )
                        col_widgets.extend(child_widgets)

                    columns.append(
                        {
                            "horizontalSizeStyle": "FILL_AVAILABLE_SPACE",
                            "horizontalAlignment": "CENTER",
                            "verticalAlignment": "CENTER",
                            "widgets": (
                                col_widgets
                                if col_widgets
                                else [{"textParagraph": {"text": "Column content"}}]
                            ),
                        }
                    )

            if columns:
                widgets.append(
                    {
                        "columns": {
                            "columnItems": columns,
                        }
                    }
                )

        elif component == "ChipList":
            # ChipList contains Chip items
            chips = []
            for child in children:
                if child["name"] == "Chip":
                    chip = {
                        "label": f"Chip {len(chips) + 1}",
                        "enabled": True,
                    }
                    # Handle Chip's children (Icon)
                    for chip_child in child.get("children", []):
                        if chip_child["name"] == "Icon":
                            chip["icon"] = self._generate_content("Icon")
                    chips.append(chip)

            widgets.append(
                {
                    "chipList": {
                        "chips": chips if chips else [{"label": "Default Chip"}],
                    }
                }
            )

        else:
            # Leaf widgets
            widgets.append(content)

        return widgets, widget_index

    def _build_message_payload(
        self,
        card_dict: Dict[str, Any],
        fallback_text: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Build a proper message payload for webhook API.

        Note: The 'text' field is optional for webhooks - cards render without it.
        If provided, text appears in notifications and as fallback.

        Args:
            card_dict: Card dictionary with 'card' key
            fallback_text: Optional text for notifications (None = card only)

        Returns:
            Message payload ready for webhook
        """
        try:
            from card_framework.v2 import Message

            message_obj = Message()
            if fallback_text:
                message_obj.text = fallback_text
            message_obj.cards_v2.append(card_dict)
            message_body = message_obj.render()

            # Fix field name issues for webhook API (expects snake_case)
            if "cards_v_2" in message_body:
                message_body["cards_v2"] = message_body.pop("cards_v_2")
            if "cardsV2" in message_body:
                message_body["cards_v2"] = message_body.pop("cardsV2")

            return message_body

        except ImportError:
            # Fallback if card_framework not available
            payload = {"cards_v2": [card_dict]}
            if fallback_text:
                payload["text"] = fallback_text
            return payload

    def structure_to_card(
        self,
        structure: GeneratedStructure,
        title: str = "Generated Card",
        subtitle: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Convert a GeneratedStructure to complete card JSON.

        Uses card_framework.v2.Message for proper message building.

        Args:
            structure: Generated structure from generate_random_structure()
            title: Card header title
            subtitle: Card header subtitle (auto-generated if None)

        Returns:
            Complete message payload with cards_v2
        """
        # Convert tree to widgets
        widgets, _ = self.tree_to_widgets(structure.tree)

        # Build card structure
        card_dict = {
            "card_id": f"dag-gen-{self.config.test_id}",
            "card": {
                "header": {
                    "title": title,
                    "subtitle": subtitle or f"DSL: {structure.dsl}",
                },
                "sections": [
                    {
                        "widgets": widgets,
                    }
                ],
            },
        }

        # Build message using Message component
        message = self._build_message_payload(card_dict)

        # Add metadata
        message["_dag_gen_meta"] = {
            "test_id": self.config.test_id,
            "dsl": structure.dsl,
            "components": structure.components,
            "depth": structure.depth,
            "is_valid": structure.is_valid,
            "timestamp": datetime.now().isoformat(),
        }

        return message

    def generate_random_card(
        self,
        root: str = "Section",
        required_components: Optional[List[str]] = None,
    ) -> Dict[str, Any]:
        """
        Generate a complete random card.

        Args:
            root: Root component
            required_components: Components that must appear

        Returns:
            Complete card JSON
        """
        # Generate new test ID for each card
        self.config.test_id = str(uuid.uuid4())[:8]

        structure = self.generate_random_structure(root, required_components)
        return self.structure_to_card(structure)

    def generate_random_cards(
        self,
        count: int = 5,
        root: str = "Section",
        required_components: Optional[List[str]] = None,
    ) -> List[Dict[str, Any]]:
        """
        Generate multiple random cards.

        Args:
            count: Number of cards to generate
            root: Root component for each card
            required_components: Components that must appear in each

        Returns:
            List of card JSON structures
        """
        return [
            self.generate_random_card(root, required_components) for _ in range(count)
        ]

    def generate_widget_showcase_carousel(self) -> Dict[str, Any]:
        """
        Generate a showcase carousel demonstrating CarouselCard capabilities.

        NOTE: NestedWidget in CarouselCard only supports:
        - textParagraph
        - buttonList
        - image

        Each carousel card shows different variations of these supported widgets.

        Returns:
            Complete card JSON with showcase carousel
        """
        self.config.test_id = str(uuid.uuid4())[:8]

        carousel_cards = []

        # Card 1: Text Formatting
        carousel_cards.append(
            {
                "widgets": [
                    {"textParagraph": {"text": "<b>📝 Text Formatting</b>"}},
                    {"textParagraph": {"text": "Normal text paragraph"}},
                    {
                        "textParagraph": {
                            "text": "<i>Italic text</i> and <b>bold text</b>"
                        }
                    },
                    {
                        "textParagraph": {
                            "text": '<font color="#4285F4">Colored text</font>'
                        }
                    },
                ],
                "footerWidgets": [{"buttonList": {"buttons": [{"text": "Text Demo"}]}}],
            }
        )

        # Card 2: Images
        carousel_cards.append(
            {
                "widgets": [
                    {"textParagraph": {"text": "<b>🖼️ Images</b>"}},
                    {
                        "image": {
                            "imageUrl": SAMPLE_IMAGES[0],
                            "altText": "Sample image 1",
                        }
                    },
                    {
                        "textParagraph": {
                            "text": "Images with alt text for accessibility"
                        }
                    },
                ],
                "footerWidgets": [
                    {"buttonList": {"buttons": [{"text": "View Gallery"}]}}
                ],
            }
        )

        # Card 3: More Images
        carousel_cards.append(
            {
                "widgets": [
                    {"textParagraph": {"text": "<b>🎨 Gallery Item</b>"}},
                    {
                        "image": {
                            "imageUrl": SAMPLE_IMAGES[1],
                            "altText": "Sample image 2",
                        }
                    },
                    {
                        "textParagraph": {
                            "text": "Carousel cards are great for galleries!"
                        }
                    },
                ],
                "footerWidgets": [{"buttonList": {"buttons": [{"text": "Next →"}]}}],
            }
        )

        # Card 4: Button Variations
        carousel_cards.append(
            {
                "widgets": [
                    {"textParagraph": {"text": "<b>🔘 Button List</b>"}},
                    {"textParagraph": {"text": "Buttons with material icons:"}},
                    {
                        "buttonList": {
                            "buttons": [
                                {"text": "Star", "icon": create_material_icon("star")},
                                {
                                    "text": "Save",
                                    "icon": create_material_icon("bookmark"),
                                },
                                {
                                    "text": "Share",
                                    "icon": create_material_icon("share"),
                                },
                            ]
                        }
                    },
                ],
                "footerWidgets": [
                    {
                        "buttonList": {
                            "buttons": [
                                {"text": "Action 1"},
                                {"text": "Action 2"},
                            ]
                        }
                    }
                ],
            }
        )

        # Card 5: Combined
        carousel_cards.append(
            {
                "widgets": [
                    {"textParagraph": {"text": "<b>✨ Combined</b>"}},
                    {
                        "image": {
                            "imageUrl": (
                                SAMPLE_IMAGES[2]
                                if len(SAMPLE_IMAGES) > 2
                                else SAMPLE_IMAGES[0]
                            ),
                            "altText": "Feature image",
                        }
                    },
                    {
                        "textParagraph": {
                            "text": "<b>Feature Title</b><br>Description text here"
                        }
                    },
                    {
                        "buttonList": {
                            "buttons": [
                                {
                                    "text": "Learn More",
                                    "icon": create_material_icon("arrow_forward"),
                                },
                            ]
                        }
                    },
                ],
                "footerWidgets": [
                    {"buttonList": {"buttons": [{"text": "Get Started"}]}}
                ],
            }
        )

        # Build card structure
        card_dict = {
            "card_id": f"showcase-{self.config.test_id}",
            "card": {
                "header": {
                    "title": "Carousel Showcase",
                    "subtitle": "NestedWidget: textParagraph, buttonList, image",
                },
                "sections": [
                    {
                        "widgets": [
                            {"carousel": {"carouselCards": carousel_cards}},
                        ],
                    }
                ],
            },
        }

        message = self._build_message_payload(card_dict)
        message["_dag_gen_meta"] = {
            "test_id": self.config.test_id,
            "dsl": "§[◦[▲×5]]",
            "components": [
                "Carousel",
                "CarouselCard",
                "NestedWidget",
                "TextParagraph",
                "Image",
                "ButtonList",
                "Button",
                "Icon",
            ],
            "is_showcase": True,
        }

        return message

    def generate_carousel_card(
        self,
        num_carousel_items: int = 3,
    ) -> Dict[str, Any]:
        """
        Generate a card containing a Carousel widget.

        Args:
            num_carousel_items: Number of CarouselCard items

        Returns:
            Complete card JSON with carousel
        """
        self.config.test_id = str(uuid.uuid4())[:8]

        # Build carousel cards manually for reliable structure
        carousel_cards = []
        for i in range(num_carousel_items):
            carousel_cards.append(
                {
                    "widgets": [
                        {"textParagraph": {"text": f"<b>Carousel Item {i + 1}</b>"}},
                        {"textParagraph": {"text": random.choice(SAMPLE_TEXTS)}},
                        {
                            "image": {
                                "imageUrl": random.choice(SAMPLE_IMAGES),
                                "altText": f"Image {i + 1}",
                            }
                        },
                    ],
                    "footerWidgets": [
                        {
                            "buttonList": {
                                "buttons": [
                                    {"text": f"Action {i + 1}"},
                                ]
                            }
                        },
                    ],
                }
            )

        # Build card structure
        card_dict = {
            "card_id": f"carousel-{self.config.test_id}",
            "card": {
                "header": {
                    "title": "DAG Carousel Test",
                    "subtitle": f"DSL: §[◦[▲×{num_carousel_items}]]",
                },
                "sections": [
                    {
                        "widgets": [
                            {"carousel": {"carouselCards": carousel_cards}},
                        ],
                    }
                ],
            },
        }

        # Build message using Message component
        message = self._build_message_payload(card_dict)

        message["_dag_gen_meta"] = {
            "test_id": self.config.test_id,
            "dsl": f"§[◦[▲×{num_carousel_items}]]",
            "components": ["Section", "Carousel"]
            + ["CarouselCard"] * num_carousel_items,
            "is_carousel": True,
        }

        return message

    # =========================================================================
    # TARGETED GENERATION
    # =========================================================================

    def generate_with_components(
        self,
        components: List[str],
        root: str = "Section",
    ) -> GeneratedStructure:
        """
        Generate a structure that includes all specified components.

        The generator will find valid paths to include each component.

        Args:
            components: Components that must appear in the structure
            root: Root component to start from

        Returns:
            GeneratedStructure containing all specified components
        """
        # Store original config
        original_required = self.config.required_components

        try:
            self.config.required_components = components
            return self.generate_random_structure(root, components)
        finally:
            self.config.required_components = original_required

    def generate_all_component_cards(
        self,
        components: Optional[List[str]] = None,
        max_per_card: int = 4,
    ) -> List[Dict[str, Any]]:
        """
        Generate cards that exercise all specified components.

        Splits components into groups and generates a card for each group.

        Args:
            components: Components to exercise (default: all Section children)
            max_per_card: Maximum components to require per card

        Returns:
            List of cards that together exercise all components
        """
        if components is None:
            # Default to all Section children
            components = self.get_valid_children("Section")

        cards = []
        remaining = list(components)

        while remaining:
            # Take a batch
            batch = remaining[:max_per_card]
            remaining = remaining[max_per_card:]

            # Generate card with this batch
            card = self.generate_random_card("Section", batch)
            cards.append(card)

        return cards


# =============================================================================
# CONVENIENCE FUNCTIONS
# =============================================================================


def generate_dag_card(
    root: str = "Section",
    required: Optional[List[str]] = None,
    webhook_url: Optional[str] = None,
) -> Dict[str, Any]:
    """
    Generate a single random valid card.

    Args:
        root: Root component
        required: Required components
        webhook_url: Webhook for button callbacks

    Returns:
        Card JSON
    """
    config = DAGGeneratorConfig(webhook_url=webhook_url)
    gen = DAGStructureGenerator(config)
    return gen.generate_random_card(root, required)


def generate_dag_structure(
    root: str = "Section",
    max_depth: int = 3,
) -> GeneratedStructure:
    """
    Generate a random valid structure.

    Args:
        root: Root component
        max_depth: Maximum nesting depth

    Returns:
        GeneratedStructure with DSL and tree
    """
    config = DAGGeneratorConfig(max_depth=max_depth)
    gen = DAGStructureGenerator(config)
    return gen.generate_random_structure(root)


async def send_parameterized_cards_to_webhook(
    webhook_url: str,
    count: int = 3,
    root: str = "Section",
    required: Optional[List[str]] = None,
    include_carousel: bool = False,
    delay_seconds: float = 1.0,
    fill_optional_probability: float = 0.5,
) -> List[Dict[str, Any]]:
    """
    Generate and send parameterized cards to a webhook.

    This version uses the full parameter generation system for richer test values.

    Args:
        webhook_url: Google Chat webhook URL
        count: Number of cards to send
        root: Root component for generation
        required: Required components in each card
        include_carousel: Whether to include a Carousel card
        delay_seconds: Delay between card sends
        fill_optional_probability: Probability of filling optional fields

    Returns:
        List of results with card info and response status
    """
    import asyncio

    import httpx

    config = DAGGeneratorConfig(webhook_url=webhook_url)
    gen = DAGStructureGenerator(config)
    results = []

    async with httpx.AsyncClient(verify=False) as client:
        for i in range(count):
            config.test_id = str(uuid.uuid4())[:8]

            if include_carousel and i == count - 1:
                card = gen.generate_carousel_card()
                structure_dsl = card.get("_dag_gen_meta", {}).get("dsl", "§[◦[▲]]")
                components = card.get("_dag_gen_meta", {}).get("components", [])
                param_stats = None
            else:
                structure = gen.generate_parameterized_structure(
                    root,
                    required,
                    fill_optional_probability,
                )
                card = gen.parameterized_structure_to_card(
                    structure,
                    title=f"Parameterized Card #{i + 1}",
                    subtitle=f"DSL: {structure.dsl}",
                )
                structure_dsl = structure.dsl
                components = structure.components
                param_stats = {
                    "total": structure.total_parameters,
                    "with_values": structure.parameters_with_values,
                    "using_defaults": structure.parameters_using_defaults,
                }

            meta = card.pop("_dag_gen_meta", {})

            result = {
                "index": i + 1,
                "test_id": config.test_id,
                "dsl": structure_dsl,
                "components": components,
                "parameter_stats": param_stats,
                "valid": True,
                "success": False,
                "error": None,
            }

            try:
                response = await client.post(
                    webhook_url,
                    json=card,
                    timeout=30.0,
                )

                if response.status_code == 200:
                    result["success"] = True
                    stats_str = ""
                    if param_stats:
                        stats_str = f" [params: {param_stats['with_values']}/{param_stats['total']}]"
                    logger.info("✅ Card %s: %s%s", i + 1, structure_dsl, stats_str)
                else:
                    result["error"] = (
                        f"HTTP {response.status_code}: {response.text[:100]}"
                    )
                    logger.error("❌ Card %s: %s", i + 1, result["error"])

            except Exception as e:
                result["error"] = str(e)
                logger.exception("❌ Card %s failed", i + 1)

            results.append(result)

            if i < count - 1:
                await asyncio.sleep(delay_seconds)

    return results


async def send_dag_cards_to_webhook(
    webhook_url: str,
    count: int = 3,
    root: str = "Section",
    required: Optional[List[str]] = None,
    include_carousel: bool = False,
    delay_seconds: float = 1.0,
) -> List[Dict[str, Any]]:
    """
    Generate and send random DAG-based cards to a webhook.

    Args:
        webhook_url: Google Chat webhook URL
        count: Number of cards to send
        root: Root component for generation
        required: Required components in each card
        include_carousel: Whether to include a Carousel card
        delay_seconds: Delay between card sends

    Returns:
        List of results with card info and response status
    """
    import asyncio

    import httpx

    config = DAGGeneratorConfig(webhook_url=webhook_url)
    gen = DAGStructureGenerator(config)
    results = []

    async with httpx.AsyncClient(verify=False) as client:
        for i in range(count):
            # Generate card
            config.test_id = str(uuid.uuid4())[:8]

            if include_carousel and i == count - 1:
                # Last card includes a Carousel widget
                card = gen.generate_carousel_card()
                structure_dsl = "§[◦[▲[ŋ[...]]]]"  # Carousel DSL placeholder
                components = ["Section", "Carousel", "CarouselCard", "NestedWidget"]
            else:
                structure = gen.generate_random_structure(root, required)
                card = gen.structure_to_card(
                    structure,
                    title=f"DAG Test Card #{i + 1}",
                    subtitle=f"DSL: {structure.dsl}",
                )
                structure_dsl = structure.dsl
                components = structure.components

            meta = card.pop("_dag_gen_meta", {})

            result = {
                "index": i + 1,
                "test_id": config.test_id,
                "dsl": structure_dsl,
                "components": components,
                "valid": True,
                "success": False,
                "error": None,
            }

            try:
                response = await client.post(
                    webhook_url,
                    json=card,
                    timeout=30.0,
                )

                if response.status_code == 200:
                    result["success"] = True
                    logger.info("✅ Card %s: %s", i + 1, structure_dsl)
                else:
                    result["error"] = (
                        f"HTTP {response.status_code}: {response.text[:100]}"
                    )
                    logger.error("❌ Card %s: %s", i + 1, result["error"])

            except Exception as e:
                result["error"] = str(e)
                logger.exception("❌ Card %s failed", i + 1)

            results.append(result)

            if i < count - 1:
                await asyncio.sleep(delay_seconds)

    return results


async def run_stress_test(
    webhook_url: str,
    fill_probability: float = 0.8,
    delay_seconds: float = 1.0,
) -> Dict[str, Any]:
    """
    Run comprehensive stress test covering ALL components with complex parameters.

    This test:
    1. Tests each component type at least once
    2. Uses high parameter fill probability
    3. Generates complex nested structures
    4. Reports component coverage

    Args:
        webhook_url: Google Chat webhook URL
        fill_probability: Probability of filling optional fields (0.8 = 80%)
        delay_seconds: Delay between card sends

    Returns:
        Dict with test results and coverage stats
    """
    import asyncio

    import httpx

    # All testable components from WIDGET_HIERARCHY
    ALL_COMPONENTS = set()
    for parent, children in WIDGET_HIERARCHY.items():
        ALL_COMPONENTS.add(parent)
        ALL_COMPONENTS.update(children)

    # Remove internal/abstract types
    TESTABLE_COMPONENTS = (
        ALL_COMPONENTS - INTERNAL_TYPES - {"Card", "CardHeader", "CardFixedFooter"}
    )

    # Group components by what can contain them (for targeted testing)
    SECTION_CHILDREN = set(WIDGET_HIERARCHY.get("Section", []))
    BUTTON_RELATIVES = {"Button", "ButtonList", "Icon"}
    DECORATED_CHILDREN = {"DecoratedText", "Icon", "Button", "SwitchControl"}
    GRID_CHILDREN = {"Grid", "GridItem", "Image"}
    CHIP_CHILDREN = {"ChipList", "Chip", "Icon"}
    CAROUSEL_CHILDREN = {"Carousel", "CarouselCard", "NestedWidget"}

    config = DAGGeneratorConfig(
        max_depth=4,
        max_children_per_node=5,
        avoid_components=[],  # Don't avoid anything in stress test
    )
    gen = DAGStructureGenerator(config)

    results = {
        "total_cards": 0,
        "successful": 0,
        "failed": 0,
        "components_tested": set(),
        "components_missing": set(),
        "errors": [],
        "cards": [],
    }

    async with httpx.AsyncClient(verify=False) as client:
        # Test batch 1: Basic Section widgets
        logger.info("📦 Testing basic Section widgets...")
        basic_widgets = [
            "DecoratedText",
            "TextParagraph",
            "Image",
            "Divider",
            "ButtonList",
            "SelectionInput",
            "DateTimePicker",
        ]
        for widget in basic_widgets:
            config.test_id = str(uuid.uuid4())[:8]
            try:
                structure = gen.generate_parameterized_structure(
                    "Section",
                    required_components=[widget],
                    fill_optional_probability=fill_probability,
                )
                card = gen.parameterized_structure_to_card(
                    structure,
                    title=f"Stress: {widget}",
                    subtitle=f"DSL: {structure.dsl}",
                )
                meta = card.pop("_dag_gen_meta", {})

                response = await client.post(webhook_url, json=card, timeout=30.0)
                results["total_cards"] += 1

                if response.status_code == 200:
                    results["successful"] += 1
                    results["components_tested"].update(structure.components)
                    logger.info("  ✅ %s: %s", widget, structure.dsl)
                else:
                    results["failed"] += 1
                    results["errors"].append(
                        {
                            "widget": widget,
                            "dsl": structure.dsl,
                            "error": response.text[:100],
                        }
                    )
                    logger.error("  ❌ %s: %s", widget, response.text[:80])

                results["cards"].append(
                    {
                        "widget": widget,
                        "dsl": structure.dsl,
                        "success": response.status_code == 200,
                        "params": meta.get("parameter_stats", {}),
                    }
                )

            except Exception as e:
                results["failed"] += 1
                results["errors"].append({"widget": widget, "error": str(e)})
                logger.exception("  ❌ %s failed", widget)

            await asyncio.sleep(delay_seconds)

        # Test batch 2: Complex nested structures
        logger.info("🏗️ Testing complex nested structures...")
        complex_combos = [
            ["DecoratedText", "ButtonList", "Image"],
            ["Grid", "GridItem"],
            ["ChipList", "Chip"],
            ["Columns", "Column", "TextParagraph"],
        ]
        for combo in complex_combos:
            config.test_id = str(uuid.uuid4())[:8]
            try:
                structure = gen.generate_parameterized_structure(
                    "Section",
                    required_components=combo,
                    fill_optional_probability=fill_probability,
                )
                card = gen.parameterized_structure_to_card(
                    structure,
                    title=f"Complex: {'+'.join(combo[:2])}",
                    subtitle=f"DSL: {structure.dsl}",
                )
                meta = card.pop("_dag_gen_meta", {})

                response = await client.post(webhook_url, json=card, timeout=30.0)
                results["total_cards"] += 1

                if response.status_code == 200:
                    results["successful"] += 1
                    results["components_tested"].update(structure.components)
                    logger.info("  ✅ %s: %s", "+".join(combo[:2]), structure.dsl)
                else:
                    results["failed"] += 1
                    results["errors"].append(
                        {
                            "combo": combo,
                            "dsl": structure.dsl,
                            "error": response.text[:100],
                        }
                    )
                    logger.error("  ❌ %s: %s", "+".join(combo[:2]), response.text[:80])

            except Exception as e:
                results["failed"] += 1
                results["errors"].append({"combo": combo, "error": str(e)})
                logger.exception("  ❌ %s failed", "+".join(combo[:2]))

            await asyncio.sleep(delay_seconds)

        # Test batch 3: Carousel
        logger.info("🎠 Testing Carousel...")
        config.test_id = str(uuid.uuid4())[:8]
        try:
            card = gen.generate_carousel_card(num_carousel_items=4)
            meta = card.pop("_dag_gen_meta", {})

            response = await client.post(webhook_url, json=card, timeout=30.0)
            results["total_cards"] += 1

            if response.status_code == 200:
                results["successful"] += 1
                results["components_tested"].update(
                    ["Carousel", "CarouselCard", "NestedWidget"]
                )
                logger.info("  ✅ Carousel: %s", meta.get("dsl"))
            else:
                results["failed"] += 1
                logger.error("  ❌ Carousel: %s", response.text[:80])

        except Exception as e:
            results["failed"] += 1
            logger.exception("  ❌ Carousel failed")

    # Calculate coverage
    results["components_tested"] = set(results["components_tested"])
    results["components_missing"] = TESTABLE_COMPONENTS - results["components_tested"]
    coverage = len(results["components_tested"]) / len(TESTABLE_COMPONENTS) * 100

    # Summary
    logger.info("%s", "=" * 60)
    logger.info("STRESS TEST COMPLETE")
    logger.info("%s", "=" * 60)
    logger.info(
        "Cards: %s/%s successful",
        results["successful"],
        results["total_cards"],
    )
    logger.info(
        "Coverage: %s/%s components (%.1f%%)",
        len(results["components_tested"]),
        len(TESTABLE_COMPONENTS),
        coverage,
    )
    logger.info("Tested: %s", sorted(results["components_tested"]))
    if results["components_missing"]:
        logger.warning("Missing: %s", sorted(results["components_missing"]))
    if results["errors"]:
        logger.error("Errors (%s):", len(results["errors"]))
        for err in results["errors"][:5]:
            logger.error("  - %s", err)

    return results


def run_dag_stress_test(
    webhook_url: str,
    fill_probability: float = 0.8,
) -> Dict[str, Any]:
    """
    Run comprehensive stress test covering ALL components.

    Args:
        webhook_url: Google Chat webhook URL
        fill_probability: Probability of filling optional fields

    Returns:
        Dict with test results and coverage stats
    """
    import asyncio

    return asyncio.run(run_stress_test(webhook_url, fill_probability))


def run_dag_smoke_test(
    webhook_url: str,
    count: int = 5,
    use_parameterized: bool = True,
) -> None:
    """
    Run a DAG-based smoke test against a webhook.

    Args:
        webhook_url: Google Chat webhook URL
        count: Number of cards to generate and send
        use_parameterized: Use full parameter generation (richer test values)
    """
    import asyncio

    mode = "parameterized" if use_parameterized else "basic"
    logger.info("🔥 Running DAG smoke test (%s): %s cards", mode, count)
    logger.info("   Webhook: %s...", webhook_url[:50])

    if use_parameterized:
        results = asyncio.run(
            send_parameterized_cards_to_webhook(
                webhook_url,
                count=count,
                include_carousel=True,
                fill_optional_probability=0.5,
            )
        )
    else:
        results = asyncio.run(
            send_dag_cards_to_webhook(
                webhook_url,
                count=count,
                include_carousel=True,
            )
        )

    # Summary
    success = sum(1 for r in results if r["success"])
    logger.info("%s", "=" * 60)
    logger.info("DAG SMOKE TEST: %s/%s cards sent successfully", success, len(results))
    logger.info("%s", "=" * 60)

    for r in results:
        status = "✅" if r["success"] else "❌"
        stats_str = ""
        if r.get("parameter_stats"):
            ps = r["parameter_stats"]
            stats_str = f" [{ps['with_values']}/{ps['total']} params]"
        logger.info(
            "  %s #%s: %s%s",
            status,
            r["index"],
            r["dsl"],
            stats_str,
        )
        if r["error"]:
            logger.error("      Error: %s", r["error"][:60])


# =============================================================================
# CLI ENTRY POINT
# =============================================================================

if __name__ == "__main__":
    import json
    import os
    import sys

    # Check for webhook URL in args or env
    webhook_url = None
    count = 3

    if len(sys.argv) >= 2:
        webhook_url = sys.argv[1]
    else:
        webhook_url = os.environ.get("MCP_CHAT_WEBHOOK")

    if len(sys.argv) >= 3:
        count = int(sys.argv[2])

    if webhook_url:
        # Run smoke test with webhook (uses parameterized mode by default)
        run_dag_smoke_test(webhook_url, count, use_parameterized=True)
    else:
        # Demo mode - just print structures
        logger.info("=== DAG Structure Generator Demo ===")
        logger.info("(Set MCP_CHAT_WEBHOOK env var or pass URL to send cards)")

        gen = DAGStructureGenerator()

        # Generate random structures (basic)
        logger.info("Generating random structures (basic)...")

        for i in range(3):
            structure = gen.generate_random_structure("Section")
            logger.info("%s. DSL: %s", i + 1, structure.dsl)
            logger.info("   Components: %s", structure.components)
            logger.info("   Depth: %s, Valid: %s", structure.depth, structure.is_valid)

        # Generate parameterized structures
        logger.info("Generating parameterized structures...")

        for i in range(3):
            structure = gen.generate_parameterized_structure(
                "Section",
                fill_optional_probability=0.5,
            )
            logger.info("%s. DSL: %s", i + 1, structure.dsl)
            logger.info("   Components: %s", structure.components)
            logger.info(
                "   Parameters: %s/%s filled",
                structure.parameters_with_values,
                structure.total_parameters,
            )
            logger.info("   Using defaults: %s", structure.parameters_using_defaults)

        # Generate a Carousel
        logger.info("Generating Carousel structure...")
        carousel = gen.generate_random_structure("Carousel")
        logger.info("   DSL: %s", carousel.dsl)
        logger.info("   Components: %s", carousel.components)

        # Generate a parameterized card
        logger.info("Generating parameterized card...")
        card = gen.generate_parameterized_card(
            "Section", ["DecoratedText", "ButtonList"]
        )
        meta = card.pop("_dag_gen_meta", {})

        logger.info("Card DSL: %s", meta.get("dsl"))
        logger.info("Components: %s", meta.get("components"))
        param_stats = meta.get("parameter_stats", {})
        logger.info(
            "Parameter stats: %s/%s filled",
            param_stats.get("with_values", 0),
            param_stats.get("total", 0),
        )
        logger.info("Card JSON (first section):")
        # Handle both snake_case and camelCase field names
        cards = card.get("cards_v2") or card.get("cardsV2") or []
        if cards:
            logger.info(
                "\n%s",
                json.dumps(cards[0]["card"]["sections"][0], indent=2),
            )
