"""
Tests für Alerts Agent Repository.

Testet CRUD-Operationen für Topics, Items, Rules und Profiles.
"""
import pytest
from datetime import datetime
from unittest.mock import MagicMock, patch

# Test mit In-Memory SQLite für Isolation
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from alerts_agent.db.models import (
    AlertTopicDB, AlertItemDB, AlertRuleDB, AlertProfileDB,
    AlertSourceEnum, AlertStatusEnum, RelevanceDecisionEnum,
    FeedTypeEnum, RuleActionEnum
)
from alerts_agent.db.repository import (
    TopicRepository, AlertItemRepository, RuleRepository, ProfileRepository
)


# Nutze classroom_engine Base für konsistente Schemas
from classroom_engine.database import Base


@pytest.fixture
def db_session():
    """Erstellt eine In-Memory SQLite-Session für Tests."""
    engine = create_engine("sqlite:///:memory:", echo=False)
    Base.metadata.create_all(engine)
    SessionLocal = sessionmaker(bind=engine)
    session = SessionLocal()
    yield session
    session.close()


# =============================================================================
# TOPIC REPOSITORY TESTS
# =============================================================================

class TestTopicRepository:
    """Tests für TopicRepository."""

    def test_create_topic(self, db_session):
        """Test: Topic erstellen."""
        repo = TopicRepository(db_session)

        topic = repo.create(
            name="Test Topic",
            feed_url="https://example.com/feed",
            feed_type="rss",
            description="Test Description",
        )

        assert topic.id is not None
        assert topic.name == "Test Topic"
        assert topic.feed_url == "https://example.com/feed"
        assert topic.feed_type == FeedTypeEnum.RSS
        assert topic.is_active is True

    def test_get_topic_by_id(self, db_session):
        """Test: Topic nach ID abrufen."""
        repo = TopicRepository(db_session)

        created = repo.create(name="Find Me")
        found = repo.get_by_id(created.id)

        assert found is not None
        assert found.name == "Find Me"

    def test_get_topic_not_found(self, db_session):
        """Test: Topic nicht gefunden."""
        repo = TopicRepository(db_session)

        found = repo.get_by_id("nonexistent-id")
        assert found is None

    def test_update_topic(self, db_session):
        """Test: Topic aktualisieren."""
        repo = TopicRepository(db_session)

        topic = repo.create(name="Original Name")
        updated = repo.update(topic.id, name="Updated Name", is_active=False)

        assert updated.name == "Updated Name"
        assert updated.is_active is False

    def test_delete_topic(self, db_session):
        """Test: Topic löschen."""
        repo = TopicRepository(db_session)

        topic = repo.create(name="To Delete")
        result = repo.delete(topic.id)

        assert result is True
        assert repo.get_by_id(topic.id) is None

    def test_get_all_topics(self, db_session):
        """Test: Alle Topics abrufen."""
        repo = TopicRepository(db_session)

        repo.create(name="Topic 1")
        repo.create(name="Topic 2")
        repo.create(name="Topic 3")

        topics = repo.get_all()
        assert len(topics) == 3

    def test_get_active_topics(self, db_session):
        """Test: Nur aktive Topics abrufen."""
        repo = TopicRepository(db_session)

        repo.create(name="Active 1")
        repo.create(name="Active 2")
        inactive = repo.create(name="Inactive")
        repo.update(inactive.id, is_active=False)

        active_topics = repo.get_all(is_active=True)
        assert len(active_topics) == 2


# =============================================================================
# ALERT ITEM REPOSITORY TESTS
# =============================================================================

class TestAlertItemRepository:
    """Tests für AlertItemRepository."""

    @pytest.fixture
    def topic_id(self, db_session):
        """Erstellt ein Test-Topic und gibt die ID zurück."""
        topic_repo = TopicRepository(db_session)
        topic = topic_repo.create(name="Test Topic")
        return topic.id

    def test_create_alert(self, db_session, topic_id):
        """Test: Alert erstellen."""
        repo = AlertItemRepository(db_session)

        alert = repo.create(
            topic_id=topic_id,
            title="Test Alert",
            url="https://example.com/article",
            snippet="Test snippet content",
        )

        assert alert.id is not None
        assert alert.title == "Test Alert"
        assert alert.url_hash is not None
        assert alert.status == AlertStatusEnum.NEW

    def test_create_if_not_exists_creates(self, db_session, topic_id):
        """Test: Alert erstellen wenn nicht existiert."""
        repo = AlertItemRepository(db_session)

        alert = repo.create_if_not_exists(
            topic_id=topic_id,
            title="New Alert",
            url="https://example.com/new",
        )

        assert alert is not None
        assert alert.title == "New Alert"

    def test_create_if_not_exists_duplicate(self, db_session, topic_id):
        """Test: Duplikat wird nicht erstellt."""
        repo = AlertItemRepository(db_session)

        url = "https://example.com/duplicate"
        first = repo.create_if_not_exists(topic_id=topic_id, title="First", url=url)
        second = repo.create_if_not_exists(topic_id=topic_id, title="Second", url=url)

        assert first is not None
        assert second is None  # Duplikat

    def test_update_scoring(self, db_session, topic_id):
        """Test: Scoring aktualisieren."""
        repo = AlertItemRepository(db_session)

        alert = repo.create(topic_id=topic_id, title="To Score", url="https://example.com/score")

        updated = repo.update_scoring(
            alert_id=alert.id,
            score=0.85,
            decision="KEEP",
            reasons=["relevant"],
            summary="Important article",
            model="test-model",
        )

        assert updated.relevance_score == 0.85
        assert updated.relevance_decision == RelevanceDecisionEnum.KEEP
        assert updated.status == AlertStatusEnum.SCORED

    def test_get_inbox(self, db_session, topic_id):
        """Test: Inbox abrufen."""
        repo = AlertItemRepository(db_session)

        # Erstelle Alerts mit verschiedenen Decisions
        alert1 = repo.create(topic_id=topic_id, title="Keep Alert", url="https://example.com/1")
        repo.update_scoring(alert1.id, 0.9, "KEEP", [], None, None)

        alert2 = repo.create(topic_id=topic_id, title="Drop Alert", url="https://example.com/2")
        repo.update_scoring(alert2.id, 0.1, "DROP", [], None, None)

        alert3 = repo.create(topic_id=topic_id, title="Review Alert", url="https://example.com/3")
        repo.update_scoring(alert3.id, 0.5, "REVIEW", [], None, None)

        # Default-Inbox (KEEP + REVIEW)
        inbox = repo.get_inbox()
        assert len(inbox) == 2  # KEEP und REVIEW

        # Nur KEEP
        keep_only = repo.get_inbox(decision="KEEP")
        assert len(keep_only) == 1

    def test_get_unscored(self, db_session, topic_id):
        """Test: Unbewertete Alerts abrufen."""
        repo = AlertItemRepository(db_session)

        # Erstelle neue Alerts
        repo.create(topic_id=topic_id, title="Unscored 1", url="https://example.com/u1")
        repo.create(topic_id=topic_id, title="Unscored 2", url="https://example.com/u2")

        # Einen bewerten
        alert3 = repo.create(topic_id=topic_id, title="Scored", url="https://example.com/s1")
        repo.update_scoring(alert3.id, 0.5, "REVIEW", [], None, None)

        unscored = repo.get_unscored()
        assert len(unscored) == 2

    def test_mark_reviewed(self, db_session, topic_id):
        """Test: Alert als reviewed markieren."""
        repo = AlertItemRepository(db_session)

        alert = repo.create(topic_id=topic_id, title="To Review", url="https://example.com/review")

        reviewed = repo.mark_reviewed(
            alert_id=alert.id,
            is_relevant=True,
            notes="Good article",
            tags=["important"],
        )

        assert reviewed.status == AlertStatusEnum.REVIEWED
        assert reviewed.user_marked_relevant is True
        assert reviewed.user_notes == "Good article"
        assert "important" in reviewed.user_tags


# =============================================================================
# RULE REPOSITORY TESTS
# =============================================================================

class TestRuleRepository:
    """Tests für RuleRepository."""

    def test_create_rule(self, db_session):
        """Test: Regel erstellen."""
        repo = RuleRepository(db_session)

        rule = repo.create(
            name="Test Rule",
            conditions=[{"field": "title", "op": "contains", "value": "test"}],
            action_type="keep",
            priority=10,
        )

        assert rule.id is not None
        assert rule.name == "Test Rule"
        assert rule.priority == 10
        assert rule.is_active is True

    def test_get_active_rules_ordered(self, db_session):
        """Test: Aktive Regeln nach Priorität sortiert."""
        repo = RuleRepository(db_session)

        repo.create(name="Low Priority", conditions=[], priority=1)
        repo.create(name="High Priority", conditions=[], priority=100)
        repo.create(name="Medium Priority", conditions=[], priority=50)

        rules = repo.get_active()
        assert len(rules) == 3
        assert rules[0].name == "High Priority"
        assert rules[1].name == "Medium Priority"
        assert rules[2].name == "Low Priority"

    def test_update_rule(self, db_session):
        """Test: Regel aktualisieren."""
        repo = RuleRepository(db_session)

        rule = repo.create(name="Original", conditions=[], action_type="keep")
        updated = repo.update(rule.id, name="Updated", action_type="drop")

        assert updated.name == "Updated"
        assert updated.action_type == RuleActionEnum.DROP

    def test_increment_match_count(self, db_session):
        """Test: Match-Counter erhöhen."""
        repo = RuleRepository(db_session)

        rule = repo.create(name="Matcher", conditions=[])
        assert rule.match_count == 0

        repo.increment_match_count(rule.id)
        repo.increment_match_count(rule.id)

        updated = repo.get_by_id(rule.id)
        assert updated.match_count == 2
        assert updated.last_matched_at is not None


# =============================================================================
# PROFILE REPOSITORY TESTS
# =============================================================================

class TestProfileRepository:
    """Tests für ProfileRepository."""

    def test_create_default_profile(self, db_session):
        """Test: Default-Profil erstellen."""
        repo = ProfileRepository(db_session)

        profile = repo.create_default_education_profile()

        assert profile.id is not None
        assert len(profile.priorities) > 0
        assert "Inklusion" in [p["label"] for p in profile.priorities]

    def test_get_or_create(self, db_session):
        """Test: Get-or-Create Pattern."""
        repo = ProfileRepository(db_session)

        # Erstes Mal erstellt
        profile1 = repo.get_or_create(user_id="user-123")
        assert profile1 is not None

        # Zweites Mal holt existierendes
        profile2 = repo.get_or_create(user_id="user-123")
        assert profile2.id == profile1.id

    def test_update_priorities(self, db_session):
        """Test: Prioritäten aktualisieren."""
        repo = ProfileRepository(db_session)

        profile = repo.get_or_create()
        new_priorities = [
            {"label": "New Priority", "weight": 0.9, "keywords": ["test"]}
        ]

        updated = repo.update_priorities(profile.id, new_priorities)
        assert len(updated.priorities) == 1
        assert updated.priorities[0]["label"] == "New Priority"

    def test_add_feedback_positive(self, db_session):
        """Test: Positives Feedback hinzufügen."""
        repo = ProfileRepository(db_session)

        profile = repo.get_or_create()
        initial_kept = profile.total_kept

        repo.add_feedback(
            profile_id=profile.id,
            title="Relevant Article",
            url="https://example.com/relevant",
            is_relevant=True,
            reason="Very informative",
        )

        updated = repo.get_by_id(profile.id)
        assert updated.total_kept == initial_kept + 1
        assert len(updated.positive_examples) == 1

    def test_add_feedback_negative(self, db_session):
        """Test: Negatives Feedback hinzufügen."""
        repo = ProfileRepository(db_session)

        profile = repo.get_or_create()
        initial_dropped = profile.total_dropped

        repo.add_feedback(
            profile_id=profile.id,
            title="Irrelevant Article",
            url="https://example.com/irrelevant",
            is_relevant=False,
            reason="Off-topic",
        )

        updated = repo.get_by_id(profile.id)
        assert updated.total_dropped == initial_dropped + 1
        assert len(updated.negative_examples) == 1

    def test_feedback_limits_examples(self, db_session):
        """Test: Beispiele werden auf 20 begrenzt."""
        repo = ProfileRepository(db_session)

        profile = repo.get_or_create()

        # Füge 25 positive Beispiele hinzu
        for i in range(25):
            repo.add_feedback(
                profile_id=profile.id,
                title=f"Article {i}",
                url=f"https://example.com/{i}",
                is_relevant=True,
            )

        updated = repo.get_by_id(profile.id)
        assert len(updated.positive_examples) == 20  # Max 20


# =============================================================================
# INTEGRATION TESTS
# =============================================================================

class TestRepositoryIntegration:
    """Integration Tests für Repository-Zusammenspiel."""

    def test_topic_with_alerts_cascade_delete(self, db_session):
        """Test: Topic-Löschung löscht auch zugehörige Alerts."""
        topic_repo = TopicRepository(db_session)
        alert_repo = AlertItemRepository(db_session)

        # Erstelle Topic mit Alerts
        topic = topic_repo.create(name="To Delete")
        alert_repo.create(topic_id=topic.id, title="Alert 1", url="https://example.com/1")
        alert_repo.create(topic_id=topic.id, title="Alert 2", url="https://example.com/2")

        # Prüfe dass Alerts existieren
        alerts = alert_repo.get_by_topic(topic.id)
        assert len(alerts) == 2

        # Lösche Topic
        topic_repo.delete(topic.id)

        # Alerts sollten auch gelöscht sein (CASCADE)
        alerts_after = alert_repo.get_by_topic(topic.id)
        assert len(alerts_after) == 0

    def test_scoring_workflow(self, db_session):
        """Test: Kompletter Scoring-Workflow."""
        topic_repo = TopicRepository(db_session)
        alert_repo = AlertItemRepository(db_session)
        profile_repo = ProfileRepository(db_session)

        # Setup
        topic = topic_repo.create(name="Workflow Test")
        profile = profile_repo.create_default_education_profile()

        # Alerts erstellen
        alert1 = alert_repo.create(topic_id=topic.id, title="Inklusion im Unterricht", url="https://example.com/a1")
        alert2 = alert_repo.create(topic_id=topic.id, title="Stellenanzeige Lehrer", url="https://example.com/a2")
        alert3 = alert_repo.create(topic_id=topic.id, title="Neutral News", url="https://example.com/a3")

        # Scoring simulieren
        alert_repo.update_scoring(alert1.id, 0.85, "KEEP", ["priority_match"], "Relevant", "test")
        alert_repo.update_scoring(alert2.id, 0.1, "DROP", ["exclusion_match"], None, "test")
        alert_repo.update_scoring(alert3.id, 0.5, "REVIEW", [], None, "test")

        # Stats prüfen
        by_decision = alert_repo.count_by_decision(topic.id)
        assert by_decision.get("KEEP", 0) == 1
        assert by_decision.get("DROP", 0) == 1
        assert by_decision.get("REVIEW", 0) == 1
