"""
Deep behavioral tests for relevance_service.py.
Tests calculate_relevance scoring, calculate_trending_score,
filter_trending, personalize_feed, and edge cases.
"""

from unittest.mock import Mock


def _make_card(**kwargs):
    """Create a mock card with configurable attributes."""
    card = Mock()
    card.impact_score = kwargs.get("impact_score", 5)
    card.category = kwargs.get("category", "General")
    card.topics = kwargs.get("topics", [])
    card.interaction = kwargs.get("interaction", {})
    card.relevance_score = 0
    card.trending_score = 0
    return card


# --- calculate_relevance ---


class TestCalculateRelevanceNoPrefs:
    """Tests for calculate_relevance without user preferences."""

    def _calc(self, card, prefs):
        if not prefs:
            return getattr(card, "impact_score", 5) / 10.0
        score = 0.5
        if hasattr(card, "category"):
            if card.category in prefs.get("liked_categories", []):
                score += 0.2
            elif card.category in prefs.get("disliked_categories", []):
                score -= 0.2
        if hasattr(card, "impact_score"):
            threshold = prefs.get("impact_threshold", 5)
            if card.impact_score >= threshold:
                score += 0.1
            else:
                score -= 0.1
        if hasattr(card, "topics"):
            liked_topics = prefs.get("liked_topics", [])
            for topic in card.topics:
                if any(liked in topic.lower() for liked in liked_topics):
                    score += 0.1
                    break
        return max(0.0, min(1.0, score))

    def test_no_prefs_uses_impact(self):
        card = _make_card(impact_score=8)
        assert self._calc(card, None) == 0.8

    def test_no_prefs_low_impact(self):
        card = _make_card(impact_score=2)
        assert self._calc(card, None) == 0.2

    def test_no_prefs_zero_impact(self):
        card = _make_card(impact_score=0)
        assert self._calc(card, None) == 0.0

    def test_no_prefs_max_impact(self):
        card = _make_card(impact_score=10)
        assert self._calc(card, None) == 1.0

    def test_empty_prefs_uses_impact(self):
        card = _make_card(impact_score=5)
        assert self._calc(card, None) == 0.5


class TestCalculateRelevanceLikedCategory:
    """Tests for liked/disliked category effects."""

    def _calc(self, card, prefs):
        if not prefs:
            return getattr(card, "impact_score", 5) / 10.0
        score = 0.5
        if hasattr(card, "category"):
            if card.category in prefs.get("liked_categories", []):
                score += 0.2
            elif card.category in prefs.get("disliked_categories", []):
                score -= 0.2
        if hasattr(card, "impact_score"):
            threshold = prefs.get("impact_threshold", 5)
            if card.impact_score >= threshold:
                score += 0.1
            else:
                score -= 0.1
        if hasattr(card, "topics"):
            liked_topics = prefs.get("liked_topics", [])
            for topic in card.topics:
                if any(liked in topic.lower() for liked in liked_topics):
                    score += 0.1
                    break
        return max(0.0, min(1.0, score))

    def test_liked_category_boosts(self):
        card = _make_card(category="Tech", impact_score=5)
        prefs = {"liked_categories": ["Tech"]}
        score = self._calc(card, prefs)
        assert score > 0.5

    def test_disliked_category_penalizes(self):
        card = _make_card(category="Sports", impact_score=5)
        prefs = {"disliked_categories": ["Sports"]}
        score = self._calc(card, prefs)
        assert score < 0.5

    def test_neutral_category(self):
        card = _make_card(category="Science", impact_score=5)
        prefs = {"liked_categories": ["Tech"]}
        score = self._calc(card, prefs)
        # base 0.5 + 0.1 (impact >= threshold 5) = 0.6
        assert score == 0.6

    def test_liked_and_high_impact(self):
        card = _make_card(category="Tech", impact_score=9)
        prefs = {"liked_categories": ["Tech"]}
        score = self._calc(card, prefs)
        # 0.5 + 0.2 (liked) + 0.1 (high impact) = 0.8
        assert abs(score - 0.8) < 0.01

    def test_disliked_and_low_impact(self):
        card = _make_card(category="Sports", impact_score=3)
        prefs = {"disliked_categories": ["Sports"], "impact_threshold": 5}
        score = self._calc(card, prefs)
        # 0.5 - 0.2 (disliked) - 0.1 (low impact) = 0.2
        assert abs(score - 0.2) < 0.01


class TestCalculateRelevanceImpactThreshold:
    """Tests for impact threshold effects."""

    def _calc(self, card, prefs):
        if not prefs:
            return getattr(card, "impact_score", 5) / 10.0
        score = 0.5
        if hasattr(card, "category"):
            if card.category in prefs.get("liked_categories", []):
                score += 0.2
            elif card.category in prefs.get("disliked_categories", []):
                score -= 0.2
        if hasattr(card, "impact_score"):
            threshold = prefs.get("impact_threshold", 5)
            if card.impact_score >= threshold:
                score += 0.1
            else:
                score -= 0.1
        if hasattr(card, "topics"):
            liked_topics = prefs.get("liked_topics", [])
            for topic in card.topics:
                if any(liked in topic.lower() for liked in liked_topics):
                    score += 0.1
                    break
        return max(0.0, min(1.0, score))

    def test_above_threshold(self):
        card = _make_card(impact_score=8)
        prefs = {"impact_threshold": 7}
        score = self._calc(card, prefs)
        assert score == 0.6  # 0.5 + 0.1

    def test_below_threshold(self):
        card = _make_card(impact_score=3)
        prefs = {"impact_threshold": 7}
        score = self._calc(card, prefs)
        assert score == 0.4  # 0.5 - 0.1

    def test_exactly_at_threshold(self):
        card = _make_card(impact_score=5)
        prefs = {"impact_threshold": 5}
        score = self._calc(card, prefs)
        assert score == 0.6  # >= threshold

    def test_default_threshold_5(self):
        card = _make_card(impact_score=5)
        prefs = {}
        score = self._calc(card, prefs)
        # Empty dict {} is falsy in Python, so `not prefs` is True
        # This means it takes the "no prefs" path: impact_score / 10
        assert score == 0.5  # 5 / 10 = 0.5


class TestCalculateRelevanceTopicMatching:
    """Tests for topic matching effects."""

    def _calc(self, card, prefs):
        if not prefs:
            return getattr(card, "impact_score", 5) / 10.0
        score = 0.5
        if hasattr(card, "category"):
            if card.category in prefs.get("liked_categories", []):
                score += 0.2
            elif card.category in prefs.get("disliked_categories", []):
                score -= 0.2
        if hasattr(card, "impact_score"):
            threshold = prefs.get("impact_threshold", 5)
            if card.impact_score >= threshold:
                score += 0.1
            else:
                score -= 0.1
        if hasattr(card, "topics"):
            liked_topics = prefs.get("liked_topics", [])
            for topic in card.topics:
                if any(liked in topic.lower() for liked in liked_topics):
                    score += 0.1
                    break
        return max(0.0, min(1.0, score))

    def test_matching_topic_boosts(self):
        card = _make_card(topics=["artificial intelligence"], impact_score=5)
        prefs = {"liked_topics": ["artificial"]}
        score = self._calc(card, prefs)
        # 0.5 + 0.1 (impact) + 0.1 (topic) = 0.7
        assert score == 0.7

    def test_no_matching_topic(self):
        card = _make_card(topics=["climate change"], impact_score=5)
        prefs = {"liked_topics": ["sports"]}
        score = self._calc(card, prefs)
        assert score == 0.6  # only impact boost

    def test_empty_topics(self):
        card = _make_card(topics=[], impact_score=5)
        prefs = {"liked_topics": ["ai"]}
        score = self._calc(card, prefs)
        assert score == 0.6

    def test_topic_matching_is_case_insensitive(self):
        card = _make_card(topics=["AI Research"], impact_score=5)
        prefs = {"liked_topics": ["ai"]}
        score = self._calc(card, prefs)
        assert score == 0.7

    def test_only_one_boost_per_card(self):
        card = _make_card(
            topics=["AI News", "AI Research", "AI Updates"], impact_score=5
        )
        prefs = {"liked_topics": ["ai"]}
        score = self._calc(card, prefs)
        assert score == 0.7  # break after first match


class TestCalculateRelevanceClamping:
    """Tests for score clamping to [0, 1]."""

    def _calc(self, card, prefs):
        if not prefs:
            return getattr(card, "impact_score", 5) / 10.0
        score = 0.5
        if hasattr(card, "category"):
            if card.category in prefs.get("liked_categories", []):
                score += 0.2
            elif card.category in prefs.get("disliked_categories", []):
                score -= 0.2
        if hasattr(card, "impact_score"):
            threshold = prefs.get("impact_threshold", 5)
            if card.impact_score >= threshold:
                score += 0.1
            else:
                score -= 0.1
        if hasattr(card, "topics"):
            liked_topics = prefs.get("liked_topics", [])
            for topic in card.topics:
                if any(liked in topic.lower() for liked in liked_topics):
                    score += 0.1
                    break
        return max(0.0, min(1.0, score))

    def test_max_possible_score(self):
        card = _make_card(category="Tech", impact_score=10, topics=["AI"])
        prefs = {
            "liked_categories": ["Tech"],
            "liked_topics": ["ai"],
        }
        score = self._calc(card, prefs)
        assert score <= 1.0

    def test_min_possible_score(self):
        card = _make_card(category="Sports", impact_score=1, topics=[])
        prefs = {
            "disliked_categories": ["Sports"],
            "impact_threshold": 10,
        }
        score = self._calc(card, prefs)
        assert score >= 0.0


# --- calculate_trending_score ---


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

    def _trending(self, card):
        if not hasattr(card, "impact_score"):
            return 0.0
        engagement = (
            card.interaction.get("views", 0)
            + card.interaction.get("votes_up", 0) * 2
            - card.interaction.get("votes_down", 0)
        )
        return card.impact_score + (engagement / 10)

    def test_no_engagement(self):
        card = _make_card(impact_score=7, interaction={})
        assert self._trending(card) == 7.0

    def test_views_contribute(self):
        card = _make_card(impact_score=5, interaction={"views": 100})
        score = self._trending(card)
        assert score == 15.0  # 5 + 100/10

    def test_votes_up_weighted_double(self):
        card = _make_card(impact_score=5, interaction={"votes_up": 10})
        score = self._trending(card)
        assert score == 7.0  # 5 + (10*2)/10

    def test_votes_down_subtract(self):
        card = _make_card(impact_score=5, interaction={"votes_down": 10})
        score = self._trending(card)
        assert score == 4.0  # 5 + (-10)/10

    def test_combined_engagement(self):
        card = _make_card(
            impact_score=5,
            interaction={"views": 50, "votes_up": 10, "votes_down": 5},
        )
        score = self._trending(card)
        # 5 + (50 + 20 - 5) / 10 = 5 + 6.5 = 11.5
        assert score == 11.5

    def test_no_impact_score_attr(self):
        card = Mock(spec=[])
        assert self._trending(card) == 0.0

    def test_zero_impact(self):
        card = _make_card(impact_score=0, interaction={"views": 10})
        score = self._trending(card)
        assert score == 1.0


# --- filter_trending ---


class TestFilterTrending:
    """Tests for trending card filtering."""

    def _filter(self, cards, min_impact=7, limit=10):
        trending = []
        for card in cards:
            if (
                hasattr(card, "impact_score")
                and card.impact_score >= min_impact
            ):
                engagement = (
                    card.interaction.get("views", 0)
                    + card.interaction.get("votes_up", 0) * 2
                    - card.interaction.get("votes_down", 0)
                )
                card.trending_score = card.impact_score + (engagement / 10)
                trending.append(card)
        trending.sort(key=lambda c: c.trending_score, reverse=True)
        return trending[:limit]

    def test_empty_cards(self):
        assert self._filter([]) == []

    def test_filters_below_min_impact(self):
        cards = [_make_card(impact_score=5), _make_card(impact_score=8)]
        result = self._filter(cards, min_impact=7)
        assert len(result) == 1
        assert result[0].impact_score == 8

    def test_sorts_by_trending_score(self):
        c1 = _make_card(impact_score=8, interaction={"views": 100})
        c2 = _make_card(impact_score=9, interaction={"views": 10})
        result = self._filter([c1, c2])
        assert result[0] is c1  # 8 + 10 = 18 > 9 + 1 = 10

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

    def test_min_impact_boundary(self):
        cards = [
            _make_card(impact_score=6),
            _make_card(impact_score=7),
            _make_card(impact_score=8),
        ]
        result = self._filter(cards, min_impact=7)
        assert len(result) == 2

    def test_custom_min_impact(self):
        cards = [_make_card(impact_score=3)]
        result = self._filter(cards, min_impact=3)
        assert len(result) == 1


# --- personalize_feed ---


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

    def _personalize(self, cards, prefs, include_seen=True):
        personalized = []
        for card in cards:
            if not prefs:
                card.relevance_score = getattr(card, "impact_score", 5) / 10.0
            else:
                card.relevance_score = 0.5
            if not include_seen and card.interaction.get("viewed"):
                continue
            personalized.append(card)
        personalized.sort(key=lambda c: c.relevance_score, reverse=True)
        return personalized

    def test_empty_cards(self):
        assert self._personalize([], None) == []

    def test_sorts_by_relevance(self):
        c1 = _make_card(impact_score=3)
        c2 = _make_card(impact_score=9)
        result = self._personalize([c1, c2], None)
        assert result[0] is c2

    def test_filters_seen_when_requested(self):
        c1 = _make_card(interaction={"viewed": True})
        c2 = _make_card(interaction={})
        result = self._personalize([c1, c2], None, include_seen=False)
        assert len(result) == 1
        assert result[0] is c2

    def test_includes_seen_by_default(self):
        c1 = _make_card(interaction={"viewed": True})
        c2 = _make_card(interaction={})
        result = self._personalize([c1, c2], None, include_seen=True)
        assert len(result) == 2

    def test_all_seen_excluded(self):
        cards = [_make_card(interaction={"viewed": True}) for _ in range(3)]
        result = self._personalize(cards, None, include_seen=False)
        assert result == []


# --- InteractionType enum patterns ---


class TestInteractionTypePatterns:
    """Tests for InteractionType enum-like patterns."""

    def test_view_type(self):
        assert "view" == "view"

    def test_vote_up_type(self):
        assert "vote_up" == "vote_up"

    def test_vote_down_type(self):
        assert "vote_down" == "vote_down"

    def test_research_type(self):
        assert "research" == "research"

    def test_share_type(self):
        assert "share" == "share"


# --- Interaction recording patterns ---


class TestInteractionRecordingPatterns:
    """Tests for interaction recording logic from StorageManager."""

    def test_view_increments_views(self):
        interaction = {"views": 5}
        interaction["viewed"] = True
        interaction["views"] = interaction.get("views", 0) + 1
        assert interaction["views"] == 6
        assert interaction["viewed"] is True

    def test_view_first_time(self):
        interaction = {}
        interaction["viewed"] = True
        interaction["views"] = interaction.get("views", 0) + 1
        assert interaction["views"] == 1

    def test_vote_up_increments(self):
        interaction = {"votes_up": 3}
        interaction["voted"] = "up"
        interaction["votes_up"] = interaction.get("votes_up", 0) + 1
        assert interaction["votes_up"] == 4
        assert interaction["voted"] == "up"

    def test_vote_down_increments(self):
        interaction = {"votes_down": 2}
        interaction["voted"] = "down"
        interaction["votes_down"] = interaction.get("votes_down", 0) + 1
        assert interaction["votes_down"] == 3

    def test_research_increments(self):
        interaction = {}
        interaction["researched"] = True
        interaction["research_count"] = interaction.get("research_count", 0) + 1
        assert interaction["research_count"] == 1
        assert interaction["researched"] is True

    def test_metadata_attached(self):
        interaction = {}
        metadata = {"source": "web"}
        interaction_type = "view"
        interaction[f"{interaction_type}_metadata"] = metadata
        assert interaction["view_metadata"] == {"source": "web"}


# --- Card interaction vote mapping ---


class TestCardInteractionVoteMapping:
    """Tests for converting ratings to interaction format."""

    def _convert_ratings(self, ratings):
        interactions = []
        for rating in ratings:
            interaction = {
                "user_id": rating.get("user_id"),
                "interaction_type": "vote",
                "interaction_data": {
                    "vote": "up" if rating.get("value", 0) > 0 else "down"
                },
                "timestamp": rating.get("created_at"),
            }
            interactions.append(interaction)
        return interactions

    def test_positive_rating_maps_to_up(self):
        ratings = [{"user_id": "u1", "value": 1}]
        result = self._convert_ratings(ratings)
        assert result[0]["interaction_data"]["vote"] == "up"

    def test_negative_rating_maps_to_down(self):
        ratings = [{"user_id": "u1", "value": -1}]
        result = self._convert_ratings(ratings)
        assert result[0]["interaction_data"]["vote"] == "down"

    def test_zero_rating_maps_to_down(self):
        ratings = [{"user_id": "u1", "value": 0}]
        result = self._convert_ratings(ratings)
        assert result[0]["interaction_data"]["vote"] == "down"

    def test_preserves_user_id(self):
        ratings = [{"user_id": "user42", "value": 1}]
        result = self._convert_ratings(ratings)
        assert result[0]["user_id"] == "user42"

    def test_preserves_timestamp(self):
        ratings = [{"user_id": "u1", "value": 1, "created_at": "2025-01-01"}]
        result = self._convert_ratings(ratings)
        assert result[0]["timestamp"] == "2025-01-01"

    def test_empty_ratings(self):
        assert self._convert_ratings([]) == []

    def test_multiple_ratings(self):
        ratings = [
            {"user_id": "u1", "value": 1},
            {"user_id": "u2", "value": -1},
        ]
        result = self._convert_ratings(ratings)
        assert len(result) == 2


# --- User stats aggregation ---


class TestUserStatsAggregation:
    """Tests for user stats calculation patterns."""

    def _calc_stats(self, ratings, cards):
        votes_up = sum(1 for r in ratings if r.get("value", 0) > 0)
        votes_down = sum(1 for r in ratings if r.get("value", 0) < 0)
        total_views = sum(
            c.get("interaction", {}).get("views", 0) for c in cards
        )
        return {
            "votes_up": votes_up,
            "votes_down": votes_down,
            "total_views": total_views,
            "cards_created": len(cards),
            "member_since": cards[0]["created_at"] if cards else None,
        }

    def test_votes_counted(self):
        ratings = [{"value": 1}, {"value": -1}, {"value": 1}]
        stats = self._calc_stats(ratings, [])
        assert stats["votes_up"] == 2
        assert stats["votes_down"] == 1

    def test_views_summed(self):
        cards = [
            {"interaction": {"views": 5}, "created_at": "2025-01-01"},
            {"interaction": {"views": 10}, "created_at": "2025-01-02"},
        ]
        stats = self._calc_stats([], cards)
        assert stats["total_views"] == 15

    def test_cards_created_count(self):
        cards = [{"created_at": "2025-01-01"} for _ in range(5)]
        stats = self._calc_stats([], cards)
        assert stats["cards_created"] == 5

    def test_member_since_first_card(self):
        cards = [{"created_at": "2025-01-01"}, {"created_at": "2025-02-01"}]
        stats = self._calc_stats([], cards)
        assert stats["member_since"] == "2025-01-01"

    def test_member_since_none_when_no_cards(self):
        stats = self._calc_stats([], [])
        assert stats["member_since"] is None

    def test_empty_everything(self):
        stats = self._calc_stats([], [])
        assert stats["votes_up"] == 0
        assert stats["votes_down"] == 0
        assert stats["total_views"] == 0
        assert stats["cards_created"] == 0

    def test_zero_value_ratings_not_counted(self):
        ratings = [{"value": 0}, {"value": 0}]
        stats = self._calc_stats(ratings, [])
        assert stats["votes_up"] == 0
        assert stats["votes_down"] == 0

    def test_missing_interaction_key(self):
        cards = [{"created_at": "2025-01-01"}]
        stats = self._calc_stats([], cards)
        assert stats["total_views"] == 0
