"""
Tests for AsyncSessionClient.
"""

from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock

import grpc
import pytest


def create_grpc_error(details: str = "Test error") -> grpc.RpcError:
    """Create a mock gRPC error with details method."""
    error = grpc.RpcError()
    error.details = MagicMock(return_value=details)
    return error


from daisi.aio.clients.session import AsyncSessionClient
from daisi.config import DaisiConfig
from daisi.exceptions import DaisiError


class TestAsyncSessionClientInitialization:
    """Test AsyncSessionClient initialization."""

    def test_init_with_config(self) -> None:
        """Test initialization with config."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)
        assert client.config == config
        assert client._stub is None
        assert client.session_id is None

    def test_stub_lazy_initialization(self) -> None:
        """Test stub is lazily initialized."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        with patch.object(client, "_get_channel") as mock_get_channel:
            mock_channel = MagicMock()
            mock_get_channel.return_value = mock_channel

            stub = client.stub
            assert stub is not None
            mock_get_channel.assert_called_once()


class TestAsyncSessionClientCreate:
    """Test AsyncSessionClient create method."""

    @pytest.mark.asyncio
    async def test_create_session_minimal(self) -> None:
        """Test creating session with minimal parameters."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_response = MagicMock()
        mock_response.Id = "session-123"
        mock_response.Host = MagicMock()
        mock_response.HasField = MagicMock(side_effect=lambda f: f == "Host")
        mock_stub.Create = AsyncMock(return_value=mock_response)

        client._stub = mock_stub
        session_id, host, move_to_orc = await client.create()

        assert session_id == "session-123"
        assert client.session_id == "session-123"
        assert move_to_orc is None
        mock_stub.Create.assert_called_once()

    @pytest.mark.asyncio
    async def test_create_session_with_model(self) -> None:
        """Test creating session with model name."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_response = MagicMock()
        mock_response.Id = "session-123"
        mock_response.Host = MagicMock()
        mock_response.HasField = MagicMock(side_effect=lambda f: f == "Host")
        mock_stub.Create = AsyncMock(return_value=mock_response)

        client._stub = mock_stub
        session_id, host, move_to_orc = await client.create(model_name="llama-3")

        call_args = mock_stub.Create.call_args
        request = call_args[0][0]
        assert request.ModelName == "llama-3"

    @pytest.mark.asyncio
    async def test_create_session_with_preferences(self) -> None:
        """Test creating session with preferences."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_response = MagicMock()
        mock_response.Id = "session-123"
        mock_response.Host = MagicMock()
        mock_response.HasField = MagicMock(side_effect=lambda f: f == "Host")
        mock_stub.Create = AsyncMock(return_value=mock_response)

        client._stub = mock_stub
        session_id, host, move_to_orc = await client.create(
            model_name="llama-3",
            direct_connect_required=True,
            preferred_host_names=["host1", "host2"],
            preferred_region="us-west",
            host_id="host-123",
        )

        call_args = mock_stub.Create.call_args
        request = call_args[0][0]
        assert request.DirectConnectRequired is True
        assert len(request.PreferredHostNames) == 2

    @pytest.mark.asyncio
    async def test_create_session_grpc_error(self) -> None:
        """Test create session with gRPC error."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_stub.Create = AsyncMock(side_effect=create_grpc_error())

        client._stub = mock_stub
        with pytest.raises(DaisiError, match="Failed to create session"):
            await client.create()


class TestAsyncSessionClientClaim:
    """Test AsyncSessionClient claim method."""

    @pytest.mark.asyncio
    async def test_claim_session_success(self) -> None:
        """Test successful session claim."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_response = MagicMock()
        mock_response.Success = True
        mock_response.ModelName.value = "llama-3"
        mock_stub.Claim = AsyncMock(return_value=mock_response)

        client._stub = mock_stub
        success, model_name = await client.claim("session-123")

        assert success is True
        assert model_name == "llama-3"

    @pytest.mark.asyncio
    async def test_claim_session_no_model(self) -> None:
        """Test claiming session with no model name."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_response = MagicMock()
        mock_response.Success = True
        mock_response.ModelName.value = ""
        mock_stub.Claim = AsyncMock(return_value=mock_response)

        client._stub = mock_stub
        success, model_name = await client.claim("session-123")

        assert success is True
        assert model_name is None

    @pytest.mark.asyncio
    async def test_claim_session_grpc_error(self) -> None:
        """Test claim session with gRPC error."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_stub.Claim = AsyncMock(side_effect=create_grpc_error())

        client._stub = mock_stub
        with pytest.raises(DaisiError, match="Failed to claim session"):
            await client.claim("session-123")


class TestAsyncSessionClientClose:
    """Test AsyncSessionClient close method."""

    @pytest.mark.asyncio
    async def test_close_session_with_id(self) -> None:
        """Test closing session with explicit ID."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_response = MagicMock()
        mock_response.Success = True
        mock_stub.Close = AsyncMock(return_value=mock_response)

        client._stub = mock_stub
        result = await client.close_session("session-123")

        assert result is True
        mock_stub.Close.assert_called_once()

    @pytest.mark.asyncio
    async def test_close_session_stored_id(self) -> None:
        """Test closing session with stored ID."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)
        client.session_id = "session-123"

        mock_stub = MagicMock()
        mock_response = MagicMock()
        mock_response.Success = True
        mock_stub.Close = AsyncMock(return_value=mock_response)

        client._stub = mock_stub
        result = await client.close_session()

        assert result is True
        assert client.session_id is None

    @pytest.mark.asyncio
    async def test_close_session_no_id(self) -> None:
        """Test closing session without ID raises error."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        with pytest.raises(DaisiError, match="No session ID provided or available"):
            await client.close_session()

    @pytest.mark.asyncio
    async def test_close_session_grpc_error(self) -> None:
        """Test close session with gRPC error."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_stub.Close = AsyncMock(side_effect=create_grpc_error())

        client._stub = mock_stub
        with pytest.raises(DaisiError, match="Failed to close session"):
            await client.close_session("session-123")


class TestAsyncSessionClientConnect:
    """Test AsyncSessionClient connect method."""

    @pytest.mark.asyncio
    async def test_connect_session_success(self) -> None:
        """Test successful session connection."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_response = MagicMock()
        mock_response.Id = "session-123"
        mock_response.HasCapacity = True
        mock_response.AlreadyConnected = False
        mock_stub.Connect = AsyncMock(return_value=mock_response)

        client._stub = mock_stub
        session_id, has_capacity, already_connected = await client.connect(
            "session-123"
        )

        assert session_id == "session-123"
        assert has_capacity is True
        assert already_connected is False

    @pytest.mark.asyncio
    async def test_connect_session_stored_id(self) -> None:
        """Test connecting with stored session ID."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)
        client.session_id = "session-123"

        mock_stub = MagicMock()
        mock_response = MagicMock()
        mock_response.Id = "session-123"
        mock_response.HasCapacity = True
        mock_response.AlreadyConnected = True
        mock_stub.Connect = AsyncMock(return_value=mock_response)

        client._stub = mock_stub
        session_id, has_capacity, already_connected = await client.connect()

        assert already_connected is True

    @pytest.mark.asyncio
    async def test_connect_session_no_id(self) -> None:
        """Test connecting without session ID raises error."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        with pytest.raises(DaisiError, match="No session ID provided or available"):
            await client.connect()

    @pytest.mark.asyncio
    async def test_connect_session_grpc_error(self) -> None:
        """Test connect session with gRPC error."""
        config = DaisiConfig(client_key="test-key")
        client = AsyncSessionClient(config)

        mock_stub = MagicMock()
        mock_stub.Connect = AsyncMock(side_effect=create_grpc_error())

        client._stub = mock_stub
        with pytest.raises(DaisiError, match="Failed to connect to session"):
            await client.connect("session-123")


class TestAsyncSessionClientContextManager:
    """Test AsyncSessionClient async context manager."""

    @pytest.mark.asyncio
    async def test_async_context_manager(self) -> None:
        """Test async context manager properly closes client."""
        config = DaisiConfig(client_key="test-key")

        with patch.object(
            AsyncSessionClient, "close", new_callable=AsyncMock
        ) as mock_close:
            async with AsyncSessionClient(config) as client:
                assert client is not None

            mock_close.assert_called_once()
