"""
Deep behavioral tests for BasePreferenceManager and TopicRegistry.
Tests default preferences, interest management, topic ignore,
source boosting, and topic registry trending calculations.
"""

from unittest.mock import Mock

import pytest

from local_deep_research.news.preference_manager.base_preference import (
    BasePreferenceManager,
    TopicRegistry,
)


# --- BasePreferenceManager abstract ---


class TestBasePreferenceManagerAbstract:
    """Tests for BasePreferenceManager abstract enforcement."""

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

    def test_storage_backend_default_none(self):
        """Concrete subclass should have storage_backend=None by default."""

        class ConcretePrefs(BasePreferenceManager):
            def get_preferences(self, user_id):
                return {}

            def update_preferences(self, user_id, preferences):
                return preferences

        mgr = ConcretePrefs()
        assert mgr.storage_backend is None

    def test_storage_backend_injected(self):
        class ConcretePrefs(BasePreferenceManager):
            def get_preferences(self, user_id):
                return {}

            def update_preferences(self, user_id, preferences):
                return preferences

        backend = Mock()
        mgr = ConcretePrefs(storage_backend=backend)
        assert mgr.storage_backend is backend


# --- Concrete preference manager for testing ---


class InMemoryPreferenceManager(BasePreferenceManager):
    """In-memory implementation for testing."""

    def __init__(self):
        super().__init__()
        self._store = {}

    def get_preferences(self, user_id):
        return self._store.get(user_id, self.get_default_preferences())

    def update_preferences(self, user_id, preferences):
        self._store[user_id] = preferences
        return preferences


# --- Default preferences ---


class TestDefaultPreferences:
    """Tests for get_default_preferences."""

    def test_has_liked_categories(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert "liked_categories" in prefs
        assert prefs["liked_categories"] == []

    def test_has_disliked_categories(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert prefs["disliked_categories"] == []

    def test_has_liked_topics(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert prefs["liked_topics"] == []

    def test_has_disliked_topics(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert prefs["disliked_topics"] == []

    def test_has_interests(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert prefs["interests"] == {}

    def test_has_source_weights(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert prefs["source_weights"] == {}

    def test_has_impact_threshold(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert prefs["impact_threshold"] == 5

    def test_has_focus_preferences(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        focus = prefs["focus_preferences"]
        assert focus["surprising"] is False
        assert focus["breaking"] is True
        assert focus["positive"] is False
        assert focus["local"] is False

    def test_has_custom_search_terms(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert prefs["custom_search_terms"] == ""

    def test_has_search_strategy(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert prefs["search_strategy"] == "news_aggregation"

    def test_has_created_at(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert "created_at" in prefs

    def test_has_preferences_updated_at(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_default_preferences()
        assert "preferences_updated_at" in prefs


# --- Interest management ---


class TestInterestManagement:
    """Tests for add_interest and remove_interest."""

    def test_add_interest(self):
        mgr = InMemoryPreferenceManager()
        mgr.add_interest("u1", "AI", 1.5)
        prefs = mgr.get_preferences("u1")
        assert prefs["interests"]["AI"] == 1.5

    def test_add_interest_default_weight(self):
        mgr = InMemoryPreferenceManager()
        mgr.add_interest("u1", "ML")
        prefs = mgr.get_preferences("u1")
        assert prefs["interests"]["ML"] == 1.0

    def test_add_multiple_interests(self):
        mgr = InMemoryPreferenceManager()
        mgr.add_interest("u1", "AI", 2.0)
        mgr.add_interest("u1", "ML", 1.5)
        prefs = mgr.get_preferences("u1")
        assert len(prefs["interests"]) == 2

    def test_overwrite_interest_weight(self):
        mgr = InMemoryPreferenceManager()
        mgr.add_interest("u1", "AI", 1.0)
        mgr.add_interest("u1", "AI", 3.0)
        prefs = mgr.get_preferences("u1")
        assert prefs["interests"]["AI"] == 3.0

    def test_remove_interest(self):
        mgr = InMemoryPreferenceManager()
        mgr.add_interest("u1", "AI", 1.0)
        mgr.remove_interest("u1", "AI")
        prefs = mgr.get_preferences("u1")
        assert "AI" not in prefs["interests"]

    def test_remove_nonexistent_interest(self):
        mgr = InMemoryPreferenceManager()
        # Should not raise
        mgr.remove_interest("u1", "nonexistent")

    def test_add_interest_sets_timestamp(self):
        mgr = InMemoryPreferenceManager()
        mgr.add_interest("u1", "AI")
        prefs = mgr.get_preferences("u1")
        assert "interests_updated_at" in prefs

    def test_remove_interest_sets_timestamp(self):
        mgr = InMemoryPreferenceManager()
        mgr.add_interest("u1", "AI")
        mgr.remove_interest("u1", "AI")
        prefs = mgr.get_preferences("u1")
        assert "interests_updated_at" in prefs


# --- Topic ignore ---


class TestTopicIgnore:
    """Tests for ignore_topic."""

    def test_ignore_topic_adds_to_list(self):
        mgr = InMemoryPreferenceManager()
        mgr.ignore_topic("u1", "politics")
        prefs = mgr.get_preferences("u1")
        assert "politics" in prefs["disliked_topics"]

    def test_ignore_topic_no_duplicates(self):
        mgr = InMemoryPreferenceManager()
        mgr.ignore_topic("u1", "sports")
        mgr.ignore_topic("u1", "sports")
        prefs = mgr.get_preferences("u1")
        assert prefs["disliked_topics"].count("sports") == 1

    def test_ignore_multiple_topics(self):
        mgr = InMemoryPreferenceManager()
        mgr.ignore_topic("u1", "sports")
        mgr.ignore_topic("u1", "politics")
        prefs = mgr.get_preferences("u1")
        assert len(prefs["disliked_topics"]) == 2

    def test_ignore_sets_timestamp(self):
        mgr = InMemoryPreferenceManager()
        mgr.ignore_topic("u1", "sports")
        prefs = mgr.get_preferences("u1")
        assert "preferences_updated_at" in prefs


# --- Source boosting ---


class TestSourceBoosting:
    """Tests for boost_source."""

    def test_boost_source(self):
        mgr = InMemoryPreferenceManager()
        mgr.boost_source("u1", "reuters.com", 2.0)
        prefs = mgr.get_preferences("u1")
        assert prefs["source_weights"]["reuters.com"] == 2.0

    def test_boost_default_weight(self):
        mgr = InMemoryPreferenceManager()
        mgr.boost_source("u1", "bbc.com")
        prefs = mgr.get_preferences("u1")
        assert prefs["source_weights"]["bbc.com"] == 1.5

    def test_boost_multiple_sources(self):
        mgr = InMemoryPreferenceManager()
        mgr.boost_source("u1", "reuters.com", 2.0)
        mgr.boost_source("u1", "bbc.com", 1.8)
        prefs = mgr.get_preferences("u1")
        assert len(prefs["source_weights"]) == 2

    def test_overwrite_source_weight(self):
        mgr = InMemoryPreferenceManager()
        mgr.boost_source("u1", "reuters.com", 1.5)
        mgr.boost_source("u1", "reuters.com", 3.0)
        prefs = mgr.get_preferences("u1")
        assert prefs["source_weights"]["reuters.com"] == 3.0


# --- User isolation ---


class TestPreferenceUserIsolation:
    """Tests that preferences are isolated between users."""

    def test_different_users_different_prefs(self):
        mgr = InMemoryPreferenceManager()
        mgr.add_interest("u1", "AI", 2.0)
        mgr.add_interest("u2", "Sports", 1.5)
        u1_prefs = mgr.get_preferences("u1")
        u2_prefs = mgr.get_preferences("u2")
        assert "AI" in u1_prefs["interests"]
        assert "AI" not in u2_prefs["interests"]
        assert "Sports" in u2_prefs["interests"]

    def test_new_user_gets_defaults(self):
        mgr = InMemoryPreferenceManager()
        prefs = mgr.get_preferences("new_user")
        defaults = mgr.get_default_preferences()
        assert prefs["impact_threshold"] == defaults["impact_threshold"]


# --- TopicRegistry ---


class TestTopicRegistryInit:
    """Tests for TopicRegistry initialization."""

    def test_default_no_llm(self):
        reg = TopicRegistry()
        assert reg.llm_client is None

    def test_llm_injected(self):
        llm = Mock()
        reg = TopicRegistry(llm_client=llm)
        assert reg.llm_client is llm

    def test_initial_topics_empty(self):
        reg = TopicRegistry()
        assert reg.topics == {}


# --- Topic registration ---


class TestTopicRegistration:
    """Tests for register_topic."""

    def test_register_new_topic(self):
        reg = TopicRegistry()
        reg.register_topic("AI")
        assert "AI" in reg.topics

    def test_register_sets_count_1(self):
        reg = TopicRegistry()
        reg.register_topic("AI")
        assert reg.topics["AI"]["count"] == 1

    def test_register_increments_count(self):
        reg = TopicRegistry()
        reg.register_topic("AI")
        reg.register_topic("AI")
        assert reg.topics["AI"]["count"] == 2

    def test_register_sets_first_seen(self):
        reg = TopicRegistry()
        reg.register_topic("AI")
        assert reg.topics["AI"]["first_seen"] is not None

    def test_register_sets_last_seen(self):
        reg = TopicRegistry()
        reg.register_topic("AI")
        assert reg.topics["AI"]["last_seen"] is not None

    def test_register_multiple_topics(self):
        reg = TopicRegistry()
        reg.register_topic("AI")
        reg.register_topic("ML")
        reg.register_topic("NLP")
        assert len(reg.topics) == 3


# --- Trending topics ---


class TestTrendingTopics:
    """Tests for get_trending_topics."""

    def test_empty_registry_returns_empty(self):
        reg = TopicRegistry()
        assert reg.get_trending_topics() == []

    def test_recent_topics_returned(self):
        reg = TopicRegistry()
        reg.register_topic("AI")
        topics = reg.get_trending_topics(hours=24)
        assert "AI" in topics

    def test_ordered_by_count(self):
        reg = TopicRegistry()
        reg.register_topic("less popular")
        for _ in range(5):
            reg.register_topic("popular")
        topics = reg.get_trending_topics()
        assert topics[0] == "popular"

    def test_respects_limit(self):
        reg = TopicRegistry()
        for i in range(20):
            reg.register_topic(f"topic-{i}")
        topics = reg.get_trending_topics(limit=5)
        assert len(topics) == 5

    def test_default_limit_10(self):
        reg = TopicRegistry()
        for i in range(15):
            reg.register_topic(f"topic-{i}")
        topics = reg.get_trending_topics()
        assert len(topics) == 10


# --- Topic info ---


class TestTopicInfo:
    """Tests for get_topic_info."""

    def test_returns_info_for_registered(self):
        reg = TopicRegistry()
        reg.register_topic("AI")
        info = reg.get_topic_info("AI")
        assert info is not None
        assert "count" in info

    def test_returns_none_for_unknown(self):
        reg = TopicRegistry()
        assert reg.get_topic_info("unknown") is None

    def test_count_matches_registrations(self):
        reg = TopicRegistry()
        for _ in range(7):
            reg.register_topic("AI")
        info = reg.get_topic_info("AI")
        assert info["count"] == 7

    def test_first_seen_stable(self):
        reg = TopicRegistry()
        reg.register_topic("AI")
        first_seen = reg.topics["AI"]["first_seen"]
        reg.register_topic("AI")
        assert reg.topics["AI"]["first_seen"] == first_seen

    def test_last_seen_updates(self):
        reg = TopicRegistry()
        reg.register_topic("AI")
        first_last_seen = reg.topics["AI"]["last_seen"]
        reg.register_topic("AI")
        # last_seen should be >= first registration
        assert reg.topics["AI"]["last_seen"] >= first_last_seen
