"""
Tests for the fallback translation service.
"""

import pytest
from unittest.mock import MagicMock, patch

from services.providers.fallback import (
    translate_with_fallback,
    translate_with_fallback_by_mode,
    AllProvidersFailedError,
    ALL_PROVIDERS_FAILED,
)
from services.providers.schemas import TranslationRequest, TranslationResponse
from services.providers.registry import registry


class TestAllProvidersFailedError:
    """Tests for AllProvidersFailedError exception."""

    def test_error_creation_defaults(self):
        """Test error creation with default values."""
        error = AllProvidersFailedError()

        assert error.code == ALL_PROVIDERS_FAILED
        assert "Tous les fournisseurs" in error.message
        assert error.providers_tried == []
        assert error.errors == []

    def test_error_creation_with_details(self):
        """Test error creation with specific details."""
        error = AllProvidersFailedError(
            message="Custom error message",
            providers_tried=["google", "deepl"],
            errors=[
                {"provider": "google", "error_code": "RATE_LIMITED"},
                {"provider": "deepl", "error_code": "TIMEOUT"},
            ],
        )

        assert error.code == ALL_PROVIDERS_FAILED
        assert error.message == "Custom error message"
        assert error.providers_tried == ["google", "deepl"]
        assert len(error.errors) == 2

    def test_error_to_dict(self):
        """Test error serialization to dict."""
        error = AllProvidersFailedError(
            providers_tried=["google", "deepl"],
            errors=[
                {
                    "provider": "google",
                    "error_code": "RATE_LIMITED",
                    "message": "Rate limit",
                },
            ],
        )

        result = error.to_dict()

        assert result["error"] == ALL_PROVIDERS_FAILED
        assert "Tous les fournisseurs" in result["message"]
        assert result["details"]["providers_tried"] == ["google", "deepl"]
        assert result["details"]["error_count"] == 1
        assert "last_error" in result["details"]

    def test_error_to_dict_no_errors(self):
        """Test error serialization without errors."""
        error = AllProvidersFailedError(providers_tried=[])

        result = error.to_dict()

        assert result["details"]["error_count"] == 0
        assert "last_error" not in result["details"]


class TestTranslateWithFallback:
    """Tests for translate_with_fallback function."""

    @pytest.fixture(autouse=True)
    def clean_registry(self):
        """Clean up registry before each test."""
        # Save original providers
        original_providers = dict(registry._providers)
        registry.clear()
        yield
        # Restore original providers
        registry.clear()
        for name, provider in original_providers.items():
            registry.register(name, provider)

    def test_empty_provider_list(self):
        """Test that empty provider list raises error."""
        request = TranslationRequest(text="Hello", target_language="fr")

        with pytest.raises(AllProvidersFailedError) as exc_info:
            translate_with_fallback(request, [])

        assert exc_info.value.code == ALL_PROVIDERS_FAILED
        assert exc_info.value.providers_tried == []

    def test_single_provider_success(self):
        """Test successful translation with single provider."""
        mock_provider = MagicMock()
        mock_provider.is_available.return_value = True
        mock_provider.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour",
            provider_name="google",
        )
        registry.register("google", mock_provider)

        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["google"])

        assert response.translated_text == "Bonjour"
        assert response.provider_name == "google"
        assert response.error is None
        mock_provider.translate_text.assert_called_once()

    def test_first_provider_succeeds(self):
        """Test that first provider is used when it succeeds."""
        mock_google = MagicMock()
        mock_google.is_available.return_value = True
        mock_google.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour",
            provider_name="google",
        )
        registry.register("google", mock_google)

        mock_deepl = MagicMock()
        registry.register("deepl", mock_deepl)

        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["google", "deepl"])

        assert response.translated_text == "Bonjour"
        assert response.provider_name == "google"
        mock_google.translate_text.assert_called_once()
        mock_deepl.translate_text.assert_not_called()

    def test_fallback_on_provider_error(self):
        """Test fallback when first provider returns error."""
        mock_google = MagicMock()
        mock_google.is_available.return_value = True
        mock_google.translate_text.return_value = TranslationResponse(
            translated_text="",
            provider_name="google",
            error="Rate limit exceeded",
            error_code="RATE_LIMITED",
        )
        registry.register("google", mock_google)

        mock_deepl = MagicMock()
        mock_deepl.is_available.return_value = True
        mock_deepl.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour",
            provider_name="deepl",
        )
        registry.register("deepl", mock_deepl)

        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["google", "deepl"])

        assert response.translated_text == "Bonjour"
        assert response.provider_name == "deepl"
        mock_google.translate_text.assert_called_once()
        mock_deepl.translate_text.assert_called_once()

    def test_fallback_on_provider_exception(self):
        """Test fallback when first provider raises exception."""
        mock_google = MagicMock()
        mock_google.is_available.return_value = True
        mock_google.translate_text.side_effect = Exception("Connection failed")
        registry.register("google", mock_google)

        mock_deepl = MagicMock()
        mock_deepl.is_available.return_value = True
        mock_deepl.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour",
            provider_name="deepl",
        )
        registry.register("deepl", mock_deepl)

        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["google", "deepl"])

        assert response.translated_text == "Bonjour"
        assert response.provider_name == "deepl"

    def test_all_providers_fail(self):
        """Test that error is raised when all providers fail."""
        mock_google = MagicMock()
        mock_google.is_available.return_value = True
        mock_google.translate_text.return_value = TranslationResponse(
            translated_text="",
            provider_name="google",
            error="Rate limit",
            error_code="RATE_LIMITED",
        )
        registry.register("google", mock_google)

        mock_deepl = MagicMock()
        mock_deepl.is_available.return_value = True
        mock_deepl.translate_text.return_value = TranslationResponse(
            translated_text="",
            provider_name="deepl",
            error="Timeout",
            error_code="TIMEOUT",
        )
        registry.register("deepl", mock_deepl)

        request = TranslationRequest(text="Hello", target_language="fr")

        with pytest.raises(AllProvidersFailedError) as exc_info:
            translate_with_fallback(request, ["google", "deepl"])

        assert exc_info.value.code == ALL_PROVIDERS_FAILED
        assert "google" in exc_info.value.providers_tried
        assert "deepl" in exc_info.value.providers_tried
        assert len(exc_info.value.errors) == 2

    def test_skip_unavailable_provider(self):
        """Test that unavailable providers are skipped."""
        mock_google = MagicMock()
        mock_google.is_available.return_value = False
        registry.register("google", mock_google)

        mock_deepl = MagicMock()
        mock_deepl.is_available.return_value = True
        mock_deepl.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour",
            provider_name="deepl",
        )
        registry.register("deepl", mock_deepl)

        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["google", "deepl"])

        assert response.translated_text == "Bonjour"
        assert response.provider_name == "deepl"
        mock_google.translate_text.assert_not_called()
        mock_deepl.translate_text.assert_called_once()

    def test_try_unavailable_when_skip_disabled(self):
        """Test unavailable providers are tried when skip_unavailable=False."""
        mock_google = MagicMock()
        mock_google.is_available.return_value = False
        mock_google.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour",
            provider_name="google",
        )
        registry.register("google", mock_google)

        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["google"], skip_unavailable=False)

        assert response.translated_text == "Bonjour"
        mock_google.translate_text.assert_called_once()

    def test_provider_not_registered(self):
        """Test handling of unregistered provider names."""
        mock_deepl = MagicMock()
        mock_deepl.is_available.return_value = True
        mock_deepl.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour",
            provider_name="deepl",
        )
        registry.register("deepl", mock_deepl)

        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["unknown", "deepl"])

        assert response.translated_text == "Bonjour"
        assert response.provider_name == "deepl"

    def test_response_provider_name_set(self):
        """Test that provider_name is set in response even if not present."""
        mock_provider = MagicMock()
        mock_provider.is_available.return_value = True
        # Response without provider_name
        mock_provider.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour",
            provider_name="",  # Empty
        )
        registry.register("test_provider", mock_provider)

        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["test_provider"])

        assert response.provider_name == "test_provider"

    def test_logs_failed_attempts(self):
        """Test that failed attempts are logged."""
        mock_google = MagicMock()
        mock_google.is_available.return_value = True
        mock_google.translate_text.return_value = TranslationResponse(
            translated_text="",
            provider_name="google",
            error="Rate limit",
            error_code="RATE_LIMITED",
        )
        registry.register("google", mock_google)

        mock_deepl = MagicMock()
        mock_deepl.is_available.return_value = True
        mock_deepl.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour",
            provider_name="deepl",
        )
        registry.register("deepl", mock_deepl)

        with patch("services.providers.fallback._log_warning") as mock_log:
            request = TranslationRequest(text="Hello", target_language="fr")
            translate_with_fallback(request, ["google", "deepl"])

            # Should log the failed attempt
            mock_log.assert_any_call(
                "fallback_provider_error",
                provider="google",
                error_code="RATE_LIMITED",
                error_message="Rate limit",
            )


class TestTranslateWithFallbackByMode:
    """Tests for translate_with_fallback_by_mode function."""

    @patch("services.providers.fallback.translate_with_fallback")
    def test_classic_mode(self, mock_translate):
        """Test classic mode uses classic fallback chain."""
        mock_translate.return_value = MagicMock()

        with patch("services.providers.config.ProvidersConfig") as mock_config:
            mock_config.get_fallback_chain.return_value = ["google", "deepl"]

            request = TranslationRequest(text="Hello", target_language="fr")
            translate_with_fallback_by_mode(request, mode="classic")

            mock_config.get_fallback_chain.assert_called_once_with("classic")
            mock_translate.assert_called_once()

    @patch("services.providers.fallback.translate_with_fallback")
    def test_llm_mode(self, mock_translate):
        """Test LLM mode uses LLM fallback chain."""
        mock_translate.return_value = MagicMock()

        with patch("services.providers.config.ProvidersConfig") as mock_config:
            mock_config.get_fallback_chain.return_value = ["openrouter", "openai"]

            request = TranslationRequest(text="Hello", target_language="fr")
            translate_with_fallback_by_mode(request, mode="llm")

            mock_config.get_fallback_chain.assert_called_once_with("llm")

    @patch("services.providers.fallback.translate_with_fallback")
    def test_auto_mode(self, mock_translate):
        """Test auto mode uses general fallback chain."""
        mock_translate.return_value = MagicMock()

        with patch("services.providers.config.ProvidersConfig") as mock_config:
            mock_config.get_fallback_chain.return_value = ["google", "deepl", "openai"]

            request = TranslationRequest(text="Hello", target_language="fr")
            translate_with_fallback_by_mode(request, mode="auto")

            mock_config.get_fallback_chain.assert_called_once_with("auto")

    def test_empty_chain_raises_error(self):
        """Test that empty chain raises AllProvidersFailedError."""
        with patch("services.providers.config.ProvidersConfig") as mock_config:
            mock_config.get_fallback_chain.return_value = []

            request = TranslationRequest(text="Hello", target_language="fr")

            with pytest.raises(AllProvidersFailedError) as exc_info:
                translate_with_fallback_by_mode(request, mode="classic")

            assert exc_info.value.code == ALL_PROVIDERS_FAILED
            assert "classic" in exc_info.value.message


class TestFallbackChainOrder:
    """Tests for fallback chain ordering and behavior."""

    @pytest.fixture(autouse=True)
    def clean_registry(self):
        """Clean up registry before each test."""
        original_providers = dict(registry._providers)
        registry.clear()
        yield
        registry.clear()
        for name, provider in original_providers.items():
            registry.register(name, provider)

    def test_chain_order_respected(self):
        """Test that providers are tried in the exact order specified."""
        call_order = []

        def create_mock_provider(name, should_succeed):
            mock = MagicMock()
            mock.is_available.return_value = True
            if should_succeed:
                mock.translate_text.side_effect = lambda req: (
                    call_order.append(name),
                    TranslationResponse(
                        translated_text=f"{name}_result", provider_name=name
                    ),
                )[1]
            else:
                mock.translate_text.side_effect = lambda req: (
                    call_order.append(name),
                    TranslationResponse(
                        translated_text="",
                        provider_name=name,
                        error="Failed",
                        error_code="FAILED",
                    ),
                )[1]
            return mock

        # All fail except last
        registry.register("first", create_mock_provider("first", False))
        registry.register("second", create_mock_provider("second", False))
        registry.register("third", create_mock_provider("third", True))

        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["first", "second", "third"])

        assert call_order == ["first", "second", "third"]
        assert response.translated_text == "third_result"

    def test_partial_chain_stops_on_success(self):
        """Test that chain stops when a provider succeeds."""
        mock_first = MagicMock()
        mock_first.is_available.return_value = True
        mock_first.translate_text.return_value = TranslationResponse(
            translated_text="Result",
            provider_name="first",
        )
        registry.register("first", mock_first)

        mock_second = MagicMock()
        registry.register("second", mock_second)

        request = TranslationRequest(text="Hello", target_language="fr")
        translate_with_fallback(request, ["first", "second"])

        mock_first.translate_text.assert_called_once()
        mock_second.translate_text.assert_not_called()

    def test_error_details_accumulated(self):
        """Test that error details from all providers are accumulated."""
        mock_google = MagicMock()
        mock_google.is_available.return_value = True
        mock_google.translate_text.return_value = TranslationResponse(
            translated_text="",
            provider_name="google",
            error="Google error",
            error_code="GOOGLE_ERROR",
        )
        registry.register("google", mock_google)

        mock_deepl = MagicMock()
        mock_deepl.is_available.return_value = True
        mock_deepl.translate_text.return_value = TranslationResponse(
            translated_text="",
            provider_name="deepl",
            error="DeepL error",
            error_code="DEEPL_ERROR",
        )
        registry.register("deepl", mock_deepl)

        request = TranslationRequest(text="Hello", target_language="fr")

        with pytest.raises(AllProvidersFailedError) as exc_info:
            translate_with_fallback(request, ["google", "deepl"])

        errors = exc_info.value.errors
        assert len(errors) == 2
        assert errors[0]["provider"] == "google"
        assert errors[0]["error_code"] == "GOOGLE_ERROR"
        assert errors[1]["provider"] == "deepl"
        assert errors[1]["error_code"] == "DEEPL_ERROR"


class TestIntegrationWithRealRegistry:
    """Integration-style tests with real registry."""

    @pytest.fixture(autouse=True)
    def clean_registry(self):
        """Clean up registry before each test."""
        original_providers = dict(registry._providers)
        registry.clear()
        yield
        registry.clear()
        for name, provider in original_providers.items():
            registry.register(name, provider)

    def test_end_to_end_success(self):
        """End-to-end test with mocked providers in real registry."""
        # Create realistic mock providers
        google = MagicMock()
        google.is_available.return_value = True
        google.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour from Google",
            provider_name="google",
        )

        deepl = MagicMock()
        deepl.is_available.return_value = True
        deepl.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour from DeepL",
            provider_name="deepl",
        )

        # Register them
        registry.register("google", google)
        registry.register("deepl", deepl)

        # Test: First succeeds
        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["google", "deepl"])

        assert response.translated_text == "Bonjour from Google"
        assert response.provider_name == "google"

    def test_end_to_end_fallback(self):
        """End-to-end test with fallback scenario."""
        google = MagicMock()
        google.is_available.return_value = True
        google.translate_text.return_value = TranslationResponse(
            translated_text="",
            provider_name="google",
            error="Rate limit",
            error_code="RATE_LIMITED",
        )

        deepl = MagicMock()
        deepl.is_available.return_value = True
        deepl.translate_text.return_value = TranslationResponse(
            translated_text="Bonjour from DeepL",
            provider_name="deepl",
        )

        registry.register("google", google)
        registry.register("deepl", deepl)

        request = TranslationRequest(text="Hello", target_language="fr")
        response = translate_with_fallback(request, ["google", "deepl"])

        assert response.translated_text == "Bonjour from DeepL"
        assert response.provider_name == "deepl"

    def test_end_to_end_all_fail(self):
        """End-to-end test when all providers fail."""
        google = MagicMock()
        google.is_available.return_value = True
        google.translate_text.return_value = TranslationResponse(
            translated_text="",
            provider_name="google",
            error="Google failed",
            error_code="GOOGLE_FAIL",
        )

        deepl = MagicMock()
        deepl.is_available.return_value = True
        deepl.translate_text.return_value = TranslationResponse(
            translated_text="",
            provider_name="deepl",
            error="DeepL failed",
            error_code="DEEPL_FAIL",
        )

        registry.register("google", google)
        registry.register("deepl", deepl)

        request = TranslationRequest(text="Hello", target_language="fr")

        with pytest.raises(AllProvidersFailedError) as exc_info:
            translate_with_fallback(request, ["google", "deepl"])

        result = exc_info.value.to_dict()
        assert result["error"] == ALL_PROVIDERS_FAILED
        assert result["details"]["providers_tried"] == ["google", "deepl"]
        assert result["details"]["last_error"]["provider"] == "deepl"
