"""
Exploration Engine for the VenomQA State Explorer module.

This module provides the ExplorationEngine class which implements the core
exploration algorithms. It supports multiple exploration strategies including
Breadth-First Search (BFS), Depth-First Search (DFS), and random walk.

The engine manages the exploration process, tracking visited states and
transitions, and handling the execution of actions.

Example:
    >>> from venomqa.explorer.engine import ExplorationEngine, ExplorationStrategy
    >>> from venomqa.explorer.models import State, Action, ExplorationConfig
    >>> from venomqa.explorer.detector import StateDetector
    >>>
    >>> # Create engine with VenomQA client
    >>> config = ExplorationConfig(max_depth=5, max_states=50)
    >>> engine = ExplorationEngine(
    ...     config=config,
    ...     strategy=ExplorationStrategy.BFS,
    ...     base_url="http://api.example.com",
    ... )
    >>>
    >>> # Set up state detector
    >>> detector = StateDetector()
    >>> engine.set_state_detector(detector.detect_state)
    >>>
    >>> # Define initial state with available actions
    >>> initial_state = State(
    ...     id="initial",
    ...     name="Initial State",
    ...     available_actions=[
    ...         Action(method="GET", endpoint="/api/users"),
    ...         Action(method="GET", endpoint="/api/items"),
    ...     ]
    ... )
    >>>
    >>> # Run synchronous BFS exploration
    >>> graph = engine.explore_bfs(initial_state)
    >>> print(f"Found {len(graph.states)} states")
"""

from __future__ import annotations

import logging
import random
import time
from collections import deque
from collections.abc import Callable, Coroutine
from datetime import datetime
from enum import Enum
from typing import Any

import httpx

from venomqa.explorer.context import (
    ExplorationContext,
    extract_context_from_response,
    generate_state_name,
    substitute_path_params,
)
from venomqa.explorer.models import (
    Action,
    ChainState,
    CoverageReport,
    ExplorationConfig,
    Issue,
    IssueSeverity,
    State,
    StateGraph,
    StateID,
    Transition,
)

logger = logging.getLogger(__name__)


class ExplorationStrategy(str, Enum):
    """Supported exploration strategies."""

    BFS = "bfs"  # Breadth-First Search - explores level by level
    DFS = "dfs"  # Depth-First Search - explores deeply first
    RANDOM = "random"  # Random walk - randomly selects next action
    GREEDY = "greedy"  # Greedy - prioritizes unexplored actions
    HYBRID = "hybrid"  # Hybrid - combines multiple strategies


class ExplorationError(Exception):
    """Exception raised when exploration fails."""

    def __init__(
        self,
        message: str,
        state_id: str | None = None,
        action: Action | None = None,
        cause: Exception | None = None,
    ) -> None:
        """Initialize with message and context."""
        super().__init__(message)
        self.message = message
        self.state_id = state_id
        self.action = action
        self.cause = cause


class ExplorationEngine:
    """
    Core exploration engine implementing state space traversal.

    The ExplorationEngine is responsible for systematically exploring
    the application's state space by executing actions and tracking
    the resulting state transitions.

    Features:
    - Multiple exploration strategies (BFS, DFS, Random, Greedy, Hybrid)
    - Configurable depth and breadth limits
    - Action execution with error handling
    - Automatic state detection and graph building
    - Issue detection during exploration
    - Cycle detection to avoid infinite loops

    Attributes:
        config: Exploration configuration
        strategy: Current exploration strategy
        graph: The state graph being built
        issues: List of discovered issues
        visited_states: Set of visited state IDs
        visited_transitions: Set of explored transitions
        action_executor: Function to execute actions
        state_detector: Function to detect state from responses

    Example:
        engine = ExplorationEngine(strategy=ExplorationStrategy.BFS)
        engine.set_action_executor(http_client.execute)
        engine.set_state_detector(detector.detect_state)
        await engine.explore(initial_state)
    """

    def __init__(
        self,
        config: ExplorationConfig | None = None,
        strategy: ExplorationStrategy = ExplorationStrategy.BFS,
        base_url: str | None = None,
        http_client: httpx.AsyncClient | None = None,
    ) -> None:
        """
        Initialize the exploration engine.

        Args:
            config: Exploration configuration
            strategy: Exploration strategy to use
            base_url: Base URL for HTTP requests (used if no action executor set)
            http_client: Optional httpx AsyncClient for HTTP requests
        """
        self.config = config or ExplorationConfig()
        self.strategy = strategy
        self.base_url = base_url or ""
        self._http_client = http_client
        self._owns_client = False
        self.graph = StateGraph()
        self.issues: list[Issue] = []
        self.visited_states: set[StateID] = set()
        self.visited_transitions: set[tuple[StateID, str, str]] = set()
        self._action_executor: Callable[[Action], Any | Coroutine[Any, Any, Any]] | None = None
        self._state_detector: Callable[[dict[str, Any], str | None, str | None], State] | None = None
        self._exploration_queue: deque[tuple[State, int]] = deque()
        self._exploration_stack: list[tuple[State, int]] = []
        self._current_depth = 0
        self._all_discovered_actions: set[Action] = set()
        self._executed_actions: set[Action] = set()
        # For sync exploration with VenomQA client
        self._venomqa_sync_client: Any | None = None

    def set_action_executor(
        self,
        executor: Callable[[Action], Any],
    ) -> None:
        """
        Set the function used to execute actions.

        Args:
            executor: Async function that takes an Action and returns response data
        """
        self._action_executor = executor

    def set_state_detector(
        self,
        detector: Callable[[dict[str, Any], str | None, str | None], State],
    ) -> None:
        """
        Set the function used to detect state from responses.

        Args:
            detector: Function that takes response data and returns a State
        """
        self._state_detector = detector

    async def _ensure_http_client(self) -> httpx.AsyncClient:
        """Ensure we have an HTTP client available."""
        if self._http_client is None:
            self._http_client = httpx.AsyncClient(
                base_url=self.base_url,
                timeout=self.config.request_timeout_seconds,
                follow_redirects=self.config.follow_redirects,
                verify=self.config.verify_ssl,
                headers=self.config.headers,
            )
            self._owns_client = True
        return self._http_client

    async def _close_http_client(self) -> None:
        """Close the HTTP client if we own it."""
        if self._owns_client and self._http_client is not None:
            await self._http_client.aclose()
            self._http_client = None
            self._owns_client = False

    async def explore(
        self,
        initial_state: State,
        initial_actions: list[Action] | None = None,
    ) -> StateGraph:
        """
        Start exploration from an initial state.

        Args:
            initial_state: The starting state for exploration
            initial_actions: Optional list of initial actions to try

        Returns:
            The completed StateGraph

        Raises:
            ExplorationError: If exploration fails
            ValueError: If required components are not configured
        """
        # Add initial state to graph
        self.graph.add_state(initial_state)
        self.visited_states.add(initial_state.id)

        # Track all discovered actions
        for action in initial_state.available_actions:
            self._all_discovered_actions.add(action)

        # Add any additional initial actions
        if initial_actions:
            for action in initial_actions:
                if action not in initial_state.available_actions:
                    initial_state.available_actions.append(action)
                    self._all_discovered_actions.add(action)

        try:
            # Execute exploration based on strategy
            if self.strategy == ExplorationStrategy.BFS:
                await self._explore_bfs(initial_state)
            elif self.strategy == ExplorationStrategy.DFS:
                await self._explore_dfs(initial_state)
            elif self.strategy == ExplorationStrategy.RANDOM:
                await self._explore_random(initial_state)
            elif self.strategy == ExplorationStrategy.GREEDY:
                await self._explore_greedy(initial_state)
            elif self.strategy == ExplorationStrategy.HYBRID:
                await self._explore_hybrid(initial_state)
            else:
                # Default to BFS
                await self._explore_bfs(initial_state)
        finally:
            await self._close_http_client()

        return self.graph

    async def explore_from_state(
        self,
        state: State,
        depth: int = 0,
    ) -> list[tuple[State, Transition]]:
        """
        Explore from a specific state.

        Args:
            state: The state to explore from
            depth: Current exploration depth

        Returns:
            List of (new_state, transition) tuples discovered
        """
        discovered: list[tuple[State, Transition]] = []

        # Check depth limits
        if depth >= self.config.max_depth:
            logger.debug(f"Max depth {self.config.max_depth} reached at state {state.id}")
            return discovered

        # Check other limits
        if not self._check_limits():
            logger.debug("Exploration limits reached")
            return discovered

        self._current_depth = max(self._current_depth, depth)

        # Get available actions from the state
        available_actions = state.available_actions

        for action in available_actions:
            # Check if we should skip this action based on patterns
            if not self._should_explore_action(action):
                continue

            # Check if this action from this state has been executed
            action_key = (state.id, f"{action.method}:{action.endpoint}")
            if action_key in [(t[0], t[1]) for t in self.visited_transitions]:
                # We've already tried this action from this state
                continue

            # Execute the action
            result_state, transition = await self.execute_action(action, state)

            if result_state and transition:
                discovered.append((result_state, transition))

                # Mark transition as visited
                self._mark_transition_visited(state.id, action, result_state.id)
                self._executed_actions.add(action)

                # Track new actions from the result state
                for new_action in result_state.available_actions:
                    self._all_discovered_actions.add(new_action)

            # Check limits after each action
            if not self._check_limits():
                break

        return discovered

    async def execute_action(
        self,
        action: Action,
        from_state: State,
    ) -> tuple[State | None, Transition | None]:
        """
        Execute an action and detect the resulting state.

        Args:
            action: The action to execute
            from_state: The current state before action

        Returns:
            Tuple of (resulting state, transition) or (None, None) on failure
        """
        start_time = time.time()
        response_data: dict[str, Any] = {}
        status_code: int | None = None
        error_msg: str | None = None
        success = True

        try:
            # Use custom action executor if provided
            if self._action_executor:
                response = await self._action_executor(action)
                if isinstance(response, dict):
                    response_data = response
                    status_code = response.get("status_code")
                else:
                    # Try to extract data from response object
                    if hasattr(response, "json"):
                        try:
                            response_data = response.json()
                        except Exception:
                            response_data = {"raw": str(response)}
                    if hasattr(response, "status_code"):
                        status_code = response.status_code
            else:
                # Use built-in HTTP client
                response_data, status_code = await self._execute_http_action(action)

            # Check for error status codes
            if status_code and status_code >= 400:
                success = False
                error_msg = f"HTTP {status_code}"
                self._record_issue(
                    severity=IssueSeverity.MEDIUM if status_code < 500 else IssueSeverity.HIGH,
                    error=f"Action {action.method} {action.endpoint} returned {status_code}",
                    state=from_state.id,
                    action=action,
                    suggestion="Check if the endpoint requires authentication or different parameters",
                )

        except httpx.TimeoutException as e:
            success = False
            error_msg = f"Timeout: {e}"
            self._record_issue(
                severity=IssueSeverity.MEDIUM,
                error=f"Timeout executing {action.method} {action.endpoint}",
                state=from_state.id,
                action=action,
                suggestion="Consider increasing request timeout",
            )
        except httpx.HTTPStatusError as e:
            success = False
            status_code = e.response.status_code
            error_msg = f"HTTP error: {e}"
            self._record_issue(
                severity=IssueSeverity.HIGH,
                error=str(e),
                state=from_state.id,
                action=action,
            )
        except Exception as e:
            success = False
            error_msg = str(e)
            self._record_issue(
                severity=IssueSeverity.HIGH,
                error=f"Failed to execute {action.method} {action.endpoint}: {e}",
                state=from_state.id,
                action=action,
            )
            logger.exception(f"Error executing action: {action}")

        duration_ms = (time.time() - start_time) * 1000

        # Detect resulting state
        result_state: State | None = None
        if self._state_detector:
            try:
                result_state = self._state_detector(
                    response_data,
                    action.endpoint,
                    str(status_code) if status_code else None,
                )
            except Exception as e:
                logger.warning(f"State detection failed: {e}")

        if result_state is None:
            # Create a default state based on the response
            result_state = self._create_default_state(
                action, from_state, response_data, status_code, success
            )

        # Add state to graph if new
        if result_state.id not in self.visited_states:
            self.graph.add_state(result_state)
            self.visited_states.add(result_state.id)

        # Create transition
        transition = Transition(
            from_state=from_state.id,
            action=action,
            to_state=result_state.id,
            response=response_data,
            status_code=status_code,
            duration_ms=duration_ms,
            success=success,
            error=error_msg,
            discovered_at=datetime.now(),
        )

        # Add transition to graph
        self.graph.add_transition(transition)

        return result_state, transition

    async def _execute_http_action(
        self, action: Action
    ) -> tuple[dict[str, Any], int]:
        """
        Execute an HTTP action using the built-in client.

        Args:
            action: The action to execute

        Returns:
            Tuple of (response_data, status_code)
        """
        client = await self._ensure_http_client()

        # Build URL
        url = action.endpoint
        if not url.startswith("http"):
            url = f"{self.base_url.rstrip('/')}/{url.lstrip('/')}"

        # Build headers
        headers = dict(self.config.headers)
        if action.headers:
            headers.update(action.headers)
        if self.config.auth_token:
            headers["Authorization"] = f"Bearer {self.config.auth_token}"

        # Execute request
        response = await client.request(
            method=action.method,
            url=url,
            params=action.params,
            json=action.body,
            headers=headers,
        )

        # Parse response
        try:
            response_data = response.json()
        except Exception:
            response_data = {"raw": response.text}

        return response_data, response.status_code

    def _create_default_state(
        self,
        action: Action,
        from_state: State,
        response_data: dict[str, Any],
        status_code: int | None,
        success: bool,
    ) -> State:
        """
        Create a default state based on response characteristics.

        Args:
            action: The action that was executed
            from_state: The state the action was executed from
            response_data: The response data
            status_code: HTTP status code
            success: Whether the action succeeded

        Returns:
            A new State object
        """
        # Generate state ID based on action and response characteristics
        if not success:
            state_id = f"error_{status_code}_{action.endpoint.replace('/', '_')}"
            state_name = f"Error State ({status_code})"
        else:
            # Try to derive state from response
            state_type = response_data.get("type", response_data.get("state", ""))
            if state_type:
                state_id = f"state_{state_type}"
                state_name = f"State: {state_type}"
            else:
                # Use a hash of relevant response properties
                import hashlib
                content_hash = hashlib.md5(
                    str(sorted(response_data.keys())).encode()
                ).hexdigest()[:8]
                state_id = f"state_{action.endpoint.replace('/', '_')}_{content_hash}"
                state_name = f"State after {action.method} {action.endpoint}"

        # Determine available actions from the response (if any)
        available_actions: list[Action] = []

        # Look for links/actions in the response (HATEOAS pattern)
        links = response_data.get("_links", response_data.get("links", []))
        if isinstance(links, dict):
            for rel, link_data in links.items():
                if isinstance(link_data, dict) and "href" in link_data:
                    available_actions.append(
                        Action(
                            method=link_data.get("method", "GET"),
                            endpoint=link_data["href"],
                            description=rel,
                        )
                    )
                elif isinstance(link_data, str):
                    available_actions.append(
                        Action(
                            method="GET",
                            endpoint=link_data,
                            description=rel,
                        )
                    )
        elif isinstance(links, list):
            for link in links:
                if isinstance(link, dict) and "href" in link:
                    available_actions.append(
                        Action(
                            method=link.get("method", "GET"),
                            endpoint=link["href"],
                            description=link.get("rel", ""),
                        )
                    )

        return State(
            id=state_id,
            name=state_name,
            properties={
                "status_code": status_code,
                "success": success,
                "from_action": f"{action.method} {action.endpoint}",
            },
            available_actions=available_actions,
            metadata={"response_keys": list(response_data.keys())},
            discovered_at=datetime.now(),
        )

    def _should_explore_action(self, action: Action) -> bool:
        """
        Check if an action should be explored based on include/exclude patterns.

        Args:
            action: The action to check

        Returns:
            True if the action should be explored
        """
        endpoint = action.endpoint

        # Check exclude patterns
        for pattern in self.config.exclude_patterns:
            if pattern in endpoint or endpoint.startswith(pattern):
                return False

        # Check include patterns (if any)
        if self.config.include_patterns:
            for pattern in self.config.include_patterns:
                if pattern in endpoint or endpoint.startswith(pattern):
                    return True
            return False

        return True

    async def _explore_bfs(self, initial_state: State) -> None:
        """
        Implement Breadth-First Search exploration.

        BFS explores all states at the current depth before moving deeper.
        This is useful for finding the shortest paths to states.

        Args:
            initial_state: The starting state for exploration
        """
        # Initialize queue with initial state at depth 0
        self._exploration_queue.clear()
        self._exploration_queue.append((initial_state, 0))

        while self._exploration_queue:
            # Check overall limits
            if not self._check_limits():
                logger.info("Exploration limits reached, stopping BFS")
                break

            # Get next state from queue (FIFO for BFS)
            current_state, depth = self._exploration_queue.popleft()

            # Skip if we've exceeded depth
            if depth >= self.config.max_depth:
                continue

            # Explore from current state
            discovered = await self.explore_from_state(current_state, depth)

            # Add newly discovered states to queue
            for new_state, _ in discovered:
                if new_state.id not in self.visited_states:
                    self._exploration_queue.append((new_state, depth + 1))
                elif new_state.available_actions:
                    # Even if visited, might have new actions to explore
                    # Check if there are unexplored actions
                    for action in new_state.available_actions:
                        key = (new_state.id, f"{action.method}:{action.endpoint}")
                        if key not in [(t[0], t[1]) for t in self.visited_transitions]:
                            self._exploration_queue.append((new_state, depth + 1))
                            break

    async def _explore_dfs(self, initial_state: State) -> None:
        """
        Implement Depth-First Search exploration.

        DFS explores as deeply as possible before backtracking.
        This is useful for finding deep state sequences.

        Args:
            initial_state: The starting state for exploration
        """
        # Initialize stack with initial state at depth 0
        self._exploration_stack.clear()
        self._exploration_stack.append((initial_state, 0))

        while self._exploration_stack:
            # Check overall limits
            if not self._check_limits():
                logger.info("Exploration limits reached, stopping DFS")
                break

            # Get next state from stack (LIFO for DFS)
            current_state, depth = self._exploration_stack.pop()

            # Skip if we've exceeded depth
            if depth >= self.config.max_depth:
                continue

            # Explore from current state
            discovered = await self.explore_from_state(current_state, depth)

            # Add newly discovered states to stack (reverse order to maintain left-to-right exploration)
            for new_state, _ in reversed(discovered):
                if new_state.id not in self.visited_states:
                    self._exploration_stack.append((new_state, depth + 1))

    async def _explore_random(self, initial_state: State) -> None:
        """
        Implement random walk exploration.

        Random walk randomly selects actions to execute.
        This can help discover unexpected state paths.

        Args:
            initial_state: The starting state for exploration
        """
        current_state = initial_state
        depth = 0
        max_iterations = self.config.max_states * 2  # Limit iterations

        for _ in range(max_iterations):
            # Check limits
            if not self._check_limits():
                break

            if depth >= self.config.max_depth:
                # Reset to initial state for another random walk
                current_state = initial_state
                depth = 0
                continue

            # Get unexplored actions from current state
            unexplored_actions = []
            for action in current_state.available_actions:
                key = (current_state.id, f"{action.method}:{action.endpoint}")
                if key not in [(t[0], t[1]) for t in self.visited_transitions]:
                    unexplored_actions.append(action)

            if not unexplored_actions:
                # No more unexplored actions, pick a random visited state
                if self.graph.states:
                    random_state_id = random.choice(list(self.graph.states.keys()))
                    current_state = self.graph.states[random_state_id]
                    depth = 0
                else:
                    break
                continue

            # Randomly select an action
            action = random.choice(unexplored_actions)

            # Execute the action
            result_state, transition = await self.execute_action(action, current_state)

            if result_state and transition:
                self._mark_transition_visited(current_state.id, action, result_state.id)
                self._executed_actions.add(action)
                current_state = result_state
                depth += 1
            else:
                # Action failed, try another
                continue

    async def _explore_greedy(self, initial_state: State) -> None:
        """
        Implement greedy exploration.

        Greedy exploration prioritizes unexplored actions and states.
        This maximizes coverage quickly.

        Args:
            initial_state: The starting state for exploration
        """
        # Use a priority-based approach: states with more unexplored actions get priority
        states_to_explore: list[tuple[int, State, int]] = [
            (-len(initial_state.available_actions), initial_state, 0)
        ]

        while states_to_explore:
            if not self._check_limits():
                break

            # Sort by priority (negative count means more actions = higher priority)
            states_to_explore.sort(key=lambda x: x[0])

            _, current_state, depth = states_to_explore.pop(0)

            if depth >= self.config.max_depth:
                continue

            # Explore from current state
            discovered = await self.explore_from_state(current_state, depth)

            # Add discovered states with their priority
            for new_state, _ in discovered:
                if new_state.id not in self.visited_states:
                    # Count unexplored actions
                    unexplored_count = 0
                    for action in new_state.available_actions:
                        key = (new_state.id, f"{action.method}:{action.endpoint}")
                        if key not in [(t[0], t[1]) for t in self.visited_transitions]:
                            unexplored_count += 1
                    priority = -unexplored_count
                    states_to_explore.append((priority, new_state, depth + 1))

    async def _explore_hybrid(self, initial_state: State) -> None:
        """
        Implement hybrid exploration combining multiple strategies.

        Uses BFS for initial breadth, then switches to greedy for deep exploration.

        Args:
            initial_state: The starting state for exploration
        """
        # First, do shallow BFS exploration (up to depth 2)
        original_max_depth = self.config.max_depth
        self.config.max_depth = min(2, original_max_depth)
        await self._explore_bfs(initial_state)

        # Then, use greedy exploration for the rest
        self.config.max_depth = original_max_depth
        await self._explore_greedy(initial_state)

    def _record_issue(
        self,
        severity: IssueSeverity,
        error: str,
        state: StateID | None = None,
        action: Action | None = None,
        suggestion: str | None = None,
    ) -> None:
        """
        Record an issue discovered during exploration.

        Args:
            severity: Issue severity level
            error: Error description
            state: State where issue occurred
            action: Action that triggered the issue
            suggestion: Suggested fix
        """
        issue = Issue(
            severity=severity,
            state=state,
            action=action,
            error=error,
            suggestion=suggestion,
            discovered_at=datetime.now(),
        )
        self.issues.append(issue)

    def _is_transition_visited(
        self,
        from_state: StateID,
        action: Action,
        to_state: StateID,
    ) -> bool:
        """
        Check if a transition has already been visited.

        Args:
            from_state: Source state ID
            action: The action
            to_state: Destination state ID

        Returns:
            True if transition was already visited
        """
        key = (from_state, f"{action.method}:{action.endpoint}", to_state)
        return key in self.visited_transitions

    def _mark_transition_visited(
        self,
        from_state: StateID,
        action: Action,
        to_state: StateID,
    ) -> None:
        """
        Mark a transition as visited.

        Args:
            from_state: Source state ID
            action: The action
            to_state: Destination state ID
        """
        key = (from_state, f"{action.method}:{action.endpoint}", to_state)
        self.visited_transitions.add(key)

    def _check_limits(self) -> bool:
        """
        Check if exploration limits have been reached.

        Returns:
            True if exploration should continue, False if limits reached
        """
        if len(self.visited_states) >= self.config.max_states:
            return False
        if len(self.visited_transitions) >= self.config.max_transitions:
            return False
        if self._current_depth >= self.config.max_depth:
            return False
        return True

    def get_coverage_report(self) -> CoverageReport:
        """
        Generate a coverage report from exploration results.

        Returns:
            CoverageReport with exploration metrics
        """
        # Calculate unique endpoints
        endpoints_discovered: set[str] = set()
        endpoints_tested: set[str] = set()

        for action in self._all_discovered_actions:
            endpoints_discovered.add(action.endpoint)

        for action in self._executed_actions:
            endpoints_tested.add(action.endpoint)

        # Calculate coverage percentage
        if endpoints_discovered:
            coverage_percent = (len(endpoints_tested) / len(endpoints_discovered)) * 100
        else:
            coverage_percent = 0.0

        # Find uncovered actions
        uncovered_actions = [
            action for action in self._all_discovered_actions
            if action not in self._executed_actions
        ]

        # State breakdown
        state_breakdown: dict[str, int] = {}
        for state in self.graph.states.values():
            state_type = state.properties.get("success", True)
            key = "success" if state_type else "error"
            state_breakdown[key] = state_breakdown.get(key, 0) + 1

        # Transition breakdown
        transition_breakdown: dict[str, int] = {}
        for transition in self.graph.transitions:
            key = "success" if transition.success else "failed"
            transition_breakdown[key] = transition_breakdown.get(key, 0) + 1

        return CoverageReport(
            states_found=len(self.visited_states),
            transitions_found=len(self.visited_transitions),
            endpoints_discovered=len(endpoints_discovered),
            endpoints_tested=len(endpoints_tested),
            coverage_percent=min(100.0, coverage_percent),
            uncovered_actions=uncovered_actions,
            state_breakdown=state_breakdown,
            transition_breakdown=transition_breakdown,
        )

    def reset(self) -> None:
        """Reset the engine state for a new exploration."""
        self.graph = StateGraph()
        self.issues.clear()
        self.visited_states.clear()
        self.visited_transitions.clear()
        self._exploration_queue.clear()
        self._exploration_stack.clear()
        self._current_depth = 0
        self._all_discovered_actions.clear()
        self._executed_actions.clear()

    # =========================================================================
    # SYNCHRONOUS BFS EXPLORATION API
    # =========================================================================

    def explore_bfs(self, initial_state: State) -> StateGraph:
        """
        Synchronous BFS exploration - the main entry point for sync usage.

        This method implements Breadth-First Search exploration synchronously,
        making real HTTP calls to explore the API state space.

        The exploration:
        1. Starts from the initial state
        2. Executes all available actions from the current state
        3. Detects resulting states from responses
        4. Records transitions in the state graph
        5. Continues until max_states reached or no more actions

        Args:
            initial_state: The starting state with available actions

        Returns:
            StateGraph: The explored state graph with all states and transitions

        Raises:
            ExplorationError: If exploration fails critically

        Example:
            >>> from venomqa.explorer.engine import ExplorationEngine
            >>> from venomqa.explorer.models import State, Action, ExplorationConfig
            >>>
            >>> config = ExplorationConfig(max_states=10, max_depth=3)
            >>> engine = ExplorationEngine(
            ...     config=config,
            ...     base_url="http://localhost:8000",
            ... )
            >>>
            >>> initial = State(
            ...     id="initial",
            ...     name="Initial",
            ...     available_actions=[
            ...         Action(method="GET", endpoint="/api/users"),
            ...     ]
            ... )
            >>>
            >>> graph = engine.explore_bfs(initial)
            >>> print(f"Found {len(graph.states)} states")
        """
        # Reset state for new exploration
        self.reset()

        # Initialize queue with (state, depth)
        queue: deque[tuple[State, int]] = deque()
        queue.append((initial_state, 0))

        # Add initial state to graph
        self.graph.add_state(initial_state)
        self.visited_states.add(initial_state.id)

        # Track discovered actions
        for action in initial_state.available_actions:
            self._all_discovered_actions.add(action)

        max_states = self.config.max_states

        while queue and len(self.visited_states) < max_states:
            current_state, depth = queue.popleft()

            # Skip if already at max depth
            if depth >= self.config.max_depth:
                continue

            # Process each available action
            for action in current_state.available_actions:
                # Check if we should explore this action
                if not self._should_explore_action(action):
                    continue

                # Check if we've already executed this action from this state
                action_key = (current_state.id, f"{action.method}:{action.endpoint}")
                if any(t[0] == action_key[0] and t[1] == action_key[1]
                       for t in self.visited_transitions):
                    continue

                # Check state limit before executing
                if len(self.visited_states) >= max_states:
                    break

                try:
                    # Execute the action synchronously
                    response = self._execute_action_sync(action)

                    # Detect the resulting state
                    new_state = self._detect_state_from_response(
                        response,
                        action,
                        current_state,
                    )

                    # Create and record transition
                    transition = Transition(
                        from_state=current_state.id,
                        action=action,
                        to_state=new_state.id,
                        response=response.get("data", response),
                        status_code=response.get("status_code"),
                        duration_ms=response.get("duration_ms", 0.0),
                        success=response.get("success", True),
                        error=response.get("error"),
                        discovered_at=datetime.now(),
                    )
                    self.graph.add_transition(transition)

                    # Mark transition as visited
                    self._mark_transition_visited(
                        current_state.id, action, new_state.id
                    )
                    self._executed_actions.add(action)

                    # Add new state to graph and queue if not visited
                    if new_state.id not in self.visited_states:
                        self.graph.add_state(new_state)
                        self.visited_states.add(new_state.id)
                        queue.append((new_state, depth + 1))

                        # Track new actions
                        for new_action in new_state.available_actions:
                            self._all_discovered_actions.add(new_action)

                except Exception as e:
                    # Record the issue but continue exploring
                    self._record_issue(
                        severity=IssueSeverity.HIGH,
                        error=f"Failed to execute {action.method} {action.endpoint}: {e}",
                        state=current_state.id,
                        action=action,
                        suggestion="Check if the endpoint is reachable",
                    )
                    logger.warning(
                        f"Action execution failed: {action.method} {action.endpoint}: {e}"
                    )

        return self.graph

    def _execute_action_sync(self, action: Action) -> dict[str, Any]:
        """
        Execute an action synchronously using httpx.

        This method makes actual HTTP requests to the configured base_url.

        Args:
            action: The action to execute

        Returns:
            Dict containing response data, status_code, duration_ms, success, and error

        Raises:
            ExplorationError: If the request fails critically
        """
        start_time = time.time()
        result: dict[str, Any] = {
            "data": {},
            "status_code": None,
            "duration_ms": 0.0,
            "success": True,
            "error": None,
        }

        # Build URL
        url = action.endpoint
        if not url.startswith("http"):
            url = f"{self.base_url.rstrip('/')}/{url.lstrip('/')}"

        # Build headers
        headers = dict(self.config.headers)
        if action.headers:
            headers.update(action.headers)
        if self.config.auth_token:
            headers["Authorization"] = f"Bearer {self.config.auth_token}"

        try:
            # Use httpx for synchronous requests
            with httpx.Client(
                timeout=self.config.request_timeout_seconds,
                follow_redirects=self.config.follow_redirects,
                verify=self.config.verify_ssl,
            ) as client:
                response = client.request(
                    method=action.method,
                    url=url,
                    params=action.params,
                    json=action.body,
                    headers=headers,
                )

                result["status_code"] = response.status_code

                # Parse response body
                try:
                    result["data"] = response.json()
                except Exception:
                    result["data"] = {"raw": response.text}

                # Check for HTTP errors
                if response.status_code >= 400:
                    result["success"] = False
                    result["error"] = f"HTTP {response.status_code}"

        except httpx.TimeoutException as e:
            result["success"] = False
            result["error"] = f"Timeout: {e}"
            logger.warning(f"Request timeout for {action.method} {url}")

        except httpx.RequestError as e:
            result["success"] = False
            result["error"] = f"Request error: {e}"
            logger.warning(f"Request error for {action.method} {url}: {e}")

        except Exception as e:
            result["success"] = False
            result["error"] = str(e)
            raise ExplorationError(
                f"Failed to execute action: {e}",
                action=action,
                cause=e,
            )

        finally:
            result["duration_ms"] = (time.time() - start_time) * 1000

        return result

    def _detect_state_from_response(
        self,
        response: dict[str, Any],
        action: Action,
        from_state: State,
    ) -> State:
        """
        Detect state from an HTTP response.

        Uses the configured state detector if available, otherwise creates
        a default state based on response characteristics.

        Args:
            response: The response dict from _execute_action_sync
            action: The action that was executed
            from_state: The state from which the action was executed

        Returns:
            The detected or created State
        """
        response_data = response.get("data", {})
        status_code = response.get("status_code")
        success = response.get("success", True)

        # Try custom state detector first
        if self._state_detector:
            try:
                detected = self._state_detector(
                    response_data,
                    action.endpoint,
                    str(status_code) if status_code else None,
                )
                if detected:
                    return detected
            except Exception as e:
                logger.warning(f"State detector failed: {e}")

        # Fall back to default state creation
        return self._create_default_state(
            action=action,
            from_state=from_state,
            response_data=response_data,
            status_code=status_code,
            success=success,
        )

    def execute_action_sync(
        self,
        action: Action,
        from_state: State | None = None,
    ) -> tuple[State, Transition]:
        """
        Execute a single action synchronously and return the result.

        This is a public API for executing individual actions outside
        of the full exploration loop.

        Args:
            action: The action to execute
            from_state: Optional state context (creates a default if not provided)

        Returns:
            Tuple of (resulting State, Transition)

        Raises:
            ExplorationError: If action execution fails

        Example:
            >>> engine = ExplorationEngine(base_url="http://localhost:8000")
            >>> action = Action(method="GET", endpoint="/api/users")
            >>> state, transition = engine.execute_action_sync(action)
            >>> print(f"Status: {transition.status_code}")
        """
        if from_state is None:
            from_state = State(
                id="synthetic_start",
                name="Synthetic Start State",
                available_actions=[action],
            )

        response = self._execute_action_sync(action)
        new_state = self._detect_state_from_response(response, action, from_state)

        transition = Transition(
            from_state=from_state.id,
            action=action,
            to_state=new_state.id,
            response=response.get("data", {}),
            status_code=response.get("status_code"),
            duration_ms=response.get("duration_ms", 0.0),
            success=response.get("success", True),
            error=response.get("error"),
            discovered_at=datetime.now(),
        )

        return new_state, transition

    def explore_with_venomqa_client(
        self,
        initial_state: State,
        client: Any,
    ) -> StateGraph:
        """
        Synchronous BFS exploration using a VenomQA Client instance.

        This method allows using the full-featured VenomQA HTTP client
        with request history, retry logic, and authentication support.

        Args:
            initial_state: The starting state with available actions
            client: A venomqa.client.Client instance (already connected)

        Returns:
            StateGraph: The explored state graph

        Example:
            >>> from venomqa.client import Client
            >>> from venomqa.explorer.engine import ExplorationEngine
            >>> from venomqa.explorer.models import State, Action
            >>>
            >>> with Client("http://localhost:8000") as client:
            ...     client.set_auth_token("my-token")
            ...     engine = ExplorationEngine()
            ...     initial = State(
            ...         id="init",
            ...         name="Initial",
            ...         available_actions=[Action(method="GET", endpoint="/api/users")]
            ...     )
            ...     graph = engine.explore_with_venomqa_client(initial, client)
        """
        # Store the client for use in action execution
        self._venomqa_sync_client = client

        # Override base_url from client if not set
        if hasattr(client, "base_url") and not self.base_url:
            self.base_url = client.base_url

        # Use explore_bfs with a custom executor
        original_executor = self._execute_action_sync

        def venomqa_executor(action: Action) -> dict[str, Any]:
            """Execute action using VenomQA client."""
            start_time = time.time()
            result: dict[str, Any] = {
                "data": {},
                "status_code": None,
                "duration_ms": 0.0,
                "success": True,
                "error": None,
            }

            try:
                # Use the appropriate HTTP method
                method = action.method.upper()
                path = action.endpoint

                kwargs: dict[str, Any] = {}
                if action.params:
                    kwargs["params"] = action.params
                if action.body:
                    kwargs["json"] = action.body
                if action.headers:
                    kwargs["headers"] = action.headers

                if method == "GET":
                    response = client.get(path, **kwargs)
                elif method == "POST":
                    response = client.post(path, **kwargs)
                elif method == "PUT":
                    response = client.put(path, **kwargs)
                elif method == "PATCH":
                    response = client.patch(path, **kwargs)
                elif method == "DELETE":
                    response = client.delete(path, **kwargs)
                else:
                    response = client.request(method, path, **kwargs)

                result["status_code"] = response.status_code

                try:
                    result["data"] = response.json()
                except Exception:
                    result["data"] = {"raw": response.text}

                if response.status_code >= 400:
                    result["success"] = False
                    result["error"] = f"HTTP {response.status_code}"

            except Exception as e:
                result["success"] = False
                result["error"] = str(e)
                logger.warning(f"VenomQA client error: {e}")

            finally:
                result["duration_ms"] = (time.time() - start_time) * 1000

            return result

        # Temporarily replace the executor
        self._execute_action_sync = venomqa_executor

        try:
            return self.explore_bfs(initial_state)
        finally:
            # Restore original executor
            self._execute_action_sync = original_executor
            self._venomqa_sync_client = None

    @property
    def max_states(self) -> int:
        """Get the maximum number of states to explore."""
        return self.config.max_states

    @max_states.setter
    def max_states(self, value: int) -> None:
        """Set the maximum number of states to explore."""
        self.config.max_states = value

    # =========================================================================
    # CONTEXT-AWARE STATE CHAIN EXPLORATION
    # =========================================================================

    def explore_with_context(
        self,
        initial_actions: list[Action],
        initial_context: dict[str, Any] | None = None,
    ) -> ChainExplorationResult:
        """
        Context-aware BFS exploration that passes context through the chain.

        This is the REAL exploration that mimics how a human QA would test:
        1. Execute an action (e.g., POST /todos)
        2. Extract context from response (e.g., todo_id=42)
        3. Use that context in next actions (e.g., GET /todos/42)
        4. Build a deep, connected state graph

        Key features:
        - Context accumulates through the chain (IDs, tokens, etc.)
        - Path parameters are substituted with real values from context
        - Actions with unresolved path params are skipped (not 404'd)
        - State names are meaningful (e.g., "Anonymous | Todo:42 | Completed")
        - Parent-child relationships are tracked

        Args:
            initial_actions: Actions to start exploration with
            initial_context: Optional initial context (e.g., auth token)

        Returns:
            ChainExplorationResult with the explored graph and chain states

        Example:
            >>> engine = ExplorationEngine(base_url="http://localhost:5001")
            >>> initial_actions = [
            ...     Action(method="GET", endpoint="/health"),
            ...     Action(method="GET", endpoint="/todos"),
            ...     Action(method="POST", endpoint="/todos", body={"title": "Test"}),
            ... ]
            >>> result = engine.explore_with_context(initial_actions)
            >>> for state in result.chain_states.values():
            ...     print(f"{state.name} (depth={state.depth})")
        """
        # Reset state
        self.reset()

        # Initialize context using ExplorationContext
        context = ExplorationContext()
        if initial_context:
            context.update(initial_context)

        # Create initial chain state
        initial_state = ChainState(
            id="initial",
            name=generate_state_name(context, {}),
            context=context.to_dict(),
            response=None,
            available_actions=initial_actions,
            depth=0,
            parent_state=None,
            parent_action=None,
            discovered_at=datetime.now(),
        )

        # Add to graph
        self.graph.add_state(initial_state.to_state())
        self.graph.initial_state = initial_state.id

        # Track chain states separately (with full context)
        chain_states: dict[StateID, ChainState] = {initial_state.id: initial_state}

        # BFS queue: (ChainState, ExplorationContext)
        queue: deque[tuple[ChainState, ExplorationContext]] = deque()
        queue.append((initial_state, context.copy()))

        # Track visited (state_id, action_key) to avoid loops
        visited_actions: set[tuple[StateID, str]] = set()

        # Track skipped actions for reporting
        skipped_actions: list[tuple[Action, str]] = []

        max_states = self.config.max_states
        max_depth = self.config.max_depth

        while queue and len(chain_states) < max_states:
            current_state, current_context = queue.popleft()

            # Skip if at max depth
            if current_state.depth >= max_depth:
                continue

            for action in current_state.available_actions:
                # Check if we should explore this action
                if not self._should_explore_action(action):
                    continue

                # Create action key
                action_key = f"{action.method}:{action.endpoint}"
                visit_key = (current_state.id, action_key)

                if visit_key in visited_actions:
                    continue
                visited_actions.add(visit_key)

                # Substitute path parameters with context values
                resolved_endpoint = substitute_path_params(action.endpoint, current_context)

                if resolved_endpoint is None:
                    # Can't resolve path params - skip this action
                    skipped_actions.append((action, "Unresolved path parameters"))
                    logger.debug(
                        f"Skipping {action.method} {action.endpoint} - "
                        f"can't resolve path params with context: {current_context.to_dict()}"
                    )
                    continue

                # Check state limit
                if len(chain_states) >= max_states:
                    break

                # Create a copy of the action with resolved endpoint
                resolved_action = Action(
                    method=action.method,
                    endpoint=resolved_endpoint,
                    params=action.params,
                    body=action.body,
                    headers=action.headers,
                    description=action.description,
                    requires_auth=action.requires_auth,
                )

                try:
                    # Execute the action
                    response = self._execute_action_sync(resolved_action)
                    response_data = response.get("data", {})
                    status_code = response.get("status_code")
                    success = response.get("success", True)

                    # Extract new context from response
                    new_context = current_context.copy()
                    extract_context_from_response(
                        response_data,
                        endpoint=resolved_endpoint,
                        context=new_context,
                    )

                    # Add status to context for state naming
                    if status_code:
                        new_context.set("_status_code", status_code)

                    # Generate state name from context
                    state_name = generate_state_name(
                        new_context,
                        response_data,
                    )

                    # Generate unique state ID
                    state_id = self._generate_chain_state_id(
                        new_context.to_dict(),
                        resolved_endpoint,
                        action.method,
                        status_code,
                    )

                    # Determine available actions for the new state
                    new_actions = self._get_actions_for_state(
                        response_data,
                        resolved_endpoint,
                        action.method,
                        new_context.to_dict(),
                        initial_actions,
                    )

                    # Create chain state
                    new_chain_state = ChainState(
                        id=state_id,
                        name=state_name,
                        context=new_context.to_dict(),
                        response=response_data,
                        available_actions=new_actions,
                        depth=current_state.depth + 1,
                        parent_state=current_state.id,
                        parent_action=resolved_action,
                        metadata={
                            "status_code": status_code,
                            "success": success,
                            "original_endpoint": action.endpoint,
                        },
                        discovered_at=datetime.now(),
                    )

                    # Check if this is a new state
                    is_new_state = state_id not in chain_states

                    if is_new_state:
                        chain_states[state_id] = new_chain_state
                        self.graph.add_state(new_chain_state.to_state())
                        self.visited_states.add(state_id)

                    # Create transition
                    transition = Transition(
                        from_state=current_state.id,
                        action=resolved_action,
                        to_state=state_id,
                        response=response_data,
                        status_code=status_code,
                        duration_ms=response.get("duration_ms", 0.0),
                        success=success,
                        error=response.get("error"),
                        discovered_at=datetime.now(),
                    )
                    self.graph.add_transition(transition)
                    self._executed_actions.add(resolved_action)

                    # Add to queue for further exploration (only if new state)
                    if is_new_state and success:
                        queue.append((new_chain_state, new_context))

                    # Record issues for errors
                    if not success:
                        # Only record as issue if not an expected 404 after DELETE
                        is_expected_404 = (
                            status_code == 404
                            and action.method == "GET"
                            and current_state.parent_action
                            and current_state.parent_action.method == "DELETE"
                        )

                        if not is_expected_404:
                            self._record_issue(
                                severity=(
                                    IssueSeverity.MEDIUM
                                    if status_code and status_code < 500
                                    else IssueSeverity.HIGH
                                ),
                                error=f"{action.method} {resolved_endpoint} returned {status_code}",
                                state=current_state.id,
                                action=resolved_action,
                                suggestion="Check endpoint parameters and authentication",
                            )

                except Exception as e:
                    # Record error and continue
                    self._record_issue(
                        severity=IssueSeverity.HIGH,
                        error=f"Failed to execute {action.method} {resolved_endpoint}: {e}",
                        state=current_state.id,
                        action=resolved_action,
                        suggestion="Check if the endpoint is reachable",
                    )
                    logger.warning(
                        f"Action execution failed: {action.method} {resolved_endpoint}: {e}"
                    )

        # Create result
        return ChainExplorationResult(
            graph=self.graph,
            chain_states=chain_states,
            skipped_actions=skipped_actions,
            issues=self.issues,
            coverage=self.get_coverage_report(),
        )

    def _generate_chain_state_id(
        self,
        context: dict[str, Any],
        endpoint: str,
        method: str,
        status_code: int | None,
    ) -> StateID:
        """
        Generate a unique state ID based on context and request.

        The ID incorporates key context values to ensure states with
        different contexts are treated as different states.
        """
        import hashlib

        # Build a deterministic key from context
        key_parts = []

        # Include status code
        if status_code:
            key_parts.append(f"status:{status_code}")

        # Include entity IDs in sorted order
        entity_keys = sorted(
            k for k in context.keys()
            if k.endswith("_id") and not k.startswith("_")
        )
        for key in entity_keys:
            key_parts.append(f"{key}:{context[key]}")

        # Include status flags
        for key in ["_completed", "_status"]:
            if key in context:
                key_parts.append(f"{key}:{context[key]}")

        # Include method and endpoint pattern
        key_parts.append(f"{method}:{endpoint}")

        # Generate hash
        key_str = "|".join(key_parts)
        hash_val = hashlib.sha256(key_str.encode()).hexdigest()[:12]

        return f"chain_{hash_val}"

    def _get_actions_for_state(
        self,
        response_data: dict[str, Any],
        endpoint: str,
        method: str,
        context: dict[str, Any],
        initial_actions: list[Action],
    ) -> list[Action]:
        """
        Determine available actions for a state based on response and context.

        This generates the next possible actions based on:
        1. HATEOAS links in the response
        2. Standard CRUD operations based on extracted IDs
        3. Initial actions that might now be resolvable
        """
        actions: list[Action] = []
        seen: set[str] = set()

        def add_action(action: Action) -> None:
            key = f"{action.method}:{action.endpoint}"
            if key not in seen:
                seen.add(key)
                actions.append(action)

        # Extract HATEOAS links from response
        links = response_data.get("_links", response_data.get("links", {}))
        if isinstance(links, dict):
            for rel, link_data in links.items():
                if isinstance(link_data, dict) and "href" in link_data:
                    add_action(Action(
                        method=link_data.get("method", "GET"),
                        endpoint=link_data["href"],
                        description=rel,
                    ))

        # Generate CRUD actions based on extracted IDs
        if "todo_id" in context:
            todo_id = context["todo_id"]
            add_action(Action(method="GET", endpoint=f"/todos/{todo_id}"))
            add_action(Action(method="PUT", endpoint=f"/todos/{todo_id}",
                            body={"completed": True}))
            add_action(Action(method="DELETE", endpoint=f"/todos/{todo_id}"))

        if "attachment_id" in context and "todo_id" in context:
            todo_id = context["todo_id"]
            att_id = context["attachment_id"]
            add_action(Action(
                method="GET",
                endpoint=f"/todos/{todo_id}/attachments/{att_id}"
            ))
            add_action(Action(
                method="DELETE",
                endpoint=f"/todos/{todo_id}/attachments/{att_id}"
            ))

        # Add initial actions that might now be resolvable
        # Convert dict context to ExplorationContext for substitution
        exp_context = ExplorationContext()
        exp_context.update(context)

        for action in initial_actions:
            resolved = substitute_path_params(action.endpoint, exp_context)
            if resolved:
                add_action(Action(
                    method=action.method,
                    endpoint=resolved,
                    params=action.params,
                    body=action.body,
                    headers=action.headers,
                    description=action.description,
                ))

        return actions


class ChainExplorationResult:
    """
    Result of context-aware state chain exploration.

    Attributes:
        graph: The explored StateGraph
        chain_states: Dictionary of ChainState objects with full context
        skipped_actions: Actions that were skipped due to unresolvable params
        issues: List of discovered issues
        coverage: Coverage report
    """

    def __init__(
        self,
        graph: StateGraph,
        chain_states: dict[StateID, ChainState],
        skipped_actions: list[tuple[Action, str]],
        issues: list[Issue],
        coverage: CoverageReport,
    ) -> None:
        self.graph = graph
        self.chain_states = chain_states
        self.skipped_actions = skipped_actions
        self.issues = issues
        self.coverage = coverage

    def print_graph(self) -> None:
        """Print a text representation of the explored state graph."""
        print("\n" + "=" * 70)
        print("CONTEXT-AWARE STATE EXPLORATION RESULTS")
        print("=" * 70)

        print(f"\nStates discovered: {len(self.chain_states)}")
        print(f"Transitions: {len(self.graph.transitions)}")
        print(f"Skipped actions: {len(self.skipped_actions)}")
        print(f"Issues found: {len(self.issues)}")

        print("\n" + "-" * 70)
        print("STATES (with context)")
        print("-" * 70)

        for state_id, state in sorted(
            self.chain_states.items(),
            key=lambda x: x[1].depth
        ):
            indent = "  " * state.depth
            print(f"\n{indent}[{state.name}]")
            print(f"{indent}  ID: {state_id}")
            print(f"{indent}  Depth: {state.depth}")
            if state.context:
                ctx_str = ", ".join(
                    f"{k}={v}" for k, v in state.context.items()
                    if not k.startswith("_")
                )
                print(f"{indent}  Context: {ctx_str}")
            if state.parent_action:
                print(
                    f"{indent}  Via: {state.parent_action.method} "
                    f"{state.parent_action.endpoint}"
                )

        print("\n" + "-" * 70)
        print("TRANSITIONS")
        print("-" * 70)

        for t in self.graph.transitions:
            from_state = self.chain_states.get(t.from_state)
            to_state = self.chain_states.get(t.to_state)
            from_name = from_state.name if from_state else t.from_state
            to_name = to_state.name if to_state else t.to_state
            status = f" [{t.status_code}]" if t.status_code else ""
            print(
                f"  {from_name}\n"
                f"    --[{t.action.method} {t.action.endpoint}]{status}-->\n"
                f"  {to_name}\n"
            )

        if self.skipped_actions:
            print("\n" + "-" * 70)
            print("SKIPPED ACTIONS (unresolvable path params)")
            print("-" * 70)
            for action, reason in self.skipped_actions:
                print(f"  {action.method} {action.endpoint}: {reason}")

        if self.issues:
            print("\n" + "-" * 70)
            print("ISSUES")
            print("-" * 70)
            for issue in self.issues:
                print(f"  [{issue.severity.value.upper()}] {issue.error}")

        print("\n" + "=" * 70)
