"""
Tests für RelevanceScorer.

Testet sowohl die LLM-Integration als auch das Response-Parsing.
"""

import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from datetime import datetime

from alerts_agent.processing.relevance_scorer import (
    RelevanceScorer,
    RelevanceDecision,
    ScoringResult,
    RELEVANCE_SYSTEM_PROMPT,
)
from alerts_agent.models.alert_item import AlertItem, AlertStatus
from alerts_agent.models.relevance_profile import RelevanceProfile


class TestScoringResult:
    """Tests für ScoringResult Dataclass."""

    def test_create_result(self):
        """Test ScoringResult Erstellung."""
        result = ScoringResult(
            alert_id="test-123",
            score=0.85,
            decision=RelevanceDecision.KEEP,
            reason_codes=["matches_priority"],
            summary="Relevant für Inklusion",
        )

        assert result.alert_id == "test-123"
        assert result.score == 0.85
        assert result.decision == RelevanceDecision.KEEP

    def test_result_to_dict(self):
        """Test Serialisierung."""
        result = ScoringResult(
            alert_id="test-123",
            score=0.5,
            decision=RelevanceDecision.REVIEW,
        )
        data = result.to_dict()

        assert data["alert_id"] == "test-123"
        assert data["decision"] == "REVIEW"
        assert "scored_at" in data

    def test_decision_enum(self):
        """Test RelevanceDecision Enum."""
        assert RelevanceDecision.KEEP.value == "KEEP"
        assert RelevanceDecision.DROP.value == "DROP"
        assert RelevanceDecision.REVIEW.value == "REVIEW"


class TestRelevanceScorerInit:
    """Tests für RelevanceScorer Initialisierung."""

    def test_default_config(self):
        """Test Default-Konfiguration."""
        scorer = RelevanceScorer()

        assert scorer.gateway_url == "http://localhost:8000/llm"
        assert scorer.model == "breakpilot-teacher-8b"
        assert scorer.keep_threshold == 0.7
        assert scorer.drop_threshold == 0.4

    def test_custom_config(self):
        """Test Custom-Konfiguration."""
        scorer = RelevanceScorer(
            gateway_url="http://custom:8080/llm",
            api_key="test-key",
            model="custom-model",
            timeout=60,
        )

        assert scorer.gateway_url == "http://custom:8080/llm"
        assert scorer.api_key == "test-key"
        assert scorer.model == "custom-model"
        assert scorer.timeout == 60


class TestPromptBuilding:
    """Tests für Prompt-Erstellung."""

    def test_build_user_prompt(self):
        """Test User-Prompt Erstellung."""
        scorer = RelevanceScorer()
        alert = AlertItem(
            title="Neue Inklusions-Richtlinie",
            url="https://example.com/inklusion",
            snippet="Das Kultusministerium hat...",
            topic_label="Inklusion Bayern",
        )

        prompt = scorer._build_user_prompt(alert)

        assert "Neue Inklusions-Richtlinie" in prompt
        assert "Inklusion Bayern" in prompt
        assert "https://example.com/inklusion" in prompt
        assert "Kultusministerium" in prompt

    def test_build_user_prompt_long_snippet(self):
        """Test Snippet wird gekürzt."""
        scorer = RelevanceScorer()
        alert = AlertItem(
            title="Test",
            url="https://example.com",
            snippet="x" * 1000,  # Langer Snippet
        )

        prompt = scorer._build_user_prompt(alert)

        # Sollte auf 500 Zeichen + "..." gekürzt sein
        assert "..." in prompt
        assert len(prompt) < 1000

    def test_build_system_prompt_without_profile(self):
        """Test System-Prompt ohne Profil."""
        scorer = RelevanceScorer()
        prompt = scorer._build_system_prompt(None)

        assert "Relevanz-Filter" in prompt
        assert "KEEP" in prompt
        assert "DROP" in prompt
        assert "JSON" in prompt

    def test_build_system_prompt_with_profile(self):
        """Test System-Prompt mit Profil."""
        scorer = RelevanceScorer()
        profile = RelevanceProfile()
        profile.add_priority("Inklusion", weight=0.9)
        profile.add_exclusion("Stellenanzeige")

        prompt = scorer._build_system_prompt(profile)

        assert "Relevanzprofil" in prompt
        assert "Inklusion" in prompt
        assert "Stellenanzeige" in prompt


class TestResponseParsing:
    """Tests für LLM Response Parsing."""

    def test_parse_valid_json(self):
        """Test Parse gültiges JSON."""
        scorer = RelevanceScorer()
        response = '''{"score": 0.85, "decision": "KEEP", "reason_codes": ["matches_priority"], "summary": "Relevant"}'''

        result = scorer._parse_response(response, "test-id")

        assert result.score == 0.85
        assert result.decision == RelevanceDecision.KEEP
        assert "matches_priority" in result.reason_codes
        assert result.summary == "Relevant"

    def test_parse_json_in_markdown(self):
        """Test Parse JSON in Markdown Code-Block."""
        scorer = RelevanceScorer()
        response = '''Hier ist meine Bewertung:
```json
{"score": 0.3, "decision": "DROP", "reason_codes": ["exclusion"]}
```
'''
        result = scorer._parse_response(response, "test-id")

        assert result.score == 0.3
        assert result.decision == RelevanceDecision.DROP

    def test_parse_invalid_json(self):
        """Test Parse ungültiges JSON."""
        scorer = RelevanceScorer()
        response = "Das ist kein JSON"

        result = scorer._parse_response(response, "test-id")

        assert result.score == 0.5  # Default
        assert result.decision == RelevanceDecision.REVIEW
        # Error reason code (could be "parse_error" or "error")
        assert any(code in result.reason_codes for code in ["parse_error", "error"])

    def test_parse_score_clamping(self):
        """Test Score wird auf 0-1 begrenzt."""
        scorer = RelevanceScorer()

        # Score > 1
        result = scorer._parse_response('{"score": 1.5, "decision": "KEEP"}', "test")
        assert result.score == 1.0

        # Score < 0
        result = scorer._parse_response('{"score": -0.5, "decision": "DROP"}', "test")
        assert result.score == 0.0

    def test_parse_invalid_decision_fallback(self):
        """Test Fallback bei ungültiger Decision."""
        scorer = RelevanceScorer()

        # Hoher Score → KEEP
        result = scorer._parse_response('{"score": 0.9, "decision": "INVALID"}', "test")
        assert result.decision == RelevanceDecision.KEEP

        # Niedriger Score → DROP
        result = scorer._parse_response('{"score": 0.1, "decision": "INVALID"}', "test")
        assert result.decision == RelevanceDecision.DROP

        # Mittlerer Score → REVIEW
        result = scorer._parse_response('{"score": 0.5, "decision": "INVALID"}', "test")
        assert result.decision == RelevanceDecision.REVIEW


class TestScoreAlert:
    """Tests für score_alert Methode."""

    @pytest.mark.asyncio
    async def test_score_alert_success(self):
        """Test erfolgreiches Scoring."""
        scorer = RelevanceScorer(api_key="test-key")

        alert = AlertItem(
            title="Inklusion in Bayern",
            url="https://example.com",
        )

        # Mock HTTP Response
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "choices": [{
                "message": {
                    "content": '{"score": 0.9, "decision": "KEEP", "reason_codes": ["priority"], "summary": "Relevant"}'
                }
            }]
        }

        with patch.object(scorer, "_get_client") as mock_get_client:
            mock_client = AsyncMock()
            mock_client.post.return_value = mock_response
            mock_get_client.return_value = mock_client

            result = await scorer.score_alert(alert)

        assert result.score == 0.9
        assert result.decision == RelevanceDecision.KEEP
        assert alert.relevance_score == 0.9
        assert alert.status == AlertStatus.SCORED

    @pytest.mark.asyncio
    async def test_score_alert_http_error(self):
        """Test HTTP Error Handling."""
        import httpx

        scorer = RelevanceScorer(api_key="test-key")
        alert = AlertItem(title="Test", url="https://example.com")

        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_response.text = "Internal Server Error"

        with patch.object(scorer, "_get_client") as mock_get_client:
            mock_client = AsyncMock()
            mock_client.post.side_effect = httpx.HTTPStatusError(
                "Error", request=MagicMock(), response=mock_response
            )
            mock_get_client.return_value = mock_client

            result = await scorer.score_alert(alert)

        assert result.decision == RelevanceDecision.REVIEW
        assert "gateway_error" in result.reason_codes
        assert result.error is not None

    @pytest.mark.asyncio
    async def test_score_alert_with_profile(self):
        """Test Scoring mit Profil."""
        scorer = RelevanceScorer(api_key="test-key")
        alert = AlertItem(title="Test", url="https://example.com")
        profile = RelevanceProfile()
        profile.add_priority("Test Topic", weight=0.9)

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "choices": [{"message": {"content": '{"score": 0.8, "decision": "KEEP"}'}}]
        }

        with patch.object(scorer, "_get_client") as mock_get_client:
            mock_client = AsyncMock()
            mock_client.post.return_value = mock_response
            mock_get_client.return_value = mock_client

            result = await scorer.score_alert(alert, profile=profile)

            # Prüfe dass Profil im Request verwendet wurde
            call_args = mock_client.post.call_args
            request_body = call_args[1]["json"]
            system_prompt = request_body["messages"][0]["content"]
            assert "Test Topic" in system_prompt


class TestScoreBatch:
    """Tests für score_batch Methode."""

    @pytest.mark.asyncio
    async def test_score_batch(self):
        """Test Batch-Scoring."""
        scorer = RelevanceScorer(api_key="test-key")

        alerts = [
            AlertItem(title="Alert 1", url="https://example.com/1"),
            AlertItem(title="Alert 2", url="https://example.com/2"),
        ]

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "choices": [{"message": {"content": '{"score": 0.7, "decision": "KEEP"}'}}]
        }

        with patch.object(scorer, "_get_client") as mock_get_client:
            mock_client = AsyncMock()
            mock_client.post.return_value = mock_response
            mock_get_client.return_value = mock_client

            results = await scorer.score_batch(alerts)

        assert len(results) == 2
        assert all(r.decision == RelevanceDecision.KEEP for r in results)

    @pytest.mark.asyncio
    async def test_score_batch_skip_scored(self):
        """Test Batch-Scoring überspringt bereits bewertete."""
        scorer = RelevanceScorer(api_key="test-key")

        alert1 = AlertItem(title="New", url="https://example.com/1")
        alert2 = AlertItem(title="Scored", url="https://example.com/2")
        alert2.status = AlertStatus.SCORED

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "choices": [{"message": {"content": '{"score": 0.5, "decision": "REVIEW"}'}}]
        }

        with patch.object(scorer, "_get_client") as mock_get_client:
            mock_client = AsyncMock()
            mock_client.post.return_value = mock_response
            mock_get_client.return_value = mock_client

            results = await scorer.score_batch([alert1, alert2], skip_scored=True)

        assert len(results) == 1


class TestScorerStats:
    """Tests für Scorer Statistiken."""

    def test_get_stats(self):
        """Test Stats Berechnung."""
        scorer = RelevanceScorer()

        results = [
            ScoringResult("1", 0.9, RelevanceDecision.KEEP),
            ScoringResult("2", 0.8, RelevanceDecision.KEEP),
            ScoringResult("3", 0.2, RelevanceDecision.DROP),
            ScoringResult("4", 0.5, RelevanceDecision.REVIEW),
            ScoringResult("5", 0.5, RelevanceDecision.REVIEW, error="Test Error"),
        ]

        stats = scorer.get_stats(results)

        assert stats["total"] == 5
        assert stats["keep"] == 2
        assert stats["drop"] == 1
        assert stats["review"] == 2
        assert stats["errors"] == 1
        assert stats["keep_rate"] == 0.4
        assert stats["avg_score"] == pytest.approx(0.58, rel=0.01)

    def test_get_stats_empty(self):
        """Test Stats für leere Liste."""
        scorer = RelevanceScorer()
        stats = scorer.get_stats([])

        assert stats["total"] == 0


class TestScorerClose:
    """Tests für Scorer Cleanup."""

    @pytest.mark.asyncio
    async def test_close(self):
        """Test Close schließt Client."""
        scorer = RelevanceScorer()

        # Erstelle Client
        await scorer._get_client()
        assert scorer._client is not None

        # Close
        await scorer.close()
        assert scorer._client is None
