"""
Tests for the RelevanceService class.

Tests cover:
- Relevance calculation with and without preferences
- Trending score calculation
- Filtering by minimum impact
"""

from unittest.mock import Mock


class TestRelevanceService:
    """Tests for the RelevanceService class."""

    def test_calculate_relevance_no_prefs(self):
        """Relevance calculation with no user preferences."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        # Create mock card with impact_score
        card = Mock()
        card.impact_score = 7

        result = service.calculate_relevance(card, None)

        # Should return impact_score / 10
        assert result == 0.7

    def test_calculate_relevance_no_prefs_no_impact(self):
        """Relevance calculation with no preferences and no impact score."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        # Card without impact_score attribute
        card = Mock(spec=[])

        result = service.calculate_relevance(card, None)

        # Should return default 5/10 = 0.5
        assert result == 0.5

    def test_calculate_relevance_category_matching(self):
        """Category preference boosting."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.category = "Technology"
        card.impact_score = 5
        card.topics = []  # Empty topics list

        # Liked category should boost score
        prefs_liked = {
            "liked_categories": ["Technology", "Science"],
            "disliked_categories": [],
            "impact_threshold": 5,
        }
        result_liked = service.calculate_relevance(card, prefs_liked)

        # Disliked category should reduce score
        prefs_disliked = {
            "liked_categories": [],
            "disliked_categories": ["Technology"],
            "impact_threshold": 5,
        }
        result_disliked = service.calculate_relevance(card, prefs_disliked)

        # Neutral category
        prefs_neutral = {
            "liked_categories": ["Sports"],
            "disliked_categories": ["Politics"],
            "impact_threshold": 5,
        }
        result_neutral = service.calculate_relevance(card, prefs_neutral)

        assert result_liked > result_neutral
        assert result_neutral > result_disliked

    def test_calculate_trending_score(self):
        """Trending score calculation based on impact and engagement."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 8
        card.interaction = {
            "views": 100,
            "votes_up": 20,
            "votes_down": 5,
        }

        result = service.calculate_trending_score(card)

        # Expected: 8 + ((100 + 20*2 - 5) / 10) = 8 + 13.5 = 21.5
        expected = 8 + (100 + 20 * 2 - 5) / 10
        assert result == expected

    def test_calculate_trending_score_no_impact(self):
        """Trending score for card without impact_score."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock(spec=[])  # No impact_score attribute

        result = service.calculate_trending_score(card)

        assert result == 0.0

    def test_filter_trending_min_impact(self):
        """Filter by minimum impact score."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        # Create cards with different impact scores
        cards = []
        for impact in [9, 8, 7, 6, 5, 4]:
            card = Mock()
            card.impact_score = impact
            card.interaction = {"views": 10, "votes_up": 1, "votes_down": 0}
            cards.append(card)

        # Filter with min_impact=7
        result = service.filter_trending(cards, min_impact=7, limit=10)

        assert len(result) == 3  # Only scores 9, 8, 7

        # Verify all results have impact >= 7
        for card in result:
            assert card.impact_score >= 7

        # Verify results are sorted by trending score (descending)
        for i in range(len(result) - 1):
            assert result[i].trending_score >= result[i + 1].trending_score

    def test_filter_trending_limit(self):
        """Test limit parameter in filter_trending."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        # Create 10 high-impact cards
        cards = []
        for i in range(10):
            card = Mock()
            card.impact_score = 8
            card.interaction = {"views": i * 10, "votes_up": i, "votes_down": 0}
            cards.append(card)

        result = service.filter_trending(cards, min_impact=7, limit=3)

        assert len(result) == 3

    def test_personalize_feed_with_prefs(self):
        """Test feed personalization with user preferences."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        # Create cards with different categories
        cards = []
        for i, category in enumerate(["Technology", "Sports", "Politics"]):
            card = Mock()
            card.category = category
            card.impact_score = 5 + i
            card.topics = []
            card.interaction = {"viewed": False}
            cards.append(card)

        prefs = {
            "liked_categories": ["Technology"],
            "disliked_categories": ["Politics"],
            "impact_threshold": 5,
        }

        result = service.personalize_feed(cards, prefs)

        assert len(result) == 3
        # Technology card should be first (highest relevance)
        assert result[0].category == "Technology"
        # All cards should have relevance_score set
        for card in result:
            assert hasattr(card, "relevance_score")

    def test_personalize_feed_without_prefs(self):
        """Test feed personalization without user preferences."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        cards = []
        for impact in [8, 5, 9]:
            card = Mock()
            card.impact_score = impact
            card.interaction = {}
            cards.append(card)

        result = service.personalize_feed(cards, None)

        assert len(result) == 3
        # Should be sorted by impact_score / 10
        assert result[0].impact_score == 9  # 0.9 relevance
        assert result[1].impact_score == 8  # 0.8 relevance
        assert result[2].impact_score == 5  # 0.5 relevance

    def test_personalize_feed_exclude_seen(self):
        """Test excluding seen cards from feed."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        cards = []
        for i in range(3):
            card = Mock()
            card.impact_score = 7
            card.interaction = {"viewed": i == 1}  # Middle card is viewed
            cards.append(card)

        # Include seen
        result_with_seen = service.personalize_feed(
            cards, None, include_seen=True
        )
        assert len(result_with_seen) == 3

        # Exclude seen
        result_without_seen = service.personalize_feed(
            cards, None, include_seen=False
        )
        assert len(result_without_seen) == 2

    def test_personalize_feed_empty(self):
        """Test personalization with empty card list."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        result = service.personalize_feed([], None)

        assert len(result) == 0

    def test_calculate_relevance_topic_matching(self):
        """Test relevance boosting based on topic matching."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 5
        card.topics = ["Artificial Intelligence", "Machine Learning"]

        prefs = {
            "liked_topics": ["artificial", "programming"],
            "impact_threshold": 5,
        }

        result = service.calculate_relevance(card, prefs)

        # Should have topic match bonus
        # Base 0.5 + impact match 0.1 + topic match 0.1 = 0.7
        assert result == 0.7

    def test_calculate_relevance_score_clamping(self):
        """Test that relevance score is clamped to [0, 1]."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        # Card with everything liked
        card = Mock()
        card.category = "Technology"
        card.impact_score = 10
        card.topics = ["AI", "Tech"]

        prefs = {
            "liked_categories": ["Technology"],
            "disliked_categories": [],
            "impact_threshold": 3,
            "liked_topics": ["ai", "tech"],
        }

        result = service.calculate_relevance(card, prefs)

        # Should be clamped to 1.0
        assert result <= 1.0
        assert result >= 0.0

    def test_get_relevance_service_singleton(self):
        """Test that get_relevance_service returns singleton."""
        from local_deep_research.news.core.relevance_service import (
            get_relevance_service,
            RelevanceService,
        )

        service1 = get_relevance_service()
        service2 = get_relevance_service()

        assert service1 is service2
        assert isinstance(service1, RelevanceService)

    def test_filter_trending_empty_list(self):
        """Test filtering with empty card list."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        result = service.filter_trending([], min_impact=7)

        assert len(result) == 0

    def test_filter_trending_no_matching_cards(self):
        """Test filtering when no cards meet minimum impact."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        cards = []
        for impact in [3, 4, 5]:
            card = Mock()
            card.impact_score = impact
            card.interaction = {"views": 10, "votes_up": 1, "votes_down": 0}
            cards.append(card)

        result = service.filter_trending(cards, min_impact=7)

        assert len(result) == 0


class TestRelevanceEdgeCases:
    """Additional edge case tests for RelevanceService."""

    def test_calculate_relevance_empty_prefs_dict(self):
        """Test relevance with empty preferences dict (not None)."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 6

        # Empty dict uses internal defaults: impact_threshold=5
        # impact_score=6 >= 5, so +0.1 bonus
        result = service.calculate_relevance(card, {})

        # Base score 0.5 + 0.1 (impact meets threshold) = 0.6
        assert result == 0.6

    def test_calculate_relevance_impact_at_threshold(self):
        """Test relevance when impact exactly equals threshold."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 7
        card.topics = []

        prefs = {
            "impact_threshold": 7,
        }

        result = service.calculate_relevance(card, prefs)

        # Impact equals threshold, should get +0.1
        assert result == 0.6  # 0.5 + 0.1

    def test_calculate_relevance_impact_just_below_threshold(self):
        """Test relevance when impact is just below threshold."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 6
        card.topics = []

        prefs = {
            "impact_threshold": 7,
        }

        result = service.calculate_relevance(card, prefs)

        # Impact below threshold, should get -0.1
        assert result == 0.4  # 0.5 - 0.1

    def test_calculate_relevance_category_both_liked_and_disliked(self):
        """Test when category appears in both liked and disliked (edge case)."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )
        import pytest

        service = RelevanceService()

        card = Mock()
        card.category = "Tech"
        card.impact_score = 5
        card.topics = []

        # Unlikely but possible: same category in both lists
        prefs = {
            "liked_categories": ["Tech"],
            "disliked_categories": ["Tech"],
            "impact_threshold": 5,
        }

        result = service.calculate_relevance(card, prefs)

        # Liked check happens first, so +0.2 should be applied
        # Using pytest.approx for floating point comparison
        assert result == pytest.approx(
            0.8
        )  # 0.5 + 0.2 (liked) + 0.1 (at threshold)

    def test_calculate_relevance_multiple_topic_matches(self):
        """Test that only one topic match bonus is applied."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 5
        card.topics = ["AI Programming", "Machine Learning", "Data Science"]

        prefs = {
            "liked_topics": ["ai", "machine", "data"],  # Multiple matches
            "impact_threshold": 5,
        }

        result = service.calculate_relevance(card, prefs)

        # Should only add 0.1 once (breaks after first match)
        assert result == 0.7  # 0.5 + 0.1 (threshold) + 0.1 (topic)

    def test_calculate_relevance_case_insensitive_topic_match(self):
        """Test that topic matching is case insensitive."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 5
        card.topics = ["ARTIFICIAL INTELLIGENCE"]

        prefs = {
            "liked_topics": ["artificial"],
            "impact_threshold": 5,
        }

        result = service.calculate_relevance(card, prefs)

        # Should match despite case difference
        assert result == 0.7  # 0.5 + 0.1 + 0.1

    def test_calculate_relevance_no_topics_attribute(self):
        """Test relevance when card has no topics attribute."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )
        import pytest

        service = RelevanceService()

        card = Mock(spec=["impact_score", "category"])
        card.impact_score = 5
        card.category = "Tech"

        prefs = {
            "liked_topics": ["tech"],
            "liked_categories": ["Tech"],
            "impact_threshold": 5,
        }

        result = service.calculate_relevance(card, prefs)

        # No topics attribute, so no topic match bonus
        # Using pytest.approx for floating point comparison
        assert result == pytest.approx(
            0.8
        )  # 0.5 + 0.2 (category) + 0.1 (threshold)

    def test_calculate_relevance_no_category_attribute(self):
        """Test relevance when card has no category attribute."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock(spec=["impact_score"])
        card.impact_score = 5

        prefs = {
            "liked_categories": ["Tech"],
            "impact_threshold": 5,
        }

        result = service.calculate_relevance(card, prefs)

        # No category attribute, only threshold bonus
        assert result == 0.6  # 0.5 + 0.1

    def test_calculate_relevance_minimum_clamping(self):
        """Test that relevance is clamped to minimum 0.0."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.category = "Bad"
        card.impact_score = 1
        card.topics = []

        # All negative factors
        prefs = {
            "liked_categories": [],
            "disliked_categories": ["Bad"],
            "impact_threshold": 10,  # Way above actual
        }

        result = service.calculate_relevance(card, prefs)

        # Should be clamped to 0.0 (0.5 - 0.2 - 0.1 = 0.2, but test clamp)
        assert result >= 0.0

    def test_calculate_trending_zero_engagement(self):
        """Test trending score with zero engagement."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 5
        card.interaction = {"views": 0, "votes_up": 0, "votes_down": 0}

        result = service.calculate_trending_score(card)

        # Just the impact score
        assert result == 5.0

    def test_calculate_trending_negative_engagement(self):
        """Test trending score with more downvotes than upvotes."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 5
        card.interaction = {"views": 10, "votes_up": 1, "votes_down": 10}

        result = service.calculate_trending_score(card)

        # 5 + (10 + 2 - 10) / 10 = 5 + 0.2 = 5.2
        expected = 5 + (10 + 2 - 10) / 10
        assert result == expected

    def test_calculate_trending_missing_interaction_keys(self):
        """Test trending score when interaction dict has missing keys."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 5
        card.interaction = {}  # Missing all keys

        result = service.calculate_trending_score(card)

        # Should use .get() defaults of 0
        assert result == 5.0

    def test_filter_trending_all_same_score(self):
        """Test filtering when all cards have the same trending score."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        cards = []
        for i in range(5):
            card = Mock()
            card.impact_score = 8
            card.interaction = {"views": 10, "votes_up": 2, "votes_down": 1}
            cards.append(card)

        result = service.filter_trending(cards, min_impact=7, limit=3)

        assert len(result) == 3
        # All should have same trending score
        scores = [c.trending_score for c in result]
        assert all(s == scores[0] for s in scores)

    def test_filter_trending_exact_limit_match(self):
        """Test filtering when exactly limit cards match."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        cards = []
        for i in range(5):
            card = Mock()
            card.impact_score = 8
            card.interaction = {"views": i * 10, "votes_up": 1, "votes_down": 0}
            cards.append(card)

        result = service.filter_trending(cards, min_impact=7, limit=5)

        assert len(result) == 5

    def test_filter_trending_default_values(self):
        """Test filter_trending with default parameter values."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        cards = []
        for i in range(15):
            card = Mock()
            card.impact_score = 8
            card.interaction = {"views": i, "votes_up": 1, "votes_down": 0}
            cards.append(card)

        # Use defaults: min_impact=7, limit=10
        result = service.filter_trending(cards)

        assert len(result) == 10

    def test_personalize_feed_all_seen(self):
        """Test personalization when all cards have been seen."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        cards = []
        for i in range(3):
            card = Mock()
            card.impact_score = 7
            card.interaction = {"viewed": True}  # All seen
            cards.append(card)

        result = service.personalize_feed(cards, None, include_seen=False)

        assert len(result) == 0

    def test_personalize_feed_partial_attributes(self):
        """Test personalization with cards having partial attributes."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        # Card with only impact_score
        card1 = Mock(spec=["impact_score", "interaction"])
        card1.impact_score = 8
        card1.interaction = {}

        # Card with all attributes
        card2 = Mock()
        card2.impact_score = 6
        card2.category = "Tech"
        card2.topics = ["AI"]
        card2.interaction = {}

        cards = [card1, card2]

        prefs = {
            "liked_categories": ["Tech"],
            "liked_topics": ["ai"],
            "impact_threshold": 5,
        }

        result = service.personalize_feed(cards, prefs)

        assert len(result) == 2
        # Both should have relevance_score set
        for card in result:
            assert hasattr(card, "relevance_score")

    def test_personalize_feed_maintains_card_reference(self):
        """Test that personalization modifies cards in place."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 7
        card.interaction = {}
        card.custom_field = "test_value"

        result = service.personalize_feed([card], None)

        # Same card object should be in result
        assert result[0] is card
        assert result[0].custom_field == "test_value"

    def test_personalize_feed_with_viewed_key_missing(self):
        """Test personalization when 'viewed' key is missing from interaction."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 7
        card.interaction = {"views": 10}  # No 'viewed' key

        result = service.personalize_feed([card], None, include_seen=False)

        # Should be included since .get("viewed") returns None (falsy)
        assert len(result) == 1


class TestRelevanceBoundaryConditions:
    """Tests for boundary conditions in RelevanceService."""

    def test_impact_score_zero(self):
        """Test with impact_score of 0."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 0

        result = service.calculate_relevance(card, None)

        assert result == 0.0

    def test_impact_score_ten(self):
        """Test with maximum impact_score of 10."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 10

        result = service.calculate_relevance(card, None)

        assert result == 1.0

    def test_impact_score_negative(self):
        """Test with negative impact_score (edge case)."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = -5

        result = service.calculate_relevance(card, None)

        # When no prefs, returns impact_score / 10 directly
        # -5/10 = -0.5 (no clamping for no-prefs case)
        assert result == -0.5

    def test_impact_score_above_ten(self):
        """Test with impact_score above 10 (edge case)."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 15

        result = service.calculate_relevance(card, None)

        # When no prefs, returns impact_score / 10 directly
        # 15/10 = 1.5 (no clamping for no-prefs case)
        assert result == 1.5

    def test_filter_trending_zero_min_impact(self):
        """Test filter_trending with min_impact of 0."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        cards = []
        for impact in [0, 1, 2]:
            card = Mock()
            card.impact_score = impact
            card.interaction = {"views": 1, "votes_up": 0, "votes_down": 0}
            cards.append(card)

        result = service.filter_trending(cards, min_impact=0, limit=10)

        # All cards should pass with min_impact=0
        assert len(result) == 3

    def test_filter_trending_zero_limit(self):
        """Test filter_trending with limit of 0."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 10
        card.interaction = {"views": 100, "votes_up": 50, "votes_down": 0}

        result = service.filter_trending([card], min_impact=7, limit=0)

        # Limit of 0 should return empty list
        assert len(result) == 0

    def test_large_engagement_values(self):
        """Test trending score with very large engagement values."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 10
        card.interaction = {
            "views": 1000000,
            "votes_up": 100000,
            "votes_down": 10000,
        }

        result = service.calculate_trending_score(card)

        expected = 10 + (1000000 + 100000 * 2 - 10000) / 10
        assert result == expected

    def test_empty_topics_list(self):
        """Test relevance with empty topics list."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 5
        card.topics = []

        prefs = {
            "liked_topics": ["tech", "ai"],
            "impact_threshold": 5,
        }

        result = service.calculate_relevance(card, prefs)

        # No topics to match
        assert result == 0.6  # 0.5 + 0.1 (threshold only)

    def test_empty_liked_topics(self):
        """Test relevance with empty liked_topics preference."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = Mock()
        card.impact_score = 5
        card.topics = ["AI", "Tech"]

        prefs = {
            "liked_topics": [],
            "impact_threshold": 5,
        }

        result = service.calculate_relevance(card, prefs)

        # No topics to match against
        assert result == 0.6  # 0.5 + 0.1 (threshold only)
