"""Unit tests for ConnectionManager."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from rabbitmq_mcp_connection.connection.exceptions import (
    AuthenticationError,
    ConnectionError,
    ConnectionTimeoutError,
)
from rabbitmq_mcp_connection.connection.manager import ConnectionManager
from rabbitmq_mcp_connection.schemas.connection import ConnectionConfig, ConnectionState


@pytest.fixture
def connection_config():
    """Create a test connection config."""
    return ConnectionConfig(
        host="localhost",
        port=5672,
        user="guest",
        password="guest",
        vhost="/",
        timeout=30,
        heartbeat=60,
    )


@pytest.fixture
def manager(connection_config):
    """Create a ConnectionManager instance."""
    return ConnectionManager(connection_config)


@pytest.mark.asyncio
class TestConnectionManager:
    """Test ConnectionManager functionality."""

    async def test_initial_state(self, manager):
        """Test manager starts in disconnected state."""
        assert manager.state == ConnectionState.DISCONNECTED
        assert not manager.is_connected()
        assert manager.get_state() == ConnectionState.DISCONNECTED

    async def test_connect_success(self, manager):
        """Test successful connection."""
        mock_connection = MagicMock()
        mock_connection.is_closed = False
        mock_channel = AsyncMock()

        async def mock_channel_method():
            return mock_channel

        mock_connection.channel = mock_channel_method

        with patch(
            "rabbitmq_mcp_connection.connection.manager.aio_pika.connect_robust",
            return_value=mock_connection,
        ):
            await manager.connect()

            assert manager.state == ConnectionState.CONNECTED
            assert manager.is_connected()
            assert manager.connection == mock_connection

    async def test_connect_timeout(self, manager):
        """Test connection timeout handling."""
        with patch(
            "rabbitmq_mcp_connection.connection.manager.asyncio.wait_for",
            side_effect=TimeoutError,
        ):
            with pytest.raises(ConnectionTimeoutError) as exc_info:
                await manager.connect()

            assert "Connection timeout" in str(exc_info.value)
            assert manager.state == ConnectionState.DISCONNECTED

    async def test_connect_auth_error(self, manager):
        """Test authentication error handling."""
        from rabbitmq_mcp_connection.connection.aio_pika_compat import aio_pika

        with patch(
            "rabbitmq_mcp_connection.connection.manager.aio_pika.connect_robust",
            side_effect=aio_pika.exceptions.ProbableAuthenticationError("Auth failed"),
        ):
            with pytest.raises(AuthenticationError) as exc_info:
                await manager.connect()

            assert "Authentication failed" in str(exc_info.value)
            assert manager.config.host in str(exc_info.value)
            assert str(manager.config.port) in str(exc_info.value)
            assert manager.state == ConnectionState.DISCONNECTED

    async def test_connect_with_tls(self):
        """Test connection with TLS enabled."""
        config = ConnectionConfig(
            host="localhost",
            port=5671,
            user="guest",
            password="guest",
            vhost="/",
            use_tls=True,
            verify_cert=True,
        )
        manager = ConnectionManager(config)

        mock_connection = MagicMock()
        mock_connection.is_closed = False
        mock_channel = AsyncMock()

        async def mock_channel_method():
            return mock_channel

        mock_connection.channel = mock_channel_method

        with (
            patch(
                "rabbitmq_mcp_connection.connection.manager.aio_pika.connect_robust",
                return_value=mock_connection,
            ) as mock_connect,
            patch(
                "rabbitmq_mcp_connection.connection.manager.TLSContextManager.create_ssl_context"
            ) as mock_tls,
        ):
            await manager.connect()

            # Verify TLS context was created
            mock_tls.assert_called_once_with(verify_cert=True, ca_bundle=None)
            # Verify connect was called with ssl=True
            call_kwargs = mock_connect.call_args[1]
            assert call_kwargs["ssl"] is True
            assert call_kwargs["ssl_context"] == mock_tls.return_value

    async def test_disconnect(self, manager):
        """Test disconnect closes connection and channel."""
        mock_connection = AsyncMock()
        mock_connection.is_closed = False
        mock_channel = AsyncMock()
        mock_channel.is_closed = False

        manager.connection = mock_connection
        manager.channel = mock_channel
        manager.state = ConnectionState.CONNECTED

        await manager.disconnect()

        mock_channel.close.assert_called_once()
        mock_connection.close.assert_called_once()
        assert manager.state == ConnectionState.DISCONNECTED
        assert manager.connection is None
        assert manager.channel is None

    async def test_get_channel_when_connected(self, manager):
        """Test getting channel when connected."""
        mock_connection = MagicMock()
        mock_connection.is_closed = False
        mock_channel = MagicMock()
        mock_channel.is_closed = False

        manager.connection = mock_connection
        manager.channel = mock_channel
        manager.state = ConnectionState.CONNECTED

        channel = await manager.get_channel()
        assert channel == mock_channel

    async def test_get_channel_when_not_connected(self, manager):
        """Test getting channel when not connected raises error."""
        with pytest.raises(ConnectionError) as exc_info:
            await manager.get_channel()

        assert "not connected" in str(exc_info.value)
        assert manager.config.host in str(exc_info.value)

    async def test_state_transitions(self, manager):
        """Test connection state machine transitions."""
        assert manager.get_state() == ConnectionState.DISCONNECTED

        mock_connection = AsyncMock()
        mock_connection.is_closed = False
        mock_channel = AsyncMock()

        async def mock_channel_method():
            return mock_channel

        mock_connection.channel = mock_channel_method

        with (
            patch(
                "rabbitmq_mcp_connection.connection.manager.aio_pika.connect_robust",
                return_value=mock_connection,
            ),
            patch(
                "rabbitmq_mcp_connection.connection.manager.ConnectionMonitor"
            ) as mock_monitor,
        ):
            # Mock monitor instance
            mock_monitor_instance = AsyncMock()
            mock_monitor.return_value = mock_monitor_instance

            await manager.connect()
            assert manager.get_state() == ConnectionState.CONNECTED

        await manager.disconnect()
        assert manager.get_state() == ConnectionState.DISCONNECTED

    async def test_get_channel_during_reconnection_raises_error(self, manager):
        """Test get_channel raises ConnectionError when reconnecting (AC #6)."""
        # Set state to RECONNECTING
        manager.state = ConnectionState.RECONNECTING

        # Attempt to get channel should raise ConnectionError
        with pytest.raises(
            ConnectionError,
            match="Connection lost, please retry after reconnection",
        ):
            await manager.get_channel()

    async def test_get_channel_with_error_message_propagates(self, manager):
        """Test error message from get_channel during reconnection."""
        manager.state = ConnectionState.RECONNECTING

        try:
            await manager.get_channel()
            pytest.fail("Expected ConnectionError to be raised")
        except ConnectionError as e:
            # Verify specific error message for MCP tool callers
            assert "Connection lost, please retry after reconnection" in str(e)
