"""
Tests for the DeepSeekTranslationProvider.

Covers:
- defaults (public endpoint ``api.deepseek.com/v1``, model ``deepseek-chat``)
- translation success, 401/429 handling, empty-text short-circuit

Batch mode (ported from the OpenAI provider):
- same language pair + metadata chunk → ONE numbered-JSON request for all texts
- unparseable/failed batch answer → per-item fallback, every text still translated
- mixed metadata or a single text → no batch (individual requests)
"""

import json

import pytest
from unittest.mock import patch, MagicMock

from services.providers.deepseek_provider import (
    DeepSeekTranslationProvider,
    DeepSeekProviderError,
    DEEPSEEK_RATE_LIMITED,
    DEEPSEEK_INVALID_KEY,
    DEEPSEEK_TIMEOUT,
    DEEPSEEK_SERVICE_ERROR,
)
from services.providers.schemas import TranslationRequest


class TestDeepSeekProviderConfig:
    """Defaults must point at the real public endpoint."""

    def test_default_base_url_is_public_host(self):
        provider = DeepSeekTranslationProvider(api_key="k", max_retries=0)
        assert provider._base_url == "https://api.deepseek.com/v1"

    def test_default_model_is_deepseek_chat(self):
        provider = DeepSeekTranslationProvider(api_key="k", max_retries=0)
        assert provider._model == "deepseek-chat"

    def test_custom_base_url_respected(self):
        provider = DeepSeekTranslationProvider(
            api_key="k", base_url="https://proxy.example.com/v1", max_retries=0
        )
        assert provider._base_url == "https://proxy.example.com/v1"

    def test_get_name(self):
        provider = DeepSeekTranslationProvider(api_key="k", max_retries=0)
        assert provider.get_name() == "deepseek"

    def test_empty_api_key_raises(self):
        with pytest.raises(ValueError, match="API key cannot be empty"):
            DeepSeekTranslationProvider(api_key="   ")


class TestDeepSeekTranslateText:
    @pytest.fixture
    def provider(self):
        return DeepSeekTranslationProvider(api_key="k", max_retries=0)

    def _mock_post(self, payload, status_code=200):
        mock_response = MagicMock()
        mock_response.status_code = status_code
        mock_response.json.return_value = payload
        mock_response.text = ""
        return mock_response

    @patch("requests.post")
    def test_success(self, mock_post, provider):
        mock_post.return_value = self._mock_post(
            {"choices": [{"message": {"content": "Bonjour"}}], "usage": {}}
        )
        resp = provider.translate_text(TranslationRequest(text="Hello", target_language="fr"))
        assert resp.translated_text == "Bonjour"
        assert resp.provider_name == "deepseek"
        assert mock_post.call_args[0][0] == "https://api.deepseek.com/v1/chat/completions"

    def test_empty_text_short_circuits(self, provider):
        resp = provider.translate_text(TranslationRequest(text="", target_language="fr"))
        assert resp.translated_text == ""

    @patch("requests.post")
    def test_invalid_key_returns_error(self, mock_post, provider):
        mock_post.return_value = self._mock_post({"error": "bad key"}, status_code=401)
        resp = provider.translate_text(TranslationRequest(text="Hello", target_language="fr"))
        assert resp.error_code == DEEPSEEK_INVALID_KEY
        # Original text returned on failure.
        assert resp.translated_text == "Hello"

    @patch("time.sleep")
    @patch("requests.post")
    def test_rate_limit_then_success(self, mock_post, mock_sleep):
        provider = DeepSeekTranslationProvider(api_key="k", max_retries=2, retry_delay=0.01)
        mock_post.side_effect = [
            self._mock_post({"error": "slow down"}, status_code=429),
            self._mock_post({"choices": [{"message": {"content": "Hola"}}], "usage": {}}),
        ]
        resp = provider.translate_text(TranslationRequest(text="Hello", target_language="es"))
        assert resp.translated_text == "Hola"
        assert mock_sleep.called  # backoff happened

    @patch("requests.post")
    def test_service_error_when_empty_choices(self, mock_post, provider):
        mock_post.return_value = self._mock_post({"choices": []})
        resp = provider.translate_text(TranslationRequest(text="Hello", target_language="fr"))
        assert resp.error_code == DEEPSEEK_SERVICE_ERROR


class TestDeepSeekBatchTranslation:
    """Batch mode: one numbered-JSON request per same pair/metadata chunk."""

    @pytest.fixture
    def provider(self):
        return DeepSeekTranslationProvider(api_key="k", max_retries=0)

    def _mock_post(self, payload, status_code=200):
        mock_response = MagicMock()
        mock_response.status_code = status_code
        mock_response.json.return_value = payload
        mock_response.text = ""
        return mock_response

    def _batch_response(self, translations):
        """Mock a 200 response whose content is a numbered JSON array."""
        content = json.dumps(
            [{"id": i, "translation": t} for i, t in enumerate(translations)]
        )
        return self._mock_post({"choices": [{"message": {"content": content}}], "usage": {}})

    @patch("requests.post")
    def test_batch_of_three_uses_single_request(self, mock_post, provider):
        mock_post.return_value = self._batch_response(["Bonjour", "Au revoir", "Merci"])
        requests = [
            TranslationRequest(text="Hello", target_language="fr"),
            TranslationRequest(text="Goodbye", target_language="fr"),
            TranslationRequest(text="Thank you", target_language="fr"),
        ]

        responses = provider.translate_batch(requests)

        assert mock_post.call_count == 1
        assert [r.translated_text for r in responses] == ["Bonjour", "Au revoir", "Merci"]
        assert all(r.provider_name == "deepseek" for r in responses)
        assert all(r.error is None for r in responses)
        assert mock_post.call_args[0][0] == "https://api.deepseek.com/v1/chat/completions"

    @patch("requests.post")
    def test_batch_request_payload_is_numbered_json_with_instructions(self, mock_post, provider):
        mock_post.return_value = self._batch_response(["Bonjour", "Au revoir"])
        requests = [
            TranslationRequest(
                text="Hello",
                target_language="fr",
                source_language="en",
                metadata={"custom_prompt": "use a formal tone"},
            ),
            TranslationRequest(
                text="Goodbye",
                target_language="fr",
                source_language="en",
                metadata={"custom_prompt": "use a formal tone"},
            ),
        ]

        provider.translate_batch(requests)

        payload = mock_post.call_args[1]["json"]
        system = payload["messages"][0]["content"]
        user = payload["messages"][1]["content"]
        # System prompt = base instructions + custom prompt + strict JSON rule.
        assert "professional translator" in system
        assert "ADDITIONAL CONTEXT AND INSTRUCTIONS" in system
        assert "use a formal tone" in system
        assert "BATCH MODE" in system
        assert "ONLY a JSON array" in system
        # User message = the numbered JSON list of the texts.
        parsed = json.loads(user)
        assert [item["text"] for item in parsed] == ["Hello", "Goodbye"]
        assert [item["id"] for item in parsed] == [0, 1]

    @patch("requests.post")
    def test_invalid_batch_json_falls_back_to_individual(self, mock_post, provider):
        broken_batch = self._mock_post(
            {"choices": [{"message": {"content": "désolé, pas de JSON ici"}}], "usage": {}}
        )
        individuals = [
            self._mock_post({"choices": [{"message": {"content": "Bonjour"}}], "usage": {}}),
            self._mock_post({"choices": [{"message": {"content": "Au revoir"}}], "usage": {}}),
            self._mock_post({"choices": [{"message": {"content": "Merci"}}], "usage": {}}),
        ]
        mock_post.side_effect = [broken_batch] + individuals
        requests = [
            TranslationRequest(text="Hello", target_language="fr"),
            TranslationRequest(text="Goodbye", target_language="fr"),
            TranslationRequest(text="Thank you", target_language="fr"),
        ]

        responses = provider.translate_batch(requests)

        # 1 failed batch attempt + 3 individual calls.
        assert mock_post.call_count == 4
        assert [r.translated_text for r in responses] == ["Bonjour", "Au revoir", "Merci"]
        assert all(r.error is None for r in responses)

    @patch("requests.post")
    def test_batch_api_error_falls_back_to_individual(self, mock_post, provider):
        # The batch call itself fails (500) — per-item calls still translate everything.
        failed_batch = self._mock_post({"error": "boom"}, status_code=500)
        individuals = [
            self._mock_post({"choices": [{"message": {"content": "Bonjour"}}], "usage": {}}),
            self._mock_post({"choices": [{"message": {"content": "Au revoir"}}], "usage": {}}),
        ]
        mock_post.side_effect = [failed_batch] + individuals
        requests = [
            TranslationRequest(text="Hello", target_language="fr"),
            TranslationRequest(text="Goodbye", target_language="fr"),
        ]

        responses = provider.translate_batch(requests)

        assert mock_post.call_count == 3
        assert [r.translated_text for r in responses] == ["Bonjour", "Au revoir"]

    @patch("requests.post")
    def test_wrong_batch_length_falls_back_to_individual(self, mock_post, provider):
        # A JSON answer with a missing item is NOT trusted → fallback.
        short_batch = self._batch_response(["Bonjour", "Au revoir"])
        individuals = [
            self._mock_post({"choices": [{"message": {"content": "Bonjour"}}], "usage": {}}),
            self._mock_post({"choices": [{"message": {"content": "Au revoir"}}], "usage": {}}),
            self._mock_post({"choices": [{"message": {"content": "Merci"}}], "usage": {}}),
        ]
        mock_post.side_effect = [short_batch] + individuals
        requests = [
            TranslationRequest(text="Hello", target_language="fr"),
            TranslationRequest(text="Goodbye", target_language="fr"),
            TranslationRequest(text="Thank you", target_language="fr"),
        ]

        responses = provider.translate_batch(requests)

        assert mock_post.call_count == 4
        assert [r.translated_text for r in responses] == ["Bonjour", "Au revoir", "Merci"]

    @patch("requests.post")
    def test_mixed_metadata_skips_batch(self, mock_post, provider):
        mock_post.side_effect = [
            self._mock_post({"choices": [{"message": {"content": "Bonjour"}}], "usage": {}}),
            self._mock_post({"choices": [{"message": {"content": "Au revoir"}}], "usage": {}}),
        ]
        requests = [
            TranslationRequest(
                text="Hello", target_language="fr", metadata={"custom_prompt": "formal"}
            ),
            TranslationRequest(
                text="Goodbye", target_language="fr", metadata={"custom_prompt": "casual"}
            ),
        ]

        responses = provider.translate_batch(requests)

        # Two individual calls, no numbered-JSON batch attempt.
        assert mock_post.call_count == 2
        assert [r.translated_text for r in responses] == ["Bonjour", "Au revoir"]
        for call in mock_post.call_args_list:
            assert call[1]["json"]["messages"][1]["content"] in ("Hello", "Goodbye")

    @patch("requests.post")
    def test_mixed_language_pair_skips_batch(self, mock_post, provider):
        mock_post.side_effect = [
            self._mock_post({"choices": [{"message": {"content": "Bonjour"}}], "usage": {}}),
            self._mock_post({"choices": [{"message": {"content": "Hola"}}], "usage": {}}),
        ]
        requests = [
            TranslationRequest(text="Hello", target_language="fr"),
            TranslationRequest(text="Hello", target_language="es"),
        ]

        responses = provider.translate_batch(requests)

        assert mock_post.call_count == 2
        assert [r.translated_text for r in responses] == ["Bonjour", "Hola"]

    @patch("requests.post")
    def test_single_request_is_individual(self, mock_post, provider):
        mock_post.return_value = self._mock_post(
            {"choices": [{"message": {"content": "Bonjour"}}], "usage": {}}
        )
        requests = [TranslationRequest(text="Hello", target_language="fr")]

        responses = provider.translate_batch(requests)

        assert mock_post.call_count == 1
        assert responses[0].translated_text == "Bonjour"
        # The user message is the raw text, not a numbered JSON list.
        user_message = mock_post.call_args[1]["json"]["messages"][1]["content"]
        assert user_message == "Hello"

    def test_empty_batch_returns_empty(self, provider):
        assert provider.translate_batch([]) == []


class TestDeepSeekProviderError:
    def test_error_carries_code_and_message(self):
        err = DeepSeekProviderError(DEEPSEEK_TIMEOUT, "timed out", details={"wait": 1})
        assert err.code == DEEPSEEK_TIMEOUT
        assert err.message == "timed out"
        assert err.details == {"wait": 1}

    def test_rate_limited_code_exists(self):
        # Sanity: the retryable codes used by the provider are importable.
        assert DEEPSEEK_RATE_LIMITED == "DEEPSEEK_RATE_LIMITED"
