"""Tests for TUI error recovery and graceful degradation."""

import asyncio
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import pytest

from cleave.tui.backends.base import (
    BackendAuthError,
    BackendConfig,
    BackendConnectionError,
    BackendQueryError,
    BackendState,
)


class TestClaudeBackendErrors:
    """Test Claude backend error handling."""

    @pytest.mark.asyncio
    async def test_connection_error_missing_sdk(self):
        """Test connection error when SDK not installed."""
        with patch("cleave.tui.backends.claude._check_sdk", return_value=False):
            from cleave.tui.backends.claude import ClaudeBackend

            backend = ClaudeBackend()
            config = BackendConfig()

            with pytest.raises(BackendConnectionError) as exc_info:
                await backend.connect(config)

            assert "Claude SDK not installed" in str(exc_info.value)
            assert "pip install" in str(exc_info.value)
            assert backend.state == BackendState.ERROR
            assert backend.error == "Claude SDK not installed"

    @pytest.mark.asyncio
    async def test_auth_error_not_authenticated(self):
        """Test auth error when credentials missing."""
        with patch("cleave.tui.backends.claude._check_sdk", return_value=True):
            from cleave.tui.backends.claude import ClaudeBackend

            backend = ClaudeBackend()
            config = BackendConfig()

            # Mock check_auth as a class method
            with patch.object(
                ClaudeBackend,
                "check_auth",
                return_value=(False, "Not authenticated. Run 'claude' CLI to login via browser."),
            ):
                with pytest.raises(BackendAuthError) as exc_info:
                    await backend.connect(config)

                assert "authenticate" in str(exc_info.value).lower()
                assert backend.state == BackendState.ERROR

    @pytest.mark.asyncio
    async def test_query_error_not_connected(self):
        """Test query error when backend not connected."""
        with patch("cleave.tui.backends.claude._check_sdk", return_value=True):
            from cleave.tui.backends.claude import ClaudeBackend

            backend = ClaudeBackend()

            with pytest.raises(RuntimeError) as exc_info:
                async for _ in backend.query("test"):
                    pass

            assert "not connected" in str(exc_info.value).lower()
            assert backend.state == BackendState.ERROR

    @pytest.mark.asyncio
    async def test_query_timeout_error(self):
        """Test query timeout with actionable error message."""
        with patch("cleave.tui.backends.claude._check_sdk", return_value=True):
            from cleave.tui.backends.claude import ClaudeBackend

            backend = ClaudeBackend()
            backend._state = BackendState.CONNECTED
            backend._config = BackendConfig(timeout=60.0)

            # Mock client that raises timeout
            mock_client = AsyncMock()
            mock_client.query = AsyncMock()

            # Create an async generator that raises TimeoutError
            async def raise_timeout():
                raise asyncio.TimeoutError("timeout")
                yield  # Never reached

            mock_client.receive_response = Mock(return_value=raise_timeout())
            backend._client = mock_client

            with pytest.raises(BackendQueryError) as exc_info:
                async for _ in backend.query("test"):
                    pass

            error_msg = str(exc_info.value)
            assert "timed out" in error_msg.lower()
            assert "60" in error_msg  # Shows timeout value
            assert backend.state == BackendState.ERROR


class TestOpenAICompatBackendErrors:
    """Test OpenAI-compatible backend error handling."""

    @pytest.mark.asyncio
    async def test_connection_error_missing_package(self):
        """Test connection error when openai package not installed."""
        with patch("cleave.tui.backends.openai_compat._check_openai", return_value=False):
            from cleave.tui.backends.openai_compat import OpenAICompatBackend

            backend = OpenAICompatBackend(provider="ollama")
            config = BackendConfig()

            with pytest.raises(BackendConnectionError) as exc_info:
                await backend.connect(config)

            assert "OpenAI package not installed" in str(exc_info.value)
            assert "pip install" in str(exc_info.value)
            assert backend.state == BackendState.ERROR

    @pytest.mark.asyncio
    async def test_auth_error_openai_no_key(self):
        """Test auth error when OpenAI API key missing."""
        import sys
        import os
        mock_openai = MagicMock()
        with patch("cleave.tui.backends.openai_compat._check_openai", return_value=True):
            with patch.dict(os.environ, {}, clear=True):  # Clear OPENAI_API_KEY
                with patch.dict(sys.modules, {"openai": mock_openai}):
                    from cleave.tui.backends.openai_compat import OpenAICompatBackend

                    backend = OpenAICompatBackend(provider="openai")
                    config = BackendConfig()

                    with pytest.raises(BackendAuthError) as exc_info:
                        await backend.connect(config)

                    assert "api key" in str(exc_info.value).lower()
                    assert "OPENAI_API_KEY" in str(exc_info.value)
                    assert backend.state == BackendState.ERROR

    @pytest.mark.asyncio
    async def test_connection_error_ollama_not_running(self):
        """Test connection error when Ollama not running."""
        with patch("cleave.tui.backends.openai_compat._check_openai", return_value=True):
            # Need to import at module level with patching
            import sys
            from unittest.mock import MagicMock

            # Mock the openai module
            mock_openai = MagicMock()
            mock_client_class = MagicMock()
            mock_client = AsyncMock()
            mock_client.models.list = AsyncMock(side_effect=ConnectionError("Connection refused"))
            mock_client_class.return_value = mock_client
            mock_openai.AsyncOpenAI = mock_client_class

            with patch.dict(sys.modules, {"openai": mock_openai}):
                from cleave.tui.backends.openai_compat import OpenAICompatBackend

                backend = OpenAICompatBackend(provider="ollama")
                config = BackendConfig()

                with pytest.raises(BackendConnectionError) as exc_info:
                    await backend.connect(config)

                error_msg = str(exc_info.value)
                assert "ollama" in error_msg.lower()
                assert "not running" in error_msg.lower()
                assert backend.state == BackendState.ERROR

    @pytest.mark.asyncio
    async def test_query_error_model_not_found(self):
        """Test query error with model not found."""
        with patch("cleave.tui.backends.openai_compat._check_openai", return_value=True):
            from cleave.tui.backends.openai_compat import OpenAICompatBackend

            backend = OpenAICompatBackend(provider="ollama")
            backend._state = BackendState.CONNECTED
            backend._config = BackendConfig(model="nonexistent-model")
            backend._conversation = []

            # Mock client that raises model not found
            mock_client = AsyncMock()
            mock_client.chat.completions.create = AsyncMock(
                side_effect=Exception("model 'nonexistent-model' not found")
            )
            backend._client = mock_client

            with pytest.raises(BackendQueryError) as exc_info:
                async for _ in backend.query("test"):
                    pass

            error_msg = str(exc_info.value)
            assert "model not found" in error_msg.lower()
            assert "available models" in error_msg.lower()
            assert backend.state == BackendState.ERROR


class TestGracefulDegradation:
    """Test graceful degradation and recovery."""

    @pytest.mark.asyncio
    async def test_backend_state_persists_after_error(self):
        """Test that backend state properly reflects errors."""
        with patch("cleave.tui.backends.claude._check_sdk", return_value=False):
            from cleave.tui.backends.claude import ClaudeBackend

            backend = ClaudeBackend()
            config = BackendConfig()

            try:
                await backend.connect(config)
            except BackendConnectionError:
                pass

            # State should indicate error
            assert backend.state == BackendState.ERROR
            assert backend.error is not None
            assert "sdk not installed" in backend.error.lower()

    @pytest.mark.asyncio
    async def test_multiple_connection_attempts_safe(self):
        """Test that multiple failed connection attempts are safe."""
        with patch("cleave.tui.backends.claude._check_sdk", return_value=False):
            from cleave.tui.backends.claude import ClaudeBackend

            backend = ClaudeBackend()
            config = BackendConfig()

            # Multiple failed attempts should not crash
            for _ in range(3):
                with pytest.raises(BackendConnectionError):
                    await backend.connect(config)

                assert backend.state == BackendState.ERROR

    def test_error_messages_actionable(self):
        """Test that error messages include actionable guidance."""
        # Test BackendConnectionError
        error = BackendConnectionError(
            "Claude SDK not installed.\n\n"
            "Install with: pip install styrene-cleave[tui]\n"
            "Or: pip install claude-agent-sdk"
        )
        error_msg = str(error)
        assert "pip install" in error_msg
        assert "claude-agent-sdk" in error_msg

        # Test BackendAuthError
        error = BackendAuthError(
            "Not authenticated.\n\n"
            "Authenticate with: claude\n"
            "(Opens browser for login)"
        )
        error_msg = str(error)
        assert "authenticate" in error_msg.lower()
        assert "claude" in error_msg


class TestErrorRecovery:
    """Test error recovery mechanisms."""

    @pytest.mark.asyncio
    async def test_disconnect_after_error_safe(self):
        """Test that disconnect after error is safe."""
        with patch("cleave.tui.backends.claude._check_sdk", return_value=True):
            from cleave.tui.backends.claude import ClaudeBackend

            backend = ClaudeBackend()
            backend._state = BackendState.ERROR
            backend._client = None

            # Should not raise
            await backend.disconnect()
            # State should be DISCONNECTED after disconnect
            assert backend.state == BackendState.DISCONNECTED
            # Note: disconnect() sets state to DISCONNECTED when client is None

    @pytest.mark.asyncio
    async def test_reconnect_after_error_allowed(self):
        """Test that reconnection after error is possible."""
        import sys
        mock_sdk = MagicMock()
        mock_client = AsyncMock()
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_sdk.ClaudeSDKClient.return_value = mock_client
        with patch("cleave.tui.backends.claude._check_sdk", return_value=True):
            with patch.dict(sys.modules, {"claude_agent_sdk": mock_sdk}):
                from cleave.tui.backends.claude import ClaudeBackend

                backend = ClaudeBackend()
                backend._state = BackendState.ERROR
                backend._error = "Previous error"

                # Mock successful connection
                with patch.object(backend, "check_auth", return_value=(True, "Authenticated")):
                    config = BackendConfig()
                    await backend.connect(config)

                    # Should recover from error state
                    assert backend.state == BackendState.CONNECTED
                    assert backend.error is None
