"""Tests for State Machine and DAG Executor integration.

Tests for the integration between OrchestratorStateMachine and ActionExecutor
via the OrchestratorServer.
"""

from __future__ import annotations

import asyncio
import sys
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
from dataclasses import dataclass

import pytest

ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

from src.server import OrchestratorServer, ServerConfig
from src.state_machine import OrchestratorState, OrchestratorStateMachine, TransitionRecord
from src.executor.action_executor import ActionExecutor, ExecutionReport
from src.edge_proxy.client import EdgeProxyClient, ClientConfig


# =============================================================================
# Test Fixtures
# =============================================================================


@pytest.fixture
def server_config():
    """Create a test server config."""
    return ServerConfig(
        host="localhost",
        port=8001,
        enable_planner=False,
        enable_executor=True,
    )


@pytest.fixture
def basic_plan():
    """Create a basic test plan."""
    return {
        "intent": "Navigate to lobby",
        "workflow_topology": "DAG",
        "plan_id": "test_plan_001",
        "steps": [
            {
                "step_id": "step1",
                "type": "EXECUTION",
                "agent_role": "robot",
                "action": "CHECK_BATTERY",
                "params": {"min_level": 20},
            },
            {
                "step_id": "step2",
                "type": "EXECUTION",
                "agent_role": "robot",
                "action": "SPEAK",
                "params": {"message": "Hello"},
                "dependencies": ["step1"],
            },
        ],
    }


# =============================================================================
# Test State Machine Listener Registration
# =============================================================================


class TestStateMachineListenerRegistration:
    """Test that state machine listener is properly registered."""

    def test_edge_proxy_default_port(self):
        """Edge proxy defaults should align with the robot-side server default."""
        assert ServerConfig().edge_proxy_port == 8080

    def test_executor_created_when_enabled(self, server_config):
        """Test that executor is created when enable_executor is True."""
        server = OrchestratorServer(server_config)
        assert server.executor is not None
        # Check that executor has the expected methods
        assert hasattr(server.executor, "execute_plan")
        assert hasattr(server.executor, "register_edge_proxy_client")

    def test_executor_not_created_when_disabled(self):
        """Test that executor is not created when enable_executor is False."""
        config = ServerConfig(enable_executor=False)
        server = OrchestratorServer(config)
        assert server.executor is None

    def test_state_machine_has_listener(self, server_config):
        """Test that state machine has listener registered for executor."""
        server = OrchestratorServer(server_config)
        # State machine should have at least one listener if executor is enabled
        if server.executor:
            assert len(server.state_machine._listeners) > 0


# =============================================================================
# Test State Machine Triggers Executor
# =============================================================================


@pytest.mark.asyncio
class TestStateMachineTriggersExecutor:
    """Test that state machine transitions trigger executor."""

    async def test_executing_actions_triggers_executor(
        self, server_config, basic_plan
    ):
        """Test that EXECUTING_ACTIONS transition triggers executor."""
        server = OrchestratorServer(server_config)

        # Set up state to AWAITING_CONFIRM
        server.state_machine._state = OrchestratorState.AWAITING_CONFIRM

        # Store plan for execution
        server._pending_plan = basic_plan

        # Transition to EXECUTING_ACTIONS should trigger executor
        new_state = server.state_machine.handle_event(
            "user_confirmed", {"plan": basic_plan}
        )

        assert new_state == OrchestratorState.EXECUTING_ACTIONS

        # Give executor time to complete (if async execution is implemented)
        await asyncio.sleep(0.1)

    async def test_executor_finishes_sends_actions_complete(
        self, server_config, basic_plan
    ):
        """Test that executor finishing sends actions_complete event."""
        server = OrchestratorServer(server_config)

        # Track state transitions
        transitions = []
        server.state_machine.add_listener(lambda t: transitions.append(t))

        # Set up state to EXECUTING_ACTIONS
        server.state_machine._state = OrchestratorState.EXECUTING_ACTIONS

        # Execute the plan (simulating executor completion)
        report = await server.executor.execute_plan(basic_plan)

        # After successful execution, state should transition to SPEAKING
        # via actions_complete event
        if report.success:
            new_state = server.state_machine.handle_event("actions_complete")
            assert new_state == OrchestratorState.SPEAKING


# =============================================================================
# Test Edge Proxy Client Integration
# =============================================================================


@pytest.mark.asyncio
class TestEdgeProxyClientIntegration:
    """Test Edge Proxy client integration with executor."""

    async def test_edge_proxy_client_registered(self, server_config):
        """Test that EdgeProxyClient can be registered with executor."""
        server = OrchestratorServer(server_config)

        # Create mock client
        mock_client = MagicMock()
        mock_client.on_nav_status = MagicMock(side_effect=lambda h: h)
        mock_client.on_robot_state = MagicMock(side_effect=lambda h: h)
        mock_client.on_error = MagicMock(side_effect=lambda h: h)
        mock_client.on_waypoint_list = MagicMock(side_effect=lambda h: h)

        # Register client with executor
        if server.executor:
            server.executor.register_edge_proxy_client(mock_client)
            assert server.executor._edge_client is mock_client

    async def test_executor_with_edge_proxy_navigates(
        self, server_config
    ):
        """Test executor with Edge Proxy client can navigate."""
        server = OrchestratorServer(server_config)

        if not server.executor:
            pytest.skip("Executor not enabled")

        # Create mock client with async send method
        mock_client = MagicMock()
        mock_client.on_nav_status = MagicMock(side_effect=lambda h: h)
        mock_client.on_robot_state = MagicMock(side_effect=lambda h: h)
        mock_client.on_error = MagicMock(side_effect=lambda h: h)
        mock_client.on_waypoint_list = MagicMock(side_effect=lambda h: h)
        mock_client.send_navigate_waypoint = AsyncMock()

        server.executor.register_edge_proxy_client(mock_client)

        plan = {
            "intent": "Navigate to lobby",
            "workflow_topology": "DAG",
            "plan_id": "test_nav_plan",
            "steps": [
                {
                    "step_id": "step1",
                    "type": "EXECUTION",
                    "agent_role": "robot",
                    "action": "NAVIGATE",
                    "params": {"destination": "lobby", "speed": "normal"},
                },
            ],
        }

        # Mock waiting for navigation
        with patch.object(server.executor, "_wait_for_navigation", return_value=None):
            report = await server.executor.execute_plan(plan)

        assert report.success
        assert mock_client.send_navigate_waypoint.called


# =============================================================================
# Integration Tests
# =============================================================================


@pytest.mark.asyncio
class TestIntegration:
    """Full integration tests for state machine + executor."""

    async def test_full_flow_confirm_to_execute_to_speaking(
        self, server_config, basic_plan
    ):
        """Test full flow: AWAITING_CONFIRM -> EXECUTING_ACTIONS -> SPEAKING."""
        server = OrchestratorServer(server_config)

        transitions = []
        server.state_machine.add_listener(lambda t: transitions.append(t))

        # Start at AWAITING_CONFIRM
        server.state_machine._state = OrchestratorState.AWAITING_CONFIRM

        # User confirms - transition to EXECUTING_ACTIONS
        state1 = server.state_machine.handle_event(
            "user_confirmed", {"plan": basic_plan}
        )
        assert state1 == OrchestratorState.EXECUTING_ACTIONS

        # Execute the plan
        report = await server.executor.execute_plan(basic_plan)
        assert report.success

        # Actions complete - transition to SPEAKING
        state2 = server.state_machine.handle_event("actions_complete")
        assert state2 == OrchestratorState.SPEAKING

        # Verify all transitions were recorded
        assert len(transitions) == 2
        assert transitions[0].next == OrchestratorState.EXECUTING_ACTIONS
        assert transitions[1].next == OrchestratorState.SPEAKING

    async def test_failed_execution_sends_actions_failed(
        self, server_config
    ):
        """Test that failed execution sends actions_failed event."""
        server = OrchestratorServer(server_config)

        if not server.executor:
            pytest.skip("Executor not enabled")

        transitions = []
        server.state_machine.add_listener(lambda t: transitions.append(t))

        server.state_machine._state = OrchestratorState.EXECUTING_ACTIONS

        # Create an invalid plan that will fail
        invalid_plan = {
            "intent": "Invalid action",
            "workflow_topology": "DAG",
            "plan_id": "invalid_plan",
            "steps": [
                {
                    "step_id": "step1",
                    "type": "EXECUTION",
                    "agent_role": "robot",
                    "action": "INVALID_ACTION",
                },
            ],
        }

        report = await server.executor.execute_plan(invalid_plan)

        # After failed execution, state should transition to SPEAKING
        # via actions_failed event
        if not report.success:
            new_state = server.state_machine.handle_event("actions_failed")
            assert new_state == OrchestratorState.SPEAKING
