"""
Extended tests for news/recommender/topic_based.py

Tests cover:
- TopicBasedRecommender initialization
- generate_recommendations() method
- _get_trending_topics() method
- _filter_topics_by_preferences() method
- _generate_topic_query() method
- _create_recommendation_card() method
- SearchBasedRecommender class
"""

from unittest.mock import Mock, patch


class TestTopicBasedRecommenderInit:
    """Tests for TopicBasedRecommender initialization."""

    def test_creates_instance(self):
        """Creates TopicBasedRecommender instance."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        assert recommender is not None

    def test_inherits_from_base_recommender(self):
        """Inherits from BaseRecommender."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )
        from local_deep_research.news.recommender.base_recommender import (
            BaseRecommender,
        )

        recommender = TopicBasedRecommender()

        assert isinstance(recommender, BaseRecommender)

    def test_default_max_recommendations(self):
        """Default max_recommendations is 5."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        assert recommender.max_recommendations == 5

    def test_accepts_topic_registry(self):
        """Accepts topic_registry parameter."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        mock_registry = Mock()
        recommender = TopicBasedRecommender(topic_registry=mock_registry)

        assert recommender.topic_registry is mock_registry

    def test_accepts_preference_manager(self):
        """Accepts preference_manager parameter."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        mock_manager = Mock()
        recommender = TopicBasedRecommender(preference_manager=mock_manager)

        assert recommender.preference_manager is mock_manager


class TestGenerateRecommendations:
    """Tests for generate_recommendations() method."""

    def test_returns_list(self):
        """Returns a list."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        recommender._get_trending_topics = Mock(return_value=["AI"])
        recommender._filter_topics_by_preferences = Mock(return_value=[])
        recommender._get_user_preferences = Mock(return_value={})

        result = recommender.generate_recommendations("user1")

        assert isinstance(result, list)

    def test_calls_get_trending_topics(self):
        """Calls _get_trending_topics."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        recommender._get_trending_topics = Mock(return_value=[])
        recommender._filter_topics_by_preferences = Mock(return_value=[])
        recommender._get_user_preferences = Mock(return_value={})

        recommender.generate_recommendations("user1")

        recommender._get_trending_topics.assert_called_once()

    def test_calls_filter_topics_by_preferences(self):
        """Calls _filter_topics_by_preferences."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        recommender._get_trending_topics = Mock(return_value=["AI"])
        recommender._filter_topics_by_preferences = Mock(return_value=[])
        recommender._get_user_preferences = Mock(return_value={})

        recommender.generate_recommendations("user1")

        recommender._filter_topics_by_preferences.assert_called_once()

    def test_limits_to_max_recommendations(self):
        """Limits topics to max_recommendations."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        recommender.max_recommendations = 2
        recommender._get_trending_topics = Mock(
            return_value=["A", "B", "C", "D", "E"]
        )
        recommender._filter_topics_by_preferences = Mock(
            return_value=["A", "B", "C", "D", "E"]
        )
        recommender._get_user_preferences = Mock(return_value={})
        recommender._create_recommendation_card = Mock(return_value=None)

        recommender.generate_recommendations("user1")

        # Should only process max_recommendations topics
        assert recommender._create_recommendation_card.call_count <= 2

    def test_handles_empty_topics(self):
        """Handles empty topics list."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        recommender._get_trending_topics = Mock(return_value=[])
        recommender._filter_topics_by_preferences = Mock(return_value=[])
        recommender._get_user_preferences = Mock(return_value={})

        result = recommender.generate_recommendations("user1")

        assert result == []

    def test_handles_exception_gracefully(self):
        """Handles exception gracefully."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        recommender._get_trending_topics = Mock(side_effect=Exception("Error"))

        result = recommender.generate_recommendations("user1")

        # Should return empty list, not raise
        assert result == []


class TestGetTrendingTopics:
    """Tests for _get_trending_topics() method."""

    def test_returns_list(self):
        """Returns a list."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        result = recommender._get_trending_topics(None)

        assert isinstance(result, list)

    def test_uses_topic_registry_if_available(self):
        """Uses topic_registry if available."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        mock_registry = Mock()
        mock_registry.get_trending_topics.return_value = ["AI", "ML"]

        recommender = TopicBasedRecommender(topic_registry=mock_registry)
        result = recommender._get_trending_topics(None)

        mock_registry.get_trending_topics.assert_called_once()
        assert "AI" in result

    def test_adds_context_topics(self):
        """Adds topics from context."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        context = {"current_news_topics": ["Technology"]}
        result = recommender._get_trending_topics(context)

        assert "Technology" in result

    def test_returns_fallback_topics_when_none_found(self):
        """Returns fallback topics when none found."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        result = recommender._get_trending_topics(None)

        # Should have fallback topics
        assert len(result) > 0

    def test_fallback_includes_ai_topic(self):
        """Fallback includes AI topic."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        result = recommender._get_trending_topics(None)

        assert any("artificial intelligence" in t.lower() for t in result)


class TestFilterTopicsByPreferences:
    """Tests for _filter_topics_by_preferences() method."""

    def test_returns_list(self):
        """Returns a list."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        topics = ["AI", "Sports"]
        preferences = {}

        result = recommender._filter_topics_by_preferences(topics, preferences)

        assert isinstance(result, list)

    def test_filters_disliked_topics(self):
        """Filters out disliked topics."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        topics = ["AI News", "Sports News", "Tech Updates"]
        preferences = {"disliked_topics": ["sports"]}

        result = recommender._filter_topics_by_preferences(topics, preferences)

        assert "Sports News" not in result
        assert "AI News" in result

    def test_boosts_topics_matching_interests(self):
        """Boosts topics matching interests."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        topics = ["General News", "AI Developments"]
        preferences = {"interests": {"AI": 2.0}}

        result = recommender._filter_topics_by_preferences(topics, preferences)

        # AI topic should be first due to higher boost
        assert result[0] == "AI Developments"

    def test_preserves_all_topics_when_no_disliked(self):
        """Preserves all topics when no disliked topics."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        topics = ["AI", "Tech", "Science"]
        preferences = {}

        result = recommender._filter_topics_by_preferences(topics, preferences)

        assert len(result) == 3

    def test_handles_empty_topics_list(self):
        """Handles empty topics list."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        result = recommender._filter_topics_by_preferences([], {})

        assert result == []

    def test_case_insensitive_dislike_matching(self):
        """Case insensitive dislike matching."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        topics = ["SPORTS News", "sports update"]
        preferences = {"disliked_topics": ["Sports"]}

        result = recommender._filter_topics_by_preferences(topics, preferences)

        assert len(result) == 0


class TestGenerateTopicQuery:
    """Tests for _generate_topic_query() method."""

    def test_returns_string(self):
        """Returns a string."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        result = recommender._generate_topic_query("AI")

        assert isinstance(result, str)

    def test_includes_topic_in_query(self):
        """Includes topic in query."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        result = recommender._generate_topic_query("artificial intelligence")

        assert "artificial intelligence" in result

    def test_includes_news_keywords(self):
        """Includes news-related keywords."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        result = recommender._generate_topic_query("tech")

        assert "news" in result.lower()

    def test_includes_latest_keyword(self):
        """Includes 'latest' keyword for fresh news."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        result = recommender._generate_topic_query("topic")

        assert "latest" in result.lower()


class TestCreateRecommendationCard:
    """Tests for _create_recommendation_card() method."""

    def test_returns_none_on_search_error(self):
        """Returns None when search fails."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        with patch(
            "local_deep_research.news.recommender.topic_based.AdvancedSearchSystem"
        ) as MockSearch:
            mock_instance = Mock()
            mock_instance.analyze_topic.return_value = {
                "error": "Search failed"
            }
            MockSearch.return_value = mock_instance

            recommender = TopicBasedRecommender()
            result = recommender._create_recommendation_card(
                "topic", "query", "user1"
            )

            assert result is None

    def test_returns_none_when_no_items_found(self):
        """Returns None when no news items found."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        with patch(
            "local_deep_research.news.recommender.topic_based.AdvancedSearchSystem"
        ) as MockSearch:
            mock_instance = Mock()
            mock_instance.analyze_topic.return_value = {"news_items": []}
            MockSearch.return_value = mock_instance

            recommender = TopicBasedRecommender()
            result = recommender._create_recommendation_card(
                "topic", "query", "user1"
            )

            assert result is None

    def test_handles_exception_gracefully(self):
        """Handles exception gracefully."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        with patch(
            "local_deep_research.news.recommender.topic_based.AdvancedSearchSystem"
        ) as MockSearch:
            MockSearch.side_effect = Exception("Search system error")

            recommender = TopicBasedRecommender()
            result = recommender._create_recommendation_card(
                "topic", "query", "user1"
            )

            assert result is None

    def test_returns_none_when_llm_not_configured(self):
        """Returns None when get_llm() raises ValueError; does NOT proceed to construct
        AdvancedSearchSystem. Scheduler runs repeatedly — must not propagate stack trace.

        Strengthened to distinguish the specific `except ValueError` path from the
        outer `except Exception` swallow: this asserts that the *warning* log was
        emitted (specific path) rather than `logger.exception` (catch-all path).
        Without this assertion, removing the targeted `except ValueError` block
        would still let the test pass — the outer handler would also return None.
        """
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )
        from local_deep_research.security import sanitize_for_log

        with (
            patch(
                "local_deep_research.config.llm_config.get_llm"
            ) as mock_get_llm,
            patch(
                "local_deep_research.news.recommender.topic_based.AdvancedSearchSystem"
            ) as MockSearch,
            patch(
                "local_deep_research.news.recommender.topic_based.logger"
            ) as mock_logger,
        ):
            mock_get_llm.side_effect = ValueError(
                "LLM model not configured. ..."
            )

            # Topic carries a CRLF log-injection payload; the warning must
            # interpolate the *sanitized* form. Binding it to a variable (and
            # deriving the expected substring from it) removes the previous
            # assertion's coupling to a hard-coded "AI" literal.
            topic = "AI\r\ninjected-fake-log-line"
            recommender = TopicBasedRecommender()
            result = recommender._create_recommendation_card(
                topic, "query", "user1"
            )

            assert result is None
            mock_get_llm.assert_called_once()
            # Critical: we returned before building the search system.
            MockSearch.assert_not_called()
            # The targeted except-ValueError path uses logger.warning; the
            # catch-all `except Exception` would use logger.exception. Asserting
            # warning was called and exception was NOT pins the code path.
            mock_logger.warning.assert_called_once()
            warning_msg = mock_logger.warning.call_args[0][0]
            assert "LLM not configured" in warning_msg
            # The sanitized topic is interpolated; the raw CR/LF payload never
            # reaches the log line (this is the log-injection guarantee).
            assert sanitize_for_log(topic) in warning_msg
            assert "\r" not in warning_msg
            assert "\n" not in warning_msg
            mock_logger.exception.assert_not_called()


class TestSearchBasedRecommender:
    """Tests for SearchBasedRecommender class."""

    def test_creates_instance(self):
        """Creates SearchBasedRecommender instance."""
        from local_deep_research.news.recommender.topic_based import (
            SearchBasedRecommender,
        )

        recommender = SearchBasedRecommender()

        assert recommender is not None

    def test_inherits_from_base_recommender(self):
        """Inherits from BaseRecommender."""
        from local_deep_research.news.recommender.topic_based import (
            SearchBasedRecommender,
        )
        from local_deep_research.news.recommender.base_recommender import (
            BaseRecommender,
        )

        recommender = SearchBasedRecommender()

        assert isinstance(recommender, BaseRecommender)

    def test_generate_recommendations_returns_list(self):
        """generate_recommendations returns a list."""
        from local_deep_research.news.recommender.topic_based import (
            SearchBasedRecommender,
        )

        recommender = SearchBasedRecommender()

        result = recommender.generate_recommendations("user1")

        assert isinstance(result, list)

    def test_returns_empty_list_when_tracking_disabled(self):
        """Returns empty list when search tracking is disabled."""
        from local_deep_research.news.recommender.topic_based import (
            SearchBasedRecommender,
        )

        recommender = SearchBasedRecommender()

        result = recommender.generate_recommendations("user1")

        # Currently returns empty since tracking is off by default
        assert result == []

    def test_accepts_context(self):
        """Accepts context parameter."""
        from local_deep_research.news.recommender.topic_based import (
            SearchBasedRecommender,
        )

        recommender = SearchBasedRecommender()

        result = recommender.generate_recommendations(
            "user1", context={"key": "value"}
        )

        assert isinstance(result, list)


class TestRecommenderProgressTracking:
    """Tests for progress tracking in recommenders."""

    def test_has_progress_callback_support(self):
        """Recommender supports progress callback."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        # Should have progress_callback attribute
        assert hasattr(recommender, "progress_callback")

    def test_set_progress_callback(self):
        """Can set progress callback."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        callback = Mock()
        recommender.set_progress_callback(callback)

        assert recommender.progress_callback is callback


class TestRecommenderEdgeCases:
    """Edge case tests for recommenders."""

    def test_empty_user_id(self):
        """Handles empty user_id."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        recommender._get_trending_topics = Mock(return_value=[])
        recommender._filter_topics_by_preferences = Mock(return_value=[])
        recommender._get_user_preferences = Mock(return_value={})

        result = recommender.generate_recommendations("")

        assert isinstance(result, list)

    def test_unicode_topic(self):
        """Handles unicode topics."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        result = recommender._generate_topic_query("日本語ニュース")

        assert "日本語ニュース" in result

    def test_very_long_topic(self):
        """Handles very long topics."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()
        long_topic = "A" * 1000

        result = recommender._generate_topic_query(long_topic)

        assert long_topic in result

    def test_special_characters_in_topic(self):
        """Handles special characters in topics."""
        from local_deep_research.news.recommender.topic_based import (
            TopicBasedRecommender,
        )

        recommender = TopicBasedRecommender()

        result = recommender._generate_topic_query("C++ & Python")

        assert "C++ & Python" in result
