"""
Extended tests for news/core/relevance_service.py

Tests cover:
- RelevanceService.calculate_relevance() - relevance scoring
- RelevanceService.calculate_trending_score() - trending calculation
- RelevanceService.filter_trending() - trending filter logic
- RelevanceService.personalize_feed() - feed personalization
- get_relevance_service() - singleton pattern
- Edge cases and boundary conditions
"""

from unittest.mock import MagicMock


class TestCalculateRelevance:
    """Tests for RelevanceService.calculate_relevance()."""

    def test_returns_float(self):
        """Returns a float value."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.impact_score = 5

        result = service.calculate_relevance(card, None)

        assert isinstance(result, float)

    def test_default_relevance_from_impact_when_no_prefs(self):
        """Uses impact_score for relevance when no preferences."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.impact_score = 8

        result = service.calculate_relevance(card, None)

        assert result == 0.8  # 8/10

    def test_default_relevance_when_empty_prefs(self):
        """Uses impact_score when preferences dict is empty."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.impact_score = 6

        result = service.calculate_relevance(card, {})

        # Empty prefs still counts as having prefs, so base score 0.5
        assert 0.0 <= result <= 1.0

    def test_boosts_liked_category(self):
        """Boosts score for liked categories."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.category = "Technology"
        card.impact_score = 5

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

        result = service.calculate_relevance(card, prefs)

        # Base 0.5 + category boost 0.2 + impact match 0.1 = 0.8
        assert result >= 0.7

    def test_reduces_disliked_category(self):
        """Reduces score for disliked categories."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.category = "Sports"
        card.impact_score = 5

        prefs = {"disliked_categories": ["Sports"], "impact_threshold": 5}

        result = service.calculate_relevance(card, prefs)

        # Base 0.5 - category penalty 0.2 + impact match 0.1 = 0.4
        assert result <= 0.5

    def test_boosts_high_impact(self):
        """Boosts score when impact meets threshold."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.impact_score = 8

        prefs = {"impact_threshold": 7}

        result = service.calculate_relevance(card, prefs)

        # Should include impact boost
        assert result >= 0.5

    def test_reduces_low_impact(self):
        """Reduces score when impact below threshold."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.impact_score = 3

        prefs = {"impact_threshold": 7}

        result = service.calculate_relevance(card, prefs)

        # Base 0.5 - impact penalty 0.1 = 0.4
        assert result <= 0.5

    def test_boosts_matching_topics(self):
        """Boosts score for matching liked topics."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.topics = ["artificial intelligence", "robotics"]
        card.impact_score = 5

        prefs = {"liked_topics": ["artificial"]}

        result = service.calculate_relevance(card, prefs)

        # Should include topic boost
        assert result >= 0.5

    def test_case_insensitive_topic_matching(self):
        """Topic matching is case-insensitive."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.topics = ["Artificial Intelligence"]
        card.impact_score = 5

        prefs = {"liked_topics": ["artificial"]}

        result = service.calculate_relevance(card, prefs)

        assert result >= 0.5

    def test_clamps_to_zero_minimum(self):
        """Score is clamped to minimum 0."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.category = "Disliked1"
        card.impact_score = 1

        prefs = {
            "disliked_categories": ["Disliked1"],
            "impact_threshold": 10,
        }

        result = service.calculate_relevance(card, prefs)

        assert result >= 0.0

    def test_clamps_to_one_maximum(self):
        """Score is clamped to maximum 1."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.category = "Tech"
        card.impact_score = 10
        card.topics = ["ai"]

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

        result = service.calculate_relevance(card, prefs)

        assert result <= 1.0

    def test_handles_card_without_category(self):
        """Handles card without category attribute."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock(spec=["impact_score"])
        card.impact_score = 5

        prefs = {"liked_categories": ["Tech"]}

        # Should not raise
        result = service.calculate_relevance(card, prefs)
        assert isinstance(result, float)

    def test_handles_card_without_impact_score(self):
        """Handles card without impact_score attribute."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock(spec=["category"])
        card.category = "Tech"

        prefs = {}

        # Should not raise, uses default 5
        result = service.calculate_relevance(card, prefs)
        assert isinstance(result, float)

    def test_handles_card_without_topics(self):
        """Handles card without topics attribute."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock(spec=["impact_score"])
        card.impact_score = 5

        prefs = {"liked_topics": ["ai"]}

        # Should not raise
        result = service.calculate_relevance(card, prefs)
        assert isinstance(result, float)


class TestCalculateTrendingScore:
    """Tests for RelevanceService.calculate_trending_score()."""

    def test_returns_float(self):
        """Returns a float value."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock()
        card.impact_score = 5
        card.interaction = {"views": 10, "votes_up": 2, "votes_down": 0}

        result = service.calculate_trending_score(card)

        assert isinstance(result, float)

    def test_returns_zero_without_impact_score(self):
        """Returns 0 when card has no impact_score."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()
        card = MagicMock(spec=[])

        result = service.calculate_trending_score(card)

        assert result == 0.0

    def test_combines_impact_and_engagement(self):
        """Combines impact_score and engagement metrics."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

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

        result = service.calculate_trending_score(card)

        # With no engagement, should be close to impact score
        assert result == 5.0

    def test_views_contribute_to_score(self):
        """Views contribute to trending score."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

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

        card_with_views = MagicMock()
        card_with_views.impact_score = 5
        card_with_views.interaction = {
            "views": 100,
            "votes_up": 0,
            "votes_down": 0,
        }

        score_no_views = service.calculate_trending_score(card_no_views)
        score_with_views = service.calculate_trending_score(card_with_views)

        assert score_with_views > score_no_views

    def test_upvotes_count_double(self):
        """Upvotes count as 2x in engagement."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

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

        result = service.calculate_trending_score(card)

        # engagement = 0 + 5*2 - 0 = 10
        # trending = 5 + (10/10) = 6
        assert result == 6.0

    def test_downvotes_reduce_score(self):
        """Downvotes reduce the trending score."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card_no_downvotes = MagicMock()
        card_no_downvotes.impact_score = 5
        card_no_downvotes.interaction = {
            "views": 10,
            "votes_up": 0,
            "votes_down": 0,
        }

        card_with_downvotes = MagicMock()
        card_with_downvotes.impact_score = 5
        card_with_downvotes.interaction = {
            "views": 10,
            "votes_up": 0,
            "votes_down": 5,
        }

        score_no_down = service.calculate_trending_score(card_no_downvotes)
        score_with_down = service.calculate_trending_score(card_with_downvotes)

        assert score_with_down < score_no_down


class TestFilterTrending:
    """Tests for RelevanceService.filter_trending()."""

    def test_returns_list(self):
        """Returns a list."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        result = service.filter_trending([])

        assert isinstance(result, list)

    def test_filters_by_min_impact(self):
        """Filters out cards below min_impact."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        low_impact = MagicMock()
        low_impact.impact_score = 3
        low_impact.interaction = {"views": 0, "votes_up": 0, "votes_down": 0}

        high_impact = MagicMock()
        high_impact.impact_score = 8
        high_impact.interaction = {"views": 0, "votes_up": 0, "votes_down": 0}

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

        assert len(result) == 1
        assert result[0] is high_impact

    def test_sorts_by_trending_score(self):
        """Sorts results by trending score descending."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card1 = MagicMock()
        card1.impact_score = 7
        card1.interaction = {"views": 100, "votes_up": 10, "votes_down": 0}

        card2 = MagicMock()
        card2.impact_score = 8
        card2.interaction = {"views": 10, "votes_up": 1, "votes_down": 0}

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

        # card1 has more engagement, should be first
        assert result[0].trending_score > result[1].trending_score

    def test_respects_limit(self):
        """Respects the limit parameter."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

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

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

        assert len(result) == 5

    def test_adds_trending_score_attribute(self):
        """Adds trending_score attribute to filtered cards."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

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

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

        assert hasattr(result[0], "trending_score")
        assert result[0].trending_score > 0

    def test_default_min_impact_is_7(self):
        """Default min_impact is 7."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card6 = MagicMock()
        card6.impact_score = 6
        card6.interaction = {}

        card7 = MagicMock()
        card7.impact_score = 7
        card7.interaction = {}

        result = service.filter_trending([card6, card7])

        assert len(result) == 1

    def test_default_limit_is_10(self):
        """Default limit is 10."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        cards = []
        for i in range(20):
            card = MagicMock()
            card.impact_score = 9
            card.interaction = {}
            cards.append(card)

        result = service.filter_trending(cards)

        assert len(result) == 10

    def test_handles_cards_without_impact_score(self):
        """Skips cards without impact_score."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card_with = MagicMock()
        card_with.impact_score = 8
        card_with.interaction = {}

        card_without = MagicMock(spec=[])

        result = service.filter_trending([card_with, card_without])

        assert len(result) == 1


class TestPersonalizeFeed:
    """Tests for RelevanceService.personalize_feed()."""

    def test_returns_list(self):
        """Returns a list."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        result = service.personalize_feed([], None)

        assert isinstance(result, list)

    def test_adds_relevance_score_to_cards(self):
        """Adds relevance_score attribute to cards."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        card = MagicMock()
        card.impact_score = 5
        card.interaction = {"viewed": False}

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

        assert hasattr(result[0], "relevance_score")

    def test_sorts_by_relevance_descending(self):
        """Sorts cards by relevance score descending."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        low_rel = MagicMock()
        low_rel.impact_score = 3
        low_rel.interaction = {"viewed": False}

        high_rel = MagicMock()
        high_rel.impact_score = 9
        high_rel.interaction = {"viewed": False}

        result = service.personalize_feed([low_rel, high_rel], None)

        assert result[0].relevance_score > result[1].relevance_score

    def test_excludes_seen_when_include_seen_false(self):
        """Excludes viewed cards when include_seen is False."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        seen = MagicMock()
        seen.impact_score = 5
        seen.interaction = {"viewed": True}

        unseen = MagicMock()
        unseen.impact_score = 5
        unseen.interaction = {"viewed": False}

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

        assert len(result) == 1
        assert result[0] is unseen

    def test_includes_seen_when_include_seen_true(self):
        """Includes viewed cards when include_seen is True."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        seen = MagicMock()
        seen.impact_score = 5
        seen.interaction = {"viewed": True}

        unseen = MagicMock()
        unseen.impact_score = 5
        unseen.interaction = {"viewed": False}

        result = service.personalize_feed(
            [seen, unseen], None, include_seen=True
        )

        assert len(result) == 2

    def test_default_include_seen_is_true(self):
        """Default include_seen is True."""
        from local_deep_research.news.core.relevance_service import (
            RelevanceService,
        )

        service = RelevanceService()

        seen = MagicMock()
        seen.impact_score = 5
        seen.interaction = {"viewed": True}

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

        assert len(result) == 1


class TestGetRelevanceService:
    """Tests for get_relevance_service() singleton."""

    def test_returns_relevance_service(self):
        """Returns a RelevanceService instance."""
        from local_deep_research.news.core.relevance_service import (
            get_relevance_service,
            RelevanceService,
        )

        result = get_relevance_service()

        assert isinstance(result, RelevanceService)

    def test_returns_same_instance(self):
        """Returns the same instance on multiple calls."""
        from local_deep_research.news.core.relevance_service import (
            get_relevance_service,
        )

        service1 = get_relevance_service()
        service2 = get_relevance_service()

        assert service1 is service2
