"""
Deep behavioral tests for RelevanceService.
Tests relevance scoring, trending calculation, feed personalization, and filtering.
"""

from unittest.mock import Mock

from local_deep_research.news.core.relevance_service import (
    RelevanceService,
    get_relevance_service,
)


def _make_card(
    impact_score=5,
    category="General",
    topics=None,
    interaction=None,
    viewed=False,
):
    """Create a mock card with realistic attributes."""
    card = Mock()
    card.impact_score = impact_score
    card.category = category
    card.topics = topics or []
    card.interaction = interaction or {
        "views": 0,
        "votes_up": 0,
        "votes_down": 0,
    }
    if viewed:
        card.interaction["viewed"] = True
    return card


# --- calculate_relevance ---


class TestCalculateRelevance:
    """Tests for relevance scoring logic."""

    def test_no_prefs_uses_impact_score(self):
        svc = RelevanceService()
        card = _make_card(impact_score=8)
        score = svc.calculate_relevance(card, None)
        assert score == 0.8

    def test_no_prefs_impact_10_is_max(self):
        svc = RelevanceService()
        card = _make_card(impact_score=10)
        assert svc.calculate_relevance(card, None) == 1.0

    def test_no_prefs_impact_0_is_min(self):
        svc = RelevanceService()
        card = _make_card(impact_score=0)
        assert svc.calculate_relevance(card, None) == 0.0

    def test_empty_prefs_uses_impact_score(self):
        svc = RelevanceService()
        card = _make_card(impact_score=6)
        score = svc.calculate_relevance(card, {})
        # With empty prefs, base score is 0.5
        # impact_score 6 >= default threshold 5 → +0.1
        assert score == 0.6

    def test_liked_category_boosts_score(self):
        svc = RelevanceService()
        card = _make_card(category="Tech")
        prefs = {"liked_categories": ["Tech"]}
        score = svc.calculate_relevance(card, prefs)
        # base 0.5 + 0.2 (liked) + 0.1 (impact 5 >= threshold 5) = 0.8
        assert abs(score - 0.8) < 1e-9

    def test_disliked_category_reduces_score(self):
        svc = RelevanceService()
        card = _make_card(category="Sports")
        prefs = {"disliked_categories": ["Sports"]}
        score = svc.calculate_relevance(card, prefs)
        # base 0.5 - 0.2 (disliked) + 0.1 (impact 5 >= 5) = 0.4
        assert score == 0.4

    def test_impact_above_threshold_boosts(self):
        svc = RelevanceService()
        card = _make_card(impact_score=8)
        prefs = {"impact_threshold": 7}
        score = svc.calculate_relevance(card, prefs)
        # base 0.5 + 0.1 (above threshold)
        assert score == 0.6

    def test_impact_below_threshold_reduces(self):
        svc = RelevanceService()
        card = _make_card(impact_score=3)
        prefs = {"impact_threshold": 7}
        score = svc.calculate_relevance(card, prefs)
        # base 0.5 - 0.1 (below threshold)
        assert score == 0.4

    def test_impact_at_threshold_boosts(self):
        svc = RelevanceService()
        card = _make_card(impact_score=5)
        prefs = {"impact_threshold": 5}
        score = svc.calculate_relevance(card, prefs)
        # base 0.5 + 0.1 (at threshold counts as >=)
        assert score == 0.6

    def test_liked_topic_boosts_score(self):
        svc = RelevanceService()
        card = _make_card(topics=["Artificial Intelligence", "Technology"])
        prefs = {"liked_topics": ["artificial"]}
        score = svc.calculate_relevance(card, prefs)
        # base 0.5 + 0.1 (topic match) + 0.1 (impact default) = 0.7
        assert score == 0.7

    def test_topic_match_is_case_insensitive(self):
        svc = RelevanceService()
        card = _make_card(topics=["AI Research"])
        prefs = {"liked_topics": ["ai"]}
        score_with_topic = svc.calculate_relevance(card, prefs)
        card_no_topic = _make_card(topics=["Cooking"])
        score_no_topic = svc.calculate_relevance(card_no_topic, prefs)
        assert score_with_topic > score_no_topic

    def test_only_first_topic_match_counts(self):
        """Only one +0.1 boost even if multiple topics match."""
        svc = RelevanceService()
        card = _make_card(topics=["AI", "ML"])
        prefs = {"liked_topics": ["ai", "ml"]}
        score = svc.calculate_relevance(card, prefs)
        # base 0.5 + 0.1 (one topic match, breaks after first) + 0.1 (impact)
        assert score == 0.7

    def test_combined_boosts(self):
        svc = RelevanceService()
        card = _make_card(
            impact_score=9, category="Tech", topics=["AI Research"]
        )
        prefs = {
            "liked_categories": ["Tech"],
            "impact_threshold": 5,
            "liked_topics": ["ai"],
        }
        score = svc.calculate_relevance(card, prefs)
        # 0.5 + 0.2 (liked cat) + 0.1 (above threshold) + 0.1 (topic) = 0.9
        assert abs(score - 0.9) < 1e-9

    def test_combined_penalties(self):
        svc = RelevanceService()
        card = _make_card(impact_score=2, category="Sports")
        prefs = {
            "disliked_categories": ["Sports"],
            "impact_threshold": 5,
        }
        score = svc.calculate_relevance(card, prefs)
        # 0.5 - 0.2 (disliked) - 0.1 (below threshold) = 0.2
        assert abs(score - 0.2) < 1e-9

    def test_score_clamped_to_max_1(self):
        svc = RelevanceService()
        card = _make_card(impact_score=10, category="Tech", topics=["AI"])
        prefs = {
            "liked_categories": ["Tech"],
            "impact_threshold": 1,
            "liked_topics": ["ai"],
        }
        score = svc.calculate_relevance(card, prefs)
        # Would be 0.5 + 0.2 + 0.1 + 0.1 = 0.9, but let's verify clamping
        assert score <= 1.0

    def test_score_clamped_to_min_0(self):
        svc = RelevanceService()
        card = _make_card(impact_score=1, category="Sports")
        prefs = {
            "disliked_categories": ["Sports"],
            "impact_threshold": 10,
        }
        score = svc.calculate_relevance(card, prefs)
        # 0.5 - 0.2 - 0.1 = 0.2, still valid
        assert score >= 0.0

    def test_card_without_category_attr(self):
        svc = RelevanceService()
        card = Mock(spec=[])  # No attributes
        card.impact_score = 5
        score = svc.calculate_relevance(card, {"liked_categories": ["Tech"]})
        # No category attr → skips category check
        assert isinstance(score, float)

    def test_card_without_impact_attr(self):
        svc = RelevanceService()
        card = Mock(spec=[])  # No attributes
        card.category = "Tech"
        # No impact_score → default 5 / 10.0 when no prefs
        score = svc.calculate_relevance(card, None)
        assert score == 0.5

    def test_card_without_topics_attr(self):
        svc = RelevanceService()
        card = Mock(spec=[])
        card.impact_score = 5
        card.category = "Tech"
        score = svc.calculate_relevance(card, {"liked_topics": ["ai"]})
        # No topics attr → skips topic check
        assert isinstance(score, float)


# --- calculate_trending_score ---


class TestCalculateTrendingScore:
    """Tests for trending score calculation."""

    def test_no_impact_score_returns_zero(self):
        svc = RelevanceService()
        card = Mock(spec=[])  # No impact_score attr
        assert svc.calculate_trending_score(card) == 0.0

    def test_impact_only_no_engagement(self):
        svc = RelevanceService()
        card = _make_card(impact_score=7)
        score = svc.calculate_trending_score(card)
        # 7 + (0 + 0 - 0) / 10 = 7.0
        assert score == 7.0

    def test_views_contribute(self):
        svc = RelevanceService()
        card = _make_card(
            impact_score=5,
            interaction={"views": 100, "votes_up": 0, "votes_down": 0},
        )
        score = svc.calculate_trending_score(card)
        # 5 + 100/10 = 15.0
        assert score == 15.0

    def test_votes_up_count_double(self):
        svc = RelevanceService()
        card = _make_card(
            impact_score=5,
            interaction={"views": 0, "votes_up": 10, "votes_down": 0},
        )
        score = svc.calculate_trending_score(card)
        # 5 + (0 + 10*2 - 0) / 10 = 5 + 2.0 = 7.0
        assert score == 7.0

    def test_votes_down_subtract(self):
        svc = RelevanceService()
        card = _make_card(
            impact_score=5,
            interaction={"views": 0, "votes_up": 0, "votes_down": 10},
        )
        score = svc.calculate_trending_score(card)
        # 5 + (0 + 0 - 10) / 10 = 5 - 1.0 = 4.0
        assert score == 4.0

    def test_combined_engagement(self):
        svc = RelevanceService()
        card = _make_card(
            impact_score=8,
            interaction={"views": 50, "votes_up": 20, "votes_down": 5},
        )
        score = svc.calculate_trending_score(card)
        # engagement = 50 + 20*2 - 5 = 85
        # trending = 8 + 85/10 = 16.5
        assert score == 16.5

    def test_negative_engagement_possible(self):
        svc = RelevanceService()
        card = _make_card(
            impact_score=3,
            interaction={"views": 0, "votes_up": 0, "votes_down": 50},
        )
        score = svc.calculate_trending_score(card)
        # 3 + (0 - 50)/10 = 3 - 5 = -2.0
        assert score == -2.0


# --- filter_trending ---


class TestFilterTrending:
    """Tests for trending filter logic."""

    def test_empty_list(self):
        svc = RelevanceService()
        assert svc.filter_trending([]) == []

    def test_filters_below_min_impact(self):
        svc = RelevanceService()
        low = _make_card(impact_score=5)
        high = _make_card(impact_score=8)
        result = svc.filter_trending([low, high], min_impact=7)
        assert len(result) == 1
        assert result[0] is high

    def test_respects_limit(self):
        svc = RelevanceService()
        cards = [_make_card(impact_score=8) for _ in range(20)]
        result = svc.filter_trending(cards, min_impact=7, limit=5)
        assert len(result) == 5

    def test_sorts_by_trending_score(self):
        svc = RelevanceService()
        low_engagement = _make_card(
            impact_score=8,
            interaction={"views": 0, "votes_up": 0, "votes_down": 0},
        )
        high_engagement = _make_card(
            impact_score=8,
            interaction={"views": 100, "votes_up": 50, "votes_down": 0},
        )
        result = svc.filter_trending(
            [low_engagement, high_engagement], min_impact=7
        )
        assert result[0] is high_engagement
        assert result[1] is low_engagement

    def test_sets_trending_score_on_cards(self):
        svc = RelevanceService()
        card = _make_card(impact_score=9)
        svc.filter_trending([card], min_impact=7)
        assert hasattr(card, "trending_score")

    def test_exact_min_impact_included(self):
        svc = RelevanceService()
        card = _make_card(impact_score=7)
        result = svc.filter_trending([card], min_impact=7)
        assert len(result) == 1

    def test_card_without_impact_score_excluded(self):
        svc = RelevanceService()
        card = Mock(spec=[])  # No impact_score
        result = svc.filter_trending([card], min_impact=7)
        assert len(result) == 0

    def test_default_min_impact_is_7(self):
        svc = RelevanceService()
        card_6 = _make_card(impact_score=6)
        card_7 = _make_card(impact_score=7)
        result = svc.filter_trending([card_6, card_7])
        assert card_7 in result
        assert card_6 not in result

    def test_default_limit_is_10(self):
        svc = RelevanceService()
        cards = [_make_card(impact_score=9) for _ in range(15)]
        result = svc.filter_trending(cards)
        assert len(result) == 10


# --- personalize_feed ---


class TestPersonalizeFeed:
    """Tests for feed personalization."""

    def test_empty_cards(self):
        svc = RelevanceService()
        result = svc.personalize_feed([], None)
        assert result == []

    def test_no_prefs_scores_by_impact(self):
        svc = RelevanceService()
        low = _make_card(impact_score=3)
        high = _make_card(impact_score=9)
        result = svc.personalize_feed([low, high], None)
        assert result[0] is high
        assert result[1] is low

    def test_sets_relevance_score_on_cards(self):
        svc = RelevanceService()
        card = _make_card(impact_score=5)
        svc.personalize_feed([card], None)
        assert hasattr(card, "relevance_score")
        assert card.relevance_score == 0.5

    def test_excludes_seen_when_requested(self):
        svc = RelevanceService()
        seen = _make_card(viewed=True)
        unseen = _make_card()
        result = svc.personalize_feed([seen, unseen], None, include_seen=False)
        assert len(result) == 1
        assert result[0] is unseen

    def test_includes_seen_by_default(self):
        svc = RelevanceService()
        seen = _make_card(viewed=True)
        unseen = _make_card()
        result = svc.personalize_feed([seen, unseen], None)
        assert len(result) == 2

    def test_sorts_by_relevance_with_prefs(self):
        svc = RelevanceService()
        liked = _make_card(category="Tech", impact_score=5)
        disliked = _make_card(category="Sports", impact_score=5)
        prefs = {
            "liked_categories": ["Tech"],
            "disliked_categories": ["Sports"],
        }
        result = svc.personalize_feed([disliked, liked], prefs)
        assert result[0] is liked

    def test_all_seen_excluded_returns_empty(self):
        svc = RelevanceService()
        cards = [_make_card(viewed=True) for _ in range(3)]
        result = svc.personalize_feed(cards, None, include_seen=False)
        assert result == []


# --- Singleton ---


class TestGetRelevanceService:
    """Tests for singleton getter."""

    def test_returns_instance(self):
        svc = get_relevance_service()
        assert isinstance(svc, RelevanceService)

    def test_returns_same_instance(self):
        svc1 = get_relevance_service()
        svc2 = get_relevance_service()
        assert svc1 is svc2
