"""
Tests de la conexión WebSocket del Agent CLI.
"""
import asyncio
import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from jama_agent.connection import AgentConnection


@pytest.fixture
def connection():
    """Crea una instancia de AgentConnection para tests."""
    return AgentConnection(
        server="http://localhost:8000",
        agent_id="test-agent-uuid",
        token="test-token",
        heartbeat_interval=30,
        reconnect_max_delay=60,
    )


class TestAgentConnectionInit:
    """Tests de inicialización."""

    def test_defaults(self, connection):
        """Valores por defecto correctos."""
        assert connection.server == "http://localhost:8000"
        assert connection.agent_id == "test-agent-uuid"
        assert connection.heartbeat_interval == 30
        assert connection.reconnect_max_delay == 60
        assert not connection.is_connected
        assert connection.uptime == 0.0

    def test_ws_url_http(self, connection):
        """URL http se convierte a ws."""
        url = connection._build_ws_url()
        assert url.startswith("ws://")
        assert "test-agent-uuid" in url
        assert "token=test-token" in url

    def test_ws_url_https(self):
        """URL https se convierte a wss."""
        conn = AgentConnection(
            server="https://secure.example.com",
            agent_id="id",
            token="tok",
        )
        url = conn._build_ws_url()
        assert url.startswith("wss://")


class TestReconnectBackoff:
    """Tests del cálculo de exponential backoff."""

    def test_backoff_sequence(self, connection):
        """Secuencia: 1, 2, 4, 8, 16, 32, 60, 60..."""
        expected = [1, 2, 4, 8, 16, 32, 60, 60]
        for i, expected_delay in enumerate(expected):
            connection._reconnect_attempt = i
            delay = connection._get_reconnect_delay()
            assert delay == expected_delay, (
                f"attempt={i}: expected {expected_delay}, got {delay}"
            )

    def test_backoff_never_exceeds_max(self, connection):
        """El delay nunca excede reconnect_max_delay."""
        for attempt in range(20):
            connection._reconnect_attempt = attempt
            delay = connection._get_reconnect_delay()
            assert delay <= connection.reconnect_max_delay

    def test_backoff_custom_max(self):
        """Custom max_delay se respeta."""
        conn = AgentConnection(
            server="http://x",
            agent_id="id",
            token="tok",
            reconnect_max_delay=10,
        )
        conn._reconnect_attempt = 10
        assert conn._get_reconnect_delay() == 10


class TestMessageParsing:
    """Tests de parsing y despacho de mensajes."""

    @pytest.mark.asyncio
    async def test_handle_execute(self, connection):
        """Mensaje execute crea tarea en background."""
        with patch.object(connection, "_execute_and_report", new_callable=AsyncMock) as mock:
            # Simulamos que _handle_execute crea la tarea
            # Pero la tarea real usa asyncio.create_task que necesita event loop
            message = {
                "type": "execute",
                "task_id": "task-1",
                "tool": "nmap",
                "args": {"target": "192.168.1.1"},
                "timeout": 60,
            }

            # Patch create_task para evitar tareas reales
            mock_task = MagicMock()
            with patch("asyncio.create_task", return_value=mock_task):
                await connection._handle_message(message)
                # La tarea se registra
                assert "task-1" in connection._tasks

    @pytest.mark.asyncio
    async def test_handle_ping(self, connection):
        """Mensaje ping envía heartbeat."""
        with patch.object(connection, "_send_heartbeat", new_callable=AsyncMock) as mock:
            await connection._handle_message({"type": "ping"})
            mock.assert_called_once()

    @pytest.mark.asyncio
    async def test_handle_kill(self, connection):
        """Mensaje kill detiene la conexión."""
        with patch.object(connection, "disconnect", new_callable=AsyncMock):
            await connection._handle_message({"type": "kill"})
            assert not connection._running


class TestHeartbeatFormat:
    """Tests del formato de heartbeat."""

    @pytest.mark.asyncio
    async def test_heartbeat_message_format(self, connection):
        """El heartbeat incluye los campos esperados."""
        mock_ws = AsyncMock()
        connection._ws = mock_ws

        with patch("jama_agent.connection.psutil") as mock_psutil:
            mock_psutil.cpu_percent.return_value = 15.5
            mock_psutil.virtual_memory.return_value = MagicMock(percent=42.3)

            await connection._send_heartbeat()

        mock_ws.send.assert_called_once()
        sent = json.loads(mock_ws.send.call_args[0][0])

        assert sent["type"] == "pong"
        assert "uptime" in sent
        assert sent["cpu"] == 15.5
        assert sent["memory"] == 42.3
        assert "os" in sent
        assert "hostname" in sent
