"""
Tests for TopicSubscription and TopicSubscriptionFactory.

Tests cover:
- TopicSubscription initialization and configuration
- Search query generation for topics
- Activity tracking and trending detection
- Topic evolution and merging
- Auto-expiration logic
- Statistics and serialization
- Factory methods
"""

import pytest
from unittest.mock import patch, MagicMock
from datetime import datetime

from local_deep_research.news.subscription_manager.topic_subscription import (
    TopicSubscription,
    TopicSubscriptionFactory,
)
from local_deep_research.news.core.base_card import CardSource


@pytest.fixture
def mock_storage():
    """Mock the SQLSubscriptionStorage to avoid database dependencies."""
    with patch(
        "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
    ) as mock:
        mock_instance = MagicMock()
        mock.return_value = mock_instance
        yield mock_instance


@pytest.fixture
def fixed_time():
    """Return a fixed datetime for testing."""
    return datetime(2024, 1, 15, 12, 0, 0)


@pytest.fixture
def mock_utc_now(fixed_time):
    """Mock utc_now to return consistent timestamps."""
    with patch(
        "local_deep_research.news.subscription_manager.base_subscription.utc_now"
    ) as mock_base:
        with patch(
            "local_deep_research.news.subscription_manager.topic_subscription.utc_now"
        ) as mock_topic:
            mock_base.return_value = fixed_time
            mock_topic.return_value = fixed_time
            yield fixed_time


class TestTopicSubscriptionInit:
    """Tests for TopicSubscription initialization."""

    def test_basic_initialization(self, mock_storage, mock_utc_now):
        """Test basic subscription creation with required parameters."""
        sub = TopicSubscription(
            topic="artificial intelligence",
            user_id="user_123",
        )

        assert sub.topic == "artificial intelligence"
        assert sub.current_topic == "artificial intelligence"
        assert sub.user_id == "user_123"
        assert sub.subscription_type == "topic"
        assert sub.is_active is True
        assert sub.related_topics == []

    def test_initialization_with_custom_source(
        self, mock_storage, mock_utc_now
    ):
        """Test initialization with custom CardSource."""
        source = CardSource(
            type="news_extraction",
            source_id="news_123",
            created_from="Extracted from article",
        )
        sub = TopicSubscription(
            topic="tech",
            user_id="user_123",
            source=source,
        )

        assert sub.source.type == "news_extraction"
        assert sub.source.source_id == "news_123"

    def test_initialization_creates_default_source(
        self, mock_storage, mock_utc_now
    ):
        """Test that default source is created when not provided."""
        sub = TopicSubscription(
            topic="machine learning",
            user_id="user_123",
        )

        assert sub.source.type == "news_topic"
        assert "Topic subscription" in sub.source.created_from

    def test_initialization_with_related_topics(
        self, mock_storage, mock_utc_now
    ):
        """Test initialization with related topics."""
        sub = TopicSubscription(
            topic="python",
            user_id="user_123",
            related_topics=["programming", "software development"],
        )

        assert sub.related_topics == ["programming", "software development"]

    def test_initialization_with_custom_refresh_interval(
        self, mock_storage, mock_utc_now
    ):
        """Test initialization with custom refresh interval."""
        sub = TopicSubscription(
            topic="test",
            user_id="user_123",
            refresh_interval_minutes=60,
        )

        assert sub.refresh_interval_minutes == 60

    def test_topic_history_initialized(self, mock_storage, mock_utc_now):
        """Test that topic history is initialized with original topic."""
        sub = TopicSubscription(
            topic="initial topic",
            user_id="user_123",
        )

        assert sub.topic_history == ["initial topic"]

    def test_metadata_contains_subscription_info(
        self, mock_storage, mock_utc_now
    ):
        """Test that metadata is properly populated."""
        sub = TopicSubscription(
            topic="test topic",
            user_id="user_123",
        )

        assert sub.metadata["subscription_type"] == "topic"
        assert sub.metadata["original_topic"] == "test topic"
        assert sub.metadata["is_trending"] is False
        assert sub.metadata["topic_category"] is None

    def test_activity_threshold_default(self, mock_storage, mock_utc_now):
        """Test default activity threshold is set."""
        sub = TopicSubscription(
            topic="test",
            user_id="user_123",
        )

        assert sub.activity_threshold == 3


class TestGetSubscriptionType:
    """Tests for get_subscription_type method."""

    def test_returns_topic_subscription(self, mock_storage, mock_utc_now):
        """Test returns correct subscription type identifier."""
        sub = TopicSubscription(topic="test", user_id="user_123")

        assert sub.get_subscription_type() == "topic_subscription"


class TestGenerateSearchQuery:
    """Tests for generate_search_query method."""

    def test_generates_query_for_single_topic(self, mock_storage, mock_utc_now):
        """Test query generation for single topic."""
        sub = TopicSubscription(
            topic="climate change",
            user_id="user_123",
        )

        with patch(
            "local_deep_research.news.core.utils.get_local_date_string"
        ) as mock_date:
            mock_date.return_value = "2024-01-15"
            result = sub.generate_search_query()

        assert '"climate change"' in result
        assert "news" in result.lower()

    def test_generates_query_with_related_topics(
        self, mock_storage, mock_utc_now
    ):
        """Test query includes related topics."""
        sub = TopicSubscription(
            topic="python",
            user_id="user_123",
            related_topics=["programming", "software", "coding"],
        )

        with patch(
            "local_deep_research.news.core.utils.get_local_date_string"
        ) as mock_date:
            mock_date.return_value = "2024-01-15"
            result = sub.generate_search_query()

        # Should include main topic
        assert '"python"' in result
        # Should include up to 2 related topics
        assert '"programming"' in result or '"software"' in result
        # Should use OR to combine
        assert "OR" in result

    def test_limits_related_topics_to_two(self, mock_storage, mock_utc_now):
        """Test only includes up to 2 related topics."""
        sub = TopicSubscription(
            topic="main",
            user_id="user_123",
            related_topics=["related1", "related2", "related3", "related4"],
        )

        with patch(
            "local_deep_research.news.core.utils.get_local_date_string"
        ) as mock_date:
            mock_date.return_value = "2024-01-15"
            result = sub.generate_search_query()

        # Count quoted terms (should be at most 3: main + 2 related)
        quote_count = result.count('"')
        # Each topic is quoted, so max 6 quotes (3 terms * 2 quotes each)
        assert quote_count <= 6

    def test_replaces_date_placeholder(self, mock_storage, mock_utc_now):
        """Test YYYY-MM-DD placeholder is replaced in current topic."""
        sub = TopicSubscription(
            topic="events on YYYY-MM-DD",
            user_id="user_123",
        )

        with patch(
            "local_deep_research.news.core.utils.get_local_date_string"
        ) as mock_date:
            mock_date.return_value = "2024-01-15"
            result = sub.generate_search_query()

        assert "2024-01-15" in result
        assert "YYYY-MM-DD" not in result


class TestUpdateActivity:
    """Tests for update_activity method."""

    def test_marks_trending_when_threshold_met(
        self, mock_storage, mock_utc_now
    ):
        """Test topic is marked trending when activity threshold met."""
        sub = TopicSubscription(
            topic="test",
            user_id="user_123",
        )
        sub.activity_threshold = 3

        sub.update_activity(news_count=5)

        assert sub.metadata["is_trending"] is True

    def test_marks_trending_when_significant_news(
        self, mock_storage, mock_utc_now
    ):
        """Test topic is marked trending with significant news."""
        sub = TopicSubscription(
            topic="test",
            user_id="user_123",
        )

        sub.update_activity(news_count=1, significant_news=True)

        assert sub.metadata["is_trending"] is True

    def test_removes_trending_after_inactivity(self, mock_storage):
        """Test trending is removed after 72 hours of inactivity."""
        initial_time = datetime(2024, 1, 10, 12, 0, 0)
        later_time = datetime(2024, 1, 14, 12, 0, 0)  # 4 days later

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_base:
            with patch(
                "local_deep_research.news.subscription_manager.topic_subscription.utc_now"
            ) as mock_topic:
                # Initialize at initial time
                mock_base.return_value = initial_time
                mock_topic.return_value = initial_time
                sub = TopicSubscription(topic="test", user_id="user_123")
                sub.metadata["is_trending"] = True

                # Update activity at later time with low count
                mock_topic.return_value = later_time
                sub.update_activity(news_count=1)

        assert sub.metadata["is_trending"] is False

    def test_keeps_trending_within_72_hours(self, mock_storage):
        """Test trending is kept within 72 hours even with low activity."""
        initial_time = datetime(2024, 1, 10, 12, 0, 0)
        later_time = datetime(2024, 1, 12, 12, 0, 0)  # 2 days later

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_base:
            with patch(
                "local_deep_research.news.subscription_manager.topic_subscription.utc_now"
            ) as mock_topic:
                mock_base.return_value = initial_time
                mock_topic.return_value = initial_time
                sub = TopicSubscription(topic="test", user_id="user_123")
                sub.metadata["is_trending"] = True

                mock_topic.return_value = later_time
                sub.update_activity(news_count=1)

        # Still within 72 hours, should keep trending
        assert sub.metadata["is_trending"] is True


class TestEvolveTopic:
    """Tests for evolve_topic method."""

    def test_evolve_topic_updates_current(self, mock_storage, mock_utc_now):
        """Test topic evolution updates current topic."""
        sub = TopicSubscription(
            topic="original topic",
            user_id="user_123",
        )

        sub.evolve_topic("evolved topic", reason="trend shift")

        assert sub.current_topic == "evolved topic"
        assert len(sub.topic_history) == 2

    def test_evolve_topic_records_history(self, mock_storage, mock_utc_now):
        """Test topic evolution records history."""
        sub = TopicSubscription(
            topic="first",
            user_id="user_123",
        )

        sub.evolve_topic("second", reason="reason1")
        sub.evolve_topic("third", reason="reason2")

        assert sub.topic_history == ["first", "second", "third"]

    def test_evolve_topic_no_change_for_same_topic(
        self, mock_storage, mock_utc_now
    ):
        """Test no change when evolving to same topic."""
        sub = TopicSubscription(
            topic="same topic",
            user_id="user_123",
        )

        sub.evolve_topic("same topic", reason="test")

        assert len(sub.topic_history) == 1
        assert sub.current_topic == "same topic"

    def test_evolve_topic_stores_metadata(self, mock_storage, mock_utc_now):
        """Test evolution stores metadata about change."""
        sub = TopicSubscription(
            topic="before",
            user_id="user_123",
        )

        sub.evolve_topic("after", reason="natural evolution")

        assert "last_evolution" in sub.metadata
        assert sub.metadata["last_evolution"]["from"] == "before"
        assert sub.metadata["last_evolution"]["to"] == "after"
        assert sub.metadata["last_evolution"]["reason"] == "natural evolution"


class TestAddRelatedTopic:
    """Tests for add_related_topic method."""

    def test_adds_new_related_topic(self, mock_storage, mock_utc_now):
        """Test adding a new related topic."""
        sub = TopicSubscription(
            topic="main",
            user_id="user_123",
        )

        sub.add_related_topic("related1")
        sub.add_related_topic("related2")

        assert "related1" in sub.related_topics
        assert "related2" in sub.related_topics

    def test_does_not_add_duplicate(self, mock_storage, mock_utc_now):
        """Test does not add duplicate related topic."""
        sub = TopicSubscription(
            topic="main",
            user_id="user_123",
            related_topics=["existing"],
        )

        sub.add_related_topic("existing")

        assert sub.related_topics.count("existing") == 1

    def test_does_not_add_current_topic(self, mock_storage, mock_utc_now):
        """Test does not add current topic as related."""
        sub = TopicSubscription(
            topic="main topic",
            user_id="user_123",
        )

        sub.add_related_topic("main topic")

        assert "main topic" not in sub.related_topics


class TestMergeWith:
    """Tests for merge_with method."""

    def test_merges_topics(self, mock_storage, mock_utc_now):
        """Test merging another subscription."""
        sub1 = TopicSubscription(
            topic="topic1",
            user_id="user_123",
            subscription_id="sub1",
        )
        sub2 = TopicSubscription(
            topic="topic2",
            user_id="user_123",
            subscription_id="sub2",
            related_topics=["related_a", "related_b"],
        )

        sub1.merge_with(sub2)

        assert "topic2" in sub1.related_topics
        assert "related_a" in sub1.related_topics
        assert "related_b" in sub1.related_topics

    def test_merge_stores_metadata(self, mock_storage, mock_utc_now):
        """Test merge stores metadata about merged subscription."""
        sub1 = TopicSubscription(
            topic="main",
            user_id="user_123",
        )
        sub2 = TopicSubscription(
            topic="merged",
            user_id="user_123",
            subscription_id="sub2_id",
        )

        sub1.merge_with(sub2)

        assert "merged_from" in sub1.metadata
        assert sub1.metadata["merged_from"]["topic"] == "merged"
        assert sub1.metadata["merged_from"]["subscription_id"] == "sub2_id"


class TestShouldAutoExpire:
    """Tests for should_auto_expire method."""

    def test_does_not_expire_with_recent_activity(
        self, mock_storage, mock_utc_now
    ):
        """Test does not expire with recent activity."""
        sub = TopicSubscription(
            topic="test",
            user_id="user_123",
        )
        sub.refresh_count = 5
        sub.error_count = 0

        assert sub.should_auto_expire() is False

    def test_expires_after_30_days_inactivity(self, mock_storage):
        """Test expires after 30 days of inactivity."""
        initial_time = datetime(2024, 1, 1, 12, 0, 0)
        expired_time = datetime(2024, 2, 5, 12, 0, 0)  # 35 days later

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_base:
            with patch(
                "local_deep_research.news.subscription_manager.topic_subscription.utc_now"
            ) as mock_topic:
                mock_base.return_value = initial_time
                mock_topic.return_value = initial_time
                sub = TopicSubscription(topic="test", user_id="user_123")
                sub.refresh_count = 10
                sub.error_count = 0

                mock_topic.return_value = expired_time
                result = sub.should_auto_expire()

        assert result is True

    def test_does_not_expire_with_errors(self, mock_storage, mock_utc_now):
        """Test does not expire check when there are errors."""
        sub = TopicSubscription(
            topic="test",
            user_id="user_123",
        )
        sub.refresh_count = 5
        sub.error_count = 3  # Has errors

        # Should return False because error_count > 0
        assert sub.should_auto_expire() is False


class TestGetStatistics:
    """Tests for get_statistics method."""

    def test_returns_statistics_dict(self, mock_storage, mock_utc_now):
        """Test returns proper statistics dictionary."""
        sub = TopicSubscription(
            topic="test topic",
            user_id="user_123",
        )

        stats = sub.get_statistics()

        assert "original_topic" in stats
        assert "current_topic" in stats
        assert "evolution_count" in stats
        assert "related_topics_count" in stats
        assert "is_trending" in stats
        assert "days_since_activity" in stats
        assert "total_refreshes" in stats

    def test_statistics_track_topic_evolution(self, mock_storage, mock_utc_now):
        """Test statistics track topic evolution count."""
        sub = TopicSubscription(
            topic="original",
            user_id="user_123",
        )

        sub.evolve_topic("first evolution")
        sub.evolve_topic("second evolution")

        stats = sub.get_statistics()

        assert stats["evolution_count"] == 2

    def test_statistics_count_related_topics(self, mock_storage, mock_utc_now):
        """Test statistics count related topics."""
        sub = TopicSubscription(
            topic="main",
            user_id="user_123",
            related_topics=["a", "b", "c"],
        )

        stats = sub.get_statistics()

        assert stats["related_topics_count"] == 3


class TestToDict:
    """Tests for to_dict method."""

    def test_includes_all_fields(self, mock_storage, mock_utc_now):
        """Test to_dict includes all required fields."""
        sub = TopicSubscription(
            topic="test topic",
            user_id="user_123",
            subscription_id="sub_123",
            related_topics=["related1"],
        )

        result = sub.to_dict()

        assert "id" in result
        assert "topic" in result
        assert "current_topic" in result
        assert "related_topics" in result
        assert "topic_history" in result
        assert "last_significant_activity" in result
        assert "statistics" in result

    def test_to_dict_includes_inherited_fields(
        self, mock_storage, mock_utc_now
    ):
        """Test to_dict includes fields from parent class."""
        sub = TopicSubscription(
            topic="test",
            user_id="user_123",
        )

        result = sub.to_dict()

        # From BaseSubscription
        assert "user_id" in result
        assert "source" in result
        assert "created_at" in result
        assert "is_active" in result


class TestTopicSubscriptionFactory:
    """Tests for TopicSubscriptionFactory."""

    def test_from_news_extraction_creates_subscription(
        self, mock_storage, mock_utc_now
    ):
        """Test factory creates subscription from news extraction."""
        sub = TopicSubscriptionFactory.from_news_extraction(
            user_id="user_123",
            topic="extracted topic",
            source_news_id="news_456",
            related_topics=["related1", "related2"],
        )

        assert sub.user_id == "user_123"
        assert sub.topic == "extracted topic"
        assert sub.source.type == "news_topic"
        assert sub.source.source_id == "news_456"
        assert sub.related_topics == ["related1", "related2"]

    def test_from_news_extraction_default_extraction_method(
        self, mock_storage, mock_utc_now
    ):
        """Test factory stores default extraction method in metadata."""
        sub = TopicSubscriptionFactory.from_news_extraction(
            user_id="user_123",
            topic="topic",
            source_news_id="news_123",
        )

        # Default extraction method should be "llm"
        assert sub.source.metadata["extraction_method"] == "llm"

    def test_from_user_interest_creates_subscription(
        self, mock_storage, mock_utc_now
    ):
        """Test factory creates subscription from user interest."""
        sub = TopicSubscriptionFactory.from_user_interest(
            user_id="user_123",
            topic="user's interest",
        )

        assert sub.user_id == "user_123"
        assert sub.topic == "user's interest"
        assert sub.source.type == "user_interest"
        assert "Your interest" in sub.source.created_from

    def test_from_user_interest_default_creation_method(
        self, mock_storage, mock_utc_now
    ):
        """Test factory stores default creation method in metadata."""
        sub = TopicSubscriptionFactory.from_user_interest(
            user_id="user_123",
            topic="topic",
        )

        # Default created_via should be "manual"
        assert sub.source.metadata["created_via"] == "manual"

    def test_from_user_interest_with_extra_kwargs(
        self, mock_storage, mock_utc_now
    ):
        """Test factory passes through extra kwargs."""
        sub = TopicSubscriptionFactory.from_user_interest(
            user_id="user_123",
            topic="topic",
            refresh_interval_minutes=180,
        )

        assert sub.refresh_interval_minutes == 180
