"""
Tests for the OpenAITranslationProvider.
"""

import pytest
from unittest.mock import patch, MagicMock
from requests.exceptions import Timeout, ConnectionError as RequestsConnectionError

from services.providers.openai_provider import (
    OpenAITranslationProvider,
    OpenAIProviderError,
    get_openai_provider,
    register_openai_provider,
    reset_openai_provider,
    _build_system_prompt,
    _get_language_name,
    OPENAI_RATE_LIMITED,
    OPENAI_INVALID_KEY,
    OPENAI_QUOTA_EXCEEDED,
    OPENAI_TIMEOUT,
    OPENAI_SERVICE_ERROR,
    OPENAI_CONTEXT_TOO_LONG,
)
from services.providers.schemas import TranslationRequest, TranslationResponse


class TestOpenAIProviderError:
    """Tests for OpenAIProviderError exception."""

    def test_error_creation(self):
        """Test error creation with all fields."""
        error = OpenAIProviderError(
            code=OPENAI_RATE_LIMITED,
            message="Rate limited",
            details={"retry_after": 20},
        )

        assert error.code == OPENAI_RATE_LIMITED
        assert error.message == "Rate limited"
        assert error.details == {"retry_after": 20}

    def test_error_to_dict(self):
        """Test error serialization."""
        error = OpenAIProviderError(
            code=OPENAI_INVALID_KEY,
            message="Invalid key",
            details={"provider": "openai"},
        )

        result = error.to_dict()

        assert result["error"] == OPENAI_INVALID_KEY
        assert result["message"] == "Invalid key"
        assert result["details"]["provider"] == "openai"

    def test_error_to_dict_no_details(self):
        """Test error serialization without details."""
        error = OpenAIProviderError(
            code=OPENAI_SERVICE_ERROR,
            message="Service error",
        )

        result = error.to_dict()

        assert result["error"] == OPENAI_SERVICE_ERROR
        assert result["message"] == "Service error"
        assert "details" not in result


class TestHelperFunctions:
    """Tests for helper functions."""

    def test_get_language_name_common(self):
        """Test language name lookup for common languages."""
        assert _get_language_name("en") == "English"
        assert _get_language_name("fr") == "French"
        assert _get_language_name("es") == "Spanish"
        assert _get_language_name("de") == "German"
        assert _get_language_name("zh") == "Chinese"
        assert _get_language_name("ja") == "Japanese"

    def test_get_language_name_with_variant(self):
        """Test language name lookup with variant codes."""
        assert _get_language_name("en-US") == "English"
        assert _get_language_name("pt-BR") == "Portuguese"

    def test_get_language_name_unknown(self):
        """Test language name lookup for unknown codes."""
        assert _get_language_name("xx") == "xx"

    def test_build_system_prompt_default(self):
        """Test default system prompt generation."""
        prompt = _build_system_prompt("English", "French")

        assert "English" in prompt
        assert "French" in prompt
        assert "translator" in prompt.lower()

    def test_build_system_prompt_custom(self):
        """A custom prompt AUGMENTS the base translation instructions.

        The base prompt is always present — a glossary-only custom prompt
        used to produce a system prompt with no translation instruction
        at all (fixed 2026-08-29).
        """
        custom = "Translate this text formally for business context."
        prompt = _build_system_prompt("English", "French", custom)

        # Base translation instructions survive
        assert "English" in prompt
        assert "French" in prompt
        assert "translator" in prompt.lower()
        # Custom content is appended, not replacing
        assert custom in prompt
        assert "ADDITIONAL CONTEXT AND INSTRUCTIONS" in prompt
        # The base part comes first
        assert prompt.index("French") < prompt.index(custom)


class TestOpenAITranslationProvider:
    """Tests for OpenAITranslationProvider."""

    @pytest.fixture
    def provider(self):
        """Create an OpenAI provider instance."""
        return OpenAITranslationProvider(
            api_key="test-api-key",
            model="gpt-4o-mini",
            timeout=60,
            max_retries=0,
        )

    @pytest.fixture
    def provider_with_retries(self):
        """Create an OpenAI provider with retries."""
        return OpenAITranslationProvider(
            api_key="test-api-key",
            model="gpt-4o-mini",
            timeout=60,
            max_retries=2,
            retry_delay=0.01,
        )

    def test_init(self, provider):
        """Test provider initialization."""
        assert provider._api_key == "test-api-key"
        assert provider._model == "gpt-4o-mini"
        assert provider._base_url == "https://api.openai.com/v1"
        assert provider._timeout == 60
        assert provider._provider_name == "openai"

    def test_init_with_custom_base_url(self):
        """Test provider initialization with custom base URL."""
        provider = OpenAITranslationProvider(
            api_key="test-api-key",
            model="gpt-4",
            base_url="https://custom.openai.com/v1",
        )
        assert provider._base_url == "https://custom.openai.com/v1"

    def test_get_name(self, provider):
        """Test provider name."""
        assert provider.get_name() == "openai"

    def test_translate_text_empty(self, provider):
        """Test translating empty text."""
        request = TranslationRequest(text="", target_language="fr")
        response = provider.translate_text(request)

        assert response.translated_text == ""
        assert response.provider_name == "openai"
        assert response.from_cache is False

    def test_translate_text_whitespace(self, provider):
        """Test translating whitespace-only text."""
        request = TranslationRequest(text="   ", target_language="fr")
        response = provider.translate_text(request)

        assert response.translated_text == "   "

    @patch("requests.post")
    def test_translate_text_success(self, mock_post, provider):
        """Test successful translation."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "id": "chatcmpl-123",
            "choices": [{"message": {"content": "Bonjour"}}],
            "usage": {"total_tokens": 10},
        }
        mock_post.return_value = mock_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.translated_text == "Bonjour"
        assert response.provider_name == "openai"
        assert response.from_cache is False

    @patch("requests.post")
    def test_translate_text_with_custom_prompt(self, mock_post, provider):
        """Test translation with custom system prompt."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "id": "chatcmpl-123",
            "choices": [{"message": {"content": "Bonjour (formal)"}}],
            "usage": {"total_tokens": 15},
        }
        mock_post.return_value = mock_response

        request = TranslationRequest(
            text="Hello",
            target_language="fr",
            metadata={"custom_prompt": "Translate formally for business"},
        )
        response = provider.translate_text(request)

        assert response.translated_text == "Bonjour (formal)"
        # Verify custom prompt was used in API call
        call_args = mock_post.call_args
        assert (
            "Translate formally for business"
            in call_args[1]["json"]["messages"][0]["content"]
        )

    def test_translate_batch_empty(self, provider):
        """Test batch translation with empty list."""
        responses = provider.translate_batch([])
        assert responses == []

    @patch.object(OpenAITranslationProvider, "translate_text")
    def test_translate_batch(self, mock_translate, provider):
        """Test batch translation."""
        mock_translate.side_effect = [
            TranslationResponse(translated_text="Bonjour", provider_name="openai"),
            TranslationResponse(translated_text="Au revoir", provider_name="openai"),
        ]

        requests = [
            TranslationRequest(text="Hello", target_language="fr"),
            TranslationRequest(text="Goodbye", target_language="fr"),
        ]
        responses = provider.translate_batch(requests)

        assert len(responses) == 2
        assert responses[0].translated_text == "Bonjour"
        assert responses[1].translated_text == "Au revoir"


class TestOpenAIErrorHandling:
    """Tests for OpenAI error handling."""

    @pytest.fixture
    def provider(self):
        return OpenAITranslationProvider(
            api_key="test-key",
            model="gpt-4o-mini",
            timeout=60,
            max_retries=0,
        )

    @patch("requests.post")
    def test_rate_limit_error(self, mock_post, provider):
        """Test rate limit error handling."""
        mock_response = MagicMock()
        mock_response.status_code = 429
        mock_response.json.return_value = {
            "error": {"code": "rate_limit_exceeded", "message": "Rate limit exceeded"}
        }
        mock_response.headers = {"retry-after": "20"}
        mock_post.return_value = mock_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_RATE_LIMITED
        assert "20" in response.error or "Limite" in response.error

    @patch("requests.post")
    def test_invalid_api_key_error(self, mock_post, provider):
        """Test invalid API key error handling."""
        mock_response = MagicMock()
        mock_response.status_code = 401
        mock_response.json.return_value = {
            "error": {"code": "invalid_api_key", "message": "Invalid API key"}
        }
        mock_post.return_value = mock_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_INVALID_KEY
        assert "invalide" in response.error.lower() or "Invalid" in response.error

    @patch("requests.post")
    def test_quota_exceeded_error(self, mock_post, provider):
        """Test quota exceeded error handling."""
        mock_response = MagicMock()
        mock_response.status_code = 429
        mock_response.json.return_value = {
            "error": {
                "code": "insufficient_quota",
                "message": "You exceeded your current quota",
            }
        }
        mock_post.return_value = mock_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_QUOTA_EXCEEDED
        assert "quota" in response.error.lower() or "Quota" in response.error

    @patch("requests.post")
    def test_context_too_long_error(self, mock_post, provider):
        """Test context length exceeded error."""
        mock_response = MagicMock()
        mock_response.status_code = 400
        mock_response.json.return_value = {
            "error": {
                "code": "context_length_exceeded",
                "message": "This model's maximum context length is 4097 tokens",
            }
        }
        mock_post.return_value = mock_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_CONTEXT_TOO_LONG
        assert "trop long" in response.error.lower() or "long" in response.error.lower()

    @patch("requests.post")
    def test_service_error(self, mock_post, provider):
        """Test OpenAI service error (500)."""
        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_response.json.return_value = {
            "error": {"code": "server_error", "message": "The server had an error"}
        }
        mock_post.return_value = mock_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_SERVICE_ERROR
        assert response.error is not None

    @patch("requests.post")
    def test_timeout_error(self, mock_post, provider):
        """Test timeout error handling."""
        mock_post.side_effect = Timeout("Request timed out")

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_TIMEOUT
        assert "délai" in response.error.lower() or "timeout" in response.error.lower()

    @patch("requests.post")
    def test_connection_error(self, mock_post, provider):
        """Test connection error handling."""
        mock_post.side_effect = RequestsConnectionError("Connection failed")

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        # Connection errors are mapped to service error
        assert response.error is not None
        assert response.error_code is not None


class TestOpenAIRetryLogic:
    """Tests for retry logic."""

    @pytest.fixture
    def provider_with_retries(self):
        return OpenAITranslationProvider(
            api_key="test-key",
            model="gpt-4o-mini",
            timeout=60,
            max_retries=2,
            retry_delay=0.01,  # Fast for testing
        )

    @patch("requests.post")
    def test_retry_on_rate_limit(self, mock_post, provider_with_retries):
        """Test retry on rate limit error."""
        # First call fails with rate limit, second succeeds
        error_response = MagicMock()
        error_response.status_code = 429
        error_response.json.return_value = {
            "error": {"code": "rate_limit_exceeded", "message": "Rate limited"}
        }
        error_response.headers = {}

        success_response = MagicMock()
        success_response.status_code = 200
        success_response.json.return_value = {
            "id": "chatcmpl-123",
            "choices": [{"message": {"content": "Bonjour"}}],
            "usage": {"total_tokens": 10},
        }

        mock_post.side_effect = [error_response, success_response]

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider_with_retries.translate_text(request)

        assert response.translated_text == "Bonjour"
        assert response.error is None
        assert mock_post.call_count == 2

    @patch("requests.post")
    def test_retry_exhausted(self, mock_post, provider_with_retries):
        """Test that retry eventually gives up."""
        error_response = MagicMock()
        error_response.status_code = 429
        error_response.json.return_value = {
            "error": {"code": "rate_limit_exceeded", "message": "Rate limited"}
        }
        error_response.headers = {}

        mock_post.return_value = error_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider_with_retries.translate_text(request)

        assert response.error is not None
        assert response.error_code == OPENAI_RATE_LIMITED
        assert mock_post.call_count == 3  # Initial + 2 retries


class TestOpenAIHealthCheck:
    """Tests for health check functionality."""

    @pytest.fixture
    def provider(self):
        return OpenAITranslationProvider(
            api_key="test-key",
            model="gpt-4o-mini",
            timeout=60,
        )

    @patch("requests.get")
    def test_is_available_success(self, mock_get, provider):
        """Test is_available when API is reachable."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"data": [{"id": "gpt-4"}]}
        mock_get.return_value = mock_response

        assert provider.is_available() is True

    @patch("requests.get")
    def test_is_available_failure(self, mock_get, provider):
        """Test is_available when API is unreachable."""
        mock_get.side_effect = RequestsConnectionError("Connection failed")

        assert provider.is_available() is False

    @patch("requests.get")
    def test_health_check_success(self, mock_get, provider):
        """Test health check success."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"data": [{"id": "gpt-4o-mini"}]}
        mock_get.return_value = mock_response

        status = provider.health_check()

        assert status.name == "openai"
        assert status.available is True
        assert status.latency_ms is not None

    @patch("requests.get")
    def test_health_check_failure(self, mock_get, provider):
        """Test health check failure."""
        mock_get.side_effect = RequestsConnectionError("Connection failed")

        status = provider.health_check()

        assert status.name == "openai"
        assert status.available is False
        assert status.error is not None

    @patch("requests.get")
    def test_health_check_caching(self, mock_get, provider):
        """Test that health check results are cached."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"data": []}
        mock_get.return_value = mock_response

        # First call should hit the API
        provider.health_check()
        assert mock_get.call_count == 1

        # Second call should use cache
        provider.health_check()
        assert mock_get.call_count == 1  # No additional call


class TestOpenAIRegistryIntegration:
    """Tests for registry integration."""

    def test_register_openai_provider(self):
        """Test provider registration."""
        from services.providers.registry import registry

        registry.unregister("openai")

        with patch(
            "services.providers.openai_provider.get_openai_provider"
        ) as mock_get:
            mock_provider = MagicMock()
            mock_get.return_value = mock_provider

            result = register_openai_provider()

            assert result == mock_provider
            assert "openai" in registry
            registry.unregister("openai")

    def test_get_openai_provider_singleton(self):
        """Test that get_openai_provider returns a singleton."""
        import services.providers.openai_provider as openai_module

        # Reset singleton
        openai_module._provider_instance = None

        # Create first provider directly without mocking
        # (singleton pattern is simple enough to test directly)
        provider1 = OpenAITranslationProvider(
            api_key="test-key",
            model="gpt-4o-mini",
        )

        # Manually set the singleton
        openai_module._provider_instance = provider1

        # Second call should return same instance
        provider2 = get_openai_provider()

        assert provider1 is provider2

        # Reset singleton after test
        openai_module._provider_instance = None

    def test_reset_openai_provider(self):
        """Test reset_openai_provider clears the singleton."""
        import services.providers.openai_provider as openai_module

        # Set up a singleton
        openai_module._provider_instance = OpenAITranslationProvider(
            api_key="test-key",
            model="gpt-4o-mini",
        )

        # Verify it's set
        assert openai_module._provider_instance is not None

        # Reset
        reset_openai_provider()

        # Verify it's cleared
        assert openai_module._provider_instance is None


class TestOpenAIValidation:
    """Tests for input validation."""

    def test_empty_api_key_raises_error(self):
        """Test that empty API key raises ValueError."""
        with pytest.raises(ValueError, match="API key cannot be empty"):
            OpenAITranslationProvider(api_key="")

    def test_whitespace_api_key_raises_error(self):
        """Test that whitespace-only API key raises ValueError."""
        with pytest.raises(ValueError, match="API key cannot be empty"):
            OpenAITranslationProvider(api_key="   ")

    def test_text_too_long_preemptive_check(self):
        """Test preemptive check for text exceeding token limit."""
        provider = OpenAITranslationProvider(
            api_key="test-key",
            model="gpt-4o-mini",
            max_retries=0,
        )

        # Create text longer than 16000 chars (~4000 tokens)
        long_text = "x" * 17000
        request = TranslationRequest(text=long_text, target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_CONTEXT_TOO_LONG
        assert response.error is not None
        assert "trop long" in response.error.lower()


class TestOpenAIMalformedResponses:
    """Tests for malformed API response handling."""

    @pytest.fixture
    def provider(self):
        return OpenAITranslationProvider(
            api_key="test-key",
            model="gpt-4o-mini",
            timeout=60,
            max_retries=0,
        )

    @patch("requests.post")
    def test_empty_choices_array(self, mock_post, provider):
        """Test handling of empty choices array."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "id": "chatcmpl-123",
            "choices": [],
            "usage": {"total_tokens": 10},
        }
        mock_post.return_value = mock_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_SERVICE_ERROR
        assert "vide" in response.error.lower()

    @patch("requests.post")
    def test_missing_message_content(self, mock_post, provider):
        """Test handling of missing message content."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "id": "chatcmpl-123",
            "choices": [{"message": {}}],
            "usage": {"total_tokens": 10},
        }
        mock_post.return_value = mock_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_SERVICE_ERROR

    @patch("requests.post")
    def test_missing_message_key(self, mock_post, provider):
        """Test handling of missing message key in choice."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "id": "chatcmpl-123",
            "choices": [{"finish_reason": "stop"}],
            "usage": {"total_tokens": 10},
        }
        mock_post.return_value = mock_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_SERVICE_ERROR

    @patch("requests.post")
    def test_empty_content_string(self, mock_post, provider):
        """Test handling of empty content string."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "id": "chatcmpl-123",
            "choices": [{"message": {"content": ""}}],
            "usage": {"total_tokens": 10},
        }
        mock_post.return_value = mock_response

        request = TranslationRequest(text="Hello", target_language="fr")
        response = provider.translate_text(request)

        assert response.error_code == OPENAI_SERVICE_ERROR


class TestOpenAIHealthCheckModelInfo:
    """Tests for health check model info."""

    @pytest.fixture
    def provider(self):
        return OpenAITranslationProvider(
            api_key="test-key",
            model="gpt-4o-mini",
            timeout=60,
            health_check_timeout=5,
        )

    @patch("requests.get")
    def test_health_check_includes_model(self, mock_get, provider):
        """Test that health check includes model info."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "data": [
                {"id": "gpt-4o-mini"},
                {"id": "gpt-4"},
            ]
        }
        mock_get.return_value = mock_response

        status = provider.health_check()

        assert status.model == "gpt-4o-mini"
        assert status.model_available is True

    @patch("requests.get")
    def test_health_check_model_not_available(self, mock_get, provider):
        """Test health check when configured model not in list."""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "data": [
                {"id": "gpt-4"},
                {"id": "gpt-3.5-turbo"},
            ]
        }
        mock_get.return_value = mock_response

        status = provider.health_check()

        assert status.model == "gpt-4o-mini"
        assert status.model_available is False

    @patch("requests.get")
    def test_health_check_unavailable_includes_model(self, mock_get, provider):
        """Test that health check includes model even when unavailable."""
        mock_get.side_effect = RequestsConnectionError("Connection failed")

        status = provider.health_check()

        assert status.available is False
        assert status.model == "gpt-4o-mini"
        assert status.model_available is False
