"""
Deep behavioral tests for BaseRecommender.
Tests filtering, scoring, preferences, progress callbacks, and strategy info.
"""

from unittest.mock import Mock

import pytest

from local_deep_research.news.core.base_card import CardSource, NewsCard
from local_deep_research.news.recommender.base_recommender import (
    BaseRecommender,
)


def _make_card(topic="Test", category="General", impact_score=5, **kwargs):
    source = CardSource(type="news_item")
    return NewsCard(
        topic=topic,
        source=source,
        user_id="u1",
        category=category,
        impact_score=impact_score,
        **kwargs,
    )


class ConcreteRecommender(BaseRecommender):
    """Concrete implementation for testing abstract base class."""

    def generate_recommendations(self, user_id, context=None):
        return []


# --- Init tests ---


class TestBaseRecommenderInit:
    """Tests for BaseRecommender initialization."""

    def test_all_deps_none_by_default(self):
        r = ConcreteRecommender()
        assert r.preference_manager is None
        assert r.rating_system is None
        assert r.topic_registry is None
        assert r.search_system is None

    def test_stores_preference_manager(self):
        pm = Mock()
        r = ConcreteRecommender(preference_manager=pm)
        assert r.preference_manager is pm

    def test_stores_rating_system(self):
        rs = Mock()
        r = ConcreteRecommender(rating_system=rs)
        assert r.rating_system is rs

    def test_stores_topic_registry(self):
        tr = Mock()
        r = ConcreteRecommender(topic_registry=tr)
        assert r.topic_registry is tr

    def test_stores_search_system(self):
        ss = Mock()
        r = ConcreteRecommender(search_system=ss)
        assert r.search_system is ss

    def test_progress_callback_none_initially(self):
        r = ConcreteRecommender()
        assert r.progress_callback is None

    def test_strategy_name_is_class_name(self):
        r = ConcreteRecommender()
        assert r.strategy_name == "ConcreteRecommender"


# --- Progress callback tests ---


class TestRecommenderProgressCallback:
    """Tests for progress callback mechanism."""

    def test_set_callback(self):
        r = ConcreteRecommender()
        cb = Mock()
        r.set_progress_callback(cb)
        assert r.progress_callback is cb

    def test_update_progress_calls_callback(self):
        r = ConcreteRecommender()
        cb = Mock()
        r.set_progress_callback(cb)
        r._update_progress("step1", 25, {"key": "val"})
        cb.assert_called_once_with("step1", 25, {"key": "val"})

    def test_update_progress_no_callback_no_error(self):
        r = ConcreteRecommender()
        r._update_progress("msg")  # Should not raise

    def test_update_progress_default_metadata(self):
        r = ConcreteRecommender()
        cb = Mock()
        r.set_progress_callback(cb)
        r._update_progress("msg", 50)
        cb.assert_called_once_with("msg", 50, {})


# --- Get user preferences ---


class TestGetUserPreferences:
    """Tests for _get_user_preferences method."""

    def test_returns_empty_when_no_manager(self):
        r = ConcreteRecommender()
        assert r._get_user_preferences("u1") == {}

    def test_calls_preference_manager(self):
        pm = Mock()
        pm.get_preferences.return_value = {"liked_categories": ["Tech"]}
        r = ConcreteRecommender(preference_manager=pm)

        result = r._get_user_preferences("u1")

        pm.get_preferences.assert_called_once_with("u1")
        assert result == {"liked_categories": ["Tech"]}


# --- Get user ratings ---


class TestGetUserRatings:
    """Tests for _get_user_ratings method."""

    def test_returns_empty_when_no_system(self):
        r = ConcreteRecommender()
        assert r._get_user_ratings("u1") == []

    def test_calls_rating_system(self):
        rs = Mock()
        rs.get_recent_ratings.return_value = [{"card_id": "c1", "rating": 5}]
        r = ConcreteRecommender(rating_system=rs)

        result = r._get_user_ratings("u1", limit=10)

        rs.get_recent_ratings.assert_called_once_with("u1", 10)
        assert len(result) == 1

    def test_default_limit_50(self):
        rs = Mock()
        rs.get_recent_ratings.return_value = []
        r = ConcreteRecommender(rating_system=rs)

        r._get_user_ratings("u1")

        rs.get_recent_ratings.assert_called_once_with("u1", 50)


# --- Execute search ---


class TestExecuteSearch:
    """Tests for _execute_search method."""

    def test_returns_error_when_no_system(self):
        r = ConcreteRecommender()
        result = r._execute_search("query")
        assert "error" in result

    def test_calls_search_system(self):
        ss = Mock()
        ss.analyze_topic.return_value = {"findings": ["result1"]}
        r = ConcreteRecommender(search_system=ss)

        result = r._execute_search("AI news")

        ss.analyze_topic.assert_called_once_with("AI news")
        assert result == {"findings": ["result1"]}

    def test_handles_search_exception(self):
        ss = Mock()
        ss.analyze_topic.side_effect = RuntimeError("search failed")
        r = ConcreteRecommender(search_system=ss)

        result = r._execute_search("query")

        assert "error" in result


# --- Filter by preferences ---


class TestFilterByPreferences:
    """Tests for _filter_by_preferences method."""

    def test_no_preferences_returns_all(self):
        r = ConcreteRecommender()
        cards = [_make_card(), _make_card()]
        result = r._filter_by_preferences(cards, {})
        assert len(result) == 2

    def test_boosts_liked_categories(self):
        r = ConcreteRecommender()
        tech_card = _make_card(category="Tech")
        general_card = _make_card(category="General")

        prefs = {"liked_categories": ["Tech"]}
        r._filter_by_preferences([tech_card, general_card], prefs)

        assert tech_card.metadata.get("preference_boost") == 1.2
        assert "preference_boost" not in general_card.metadata

    def test_empty_liked_categories_no_boost(self):
        r = ConcreteRecommender()
        card = _make_card(category="Tech")
        prefs = {"liked_categories": []}
        r._filter_by_preferences([card], prefs)
        assert "preference_boost" not in card.metadata

    def test_filters_by_impact_threshold(self):
        r = ConcreteRecommender()
        high = _make_card(impact_score=8)
        low = _make_card(impact_score=3)

        prefs = {"impact_threshold": 5}
        result = r._filter_by_preferences([high, low], prefs)

        assert high in result
        assert low not in result

    def test_impact_threshold_exact_boundary(self):
        r = ConcreteRecommender()
        card = _make_card(impact_score=5)

        prefs = {"impact_threshold": 5}
        result = r._filter_by_preferences([card], prefs)

        assert card in result

    def test_filters_disliked_topics(self):
        r = ConcreteRecommender()
        sports = _make_card(topic="Sports News Today")
        tech = _make_card(topic="Tech Innovation")

        prefs = {"disliked_topics": ["sports"]}
        result = r._filter_by_preferences([sports, tech], prefs)

        assert tech in result
        assert sports not in result

    def test_disliked_topics_case_insensitive(self):
        r = ConcreteRecommender()
        card = _make_card(topic="SPORTS Update")

        prefs = {"disliked_topics": ["sports"]}
        result = r._filter_by_preferences([card], prefs)

        assert card not in result

    def test_combined_filters(self):
        r = ConcreteRecommender()
        good_tech = _make_card(
            topic="AI Progress", category="Tech", impact_score=8
        )
        bad_sports = _make_card(
            topic="Sports News", category="Sports", impact_score=3
        )
        low_tech = _make_card(
            topic="Minor Tech", category="Tech", impact_score=2
        )

        prefs = {
            "liked_categories": ["Tech"],
            "impact_threshold": 5,
            "disliked_topics": ["sports"],
        }
        result = r._filter_by_preferences(
            [good_tech, bad_sports, low_tech], prefs
        )

        assert good_tech in result
        assert bad_sports not in result
        assert low_tech not in result

    def test_empty_cards_list(self):
        r = ConcreteRecommender()
        result = r._filter_by_preferences([], {"impact_threshold": 5})
        assert result == []


# --- Sort by relevance ---


class TestSortByRelevance:
    """Tests for _sort_by_relevance method."""

    def test_empty_list(self):
        r = ConcreteRecommender()
        result = r._sort_by_relevance([], "u1")
        assert result == []

    def test_sorts_by_impact_score(self):
        r = ConcreteRecommender()
        low = _make_card(impact_score=3)
        high = _make_card(impact_score=9)
        mid = _make_card(impact_score=6)

        result = r._sort_by_relevance([low, high, mid], "u1")

        assert result[0] is high
        assert result[1] is mid
        assert result[2] is low

    def test_preference_boost_affects_ordering(self):
        r = ConcreteRecommender()
        # Low impact but boosted
        boosted = _make_card(impact_score=5)
        boosted.metadata["preference_boost"] = 2.0
        # High impact not boosted
        normal = _make_card(impact_score=8)

        result = r._sort_by_relevance([normal, boosted], "u1")

        # boosted: 5/10 * 2.0 = 1.0; normal: 8/10 * 1.0 = 0.8
        assert result[0] is boosted

    def test_preserves_all_cards(self):
        r = ConcreteRecommender()
        cards = [_make_card(impact_score=i) for i in range(1, 6)]
        result = r._sort_by_relevance(cards, "u1")
        assert len(result) == 5

    def test_single_card(self):
        r = ConcreteRecommender()
        card = _make_card(impact_score=7)
        result = r._sort_by_relevance([card], "u1")
        assert result == [card]


# --- Strategy info ---


class TestGetStrategyInfo:
    """Tests for get_strategy_info method."""

    def test_includes_name(self):
        r = ConcreteRecommender()
        info = r.get_strategy_info()
        assert info["name"] == "ConcreteRecommender"

    def test_has_preference_manager_false(self):
        r = ConcreteRecommender()
        info = r.get_strategy_info()
        assert info["has_preference_manager"] is False

    def test_has_preference_manager_true(self):
        r = ConcreteRecommender(preference_manager=Mock())
        info = r.get_strategy_info()
        assert info["has_preference_manager"] is True

    def test_has_rating_system_false(self):
        r = ConcreteRecommender()
        info = r.get_strategy_info()
        assert info["has_rating_system"] is False

    def test_has_rating_system_true(self):
        r = ConcreteRecommender(rating_system=Mock())
        info = r.get_strategy_info()
        assert info["has_rating_system"] is True

    def test_has_search_system_false(self):
        r = ConcreteRecommender()
        info = r.get_strategy_info()
        assert info["has_search_system"] is False

    def test_has_search_system_true(self):
        r = ConcreteRecommender(search_system=Mock())
        info = r.get_strategy_info()
        assert info["has_search_system"] is True

    def test_includes_description(self):
        r = ConcreteRecommender()
        info = r.get_strategy_info()
        assert "description" in info


# --- Abstract method enforcement ---


class TestBaseRecommenderAbstract:
    """Tests that BaseRecommender can't be instantiated directly."""

    def test_cannot_instantiate_directly(self):
        with pytest.raises(TypeError):
            BaseRecommender()

    def test_subclass_must_implement_generate_recommendations(self):
        class Incomplete(BaseRecommender):
            pass

        with pytest.raises(TypeError):
            Incomplete()
