"""
Deep behavioral tests for BaseSubscription and SearchSubscription.
Tests subscription state transitions, configuration, query transformation,
error handling, and serialization.
"""

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

import pytest

# Mock SQLSubscriptionStorage before importing subscription classes
with patch(
    "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage",
    return_value=MagicMock(),
):
    from local_deep_research.news.subscription_manager.base_subscription import (
        BaseSubscription,
    )
    from local_deep_research.news.subscription_manager.search_subscription import (
        SearchSubscription,
    )

# Patch storage for all tests
_storage_patch = patch(
    "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage",
    return_value=MagicMock(),
)
_storage_patch.start()


# --- SearchSubscription init ---


class TestSearchSubscriptionInit:
    """Tests for SearchSubscription initialization."""

    def test_stores_query(self):
        sub = SearchSubscription(query="AI news", user_id="u1")
        assert sub.query == "AI news"

    def test_stores_original_query(self):
        sub = SearchSubscription(query="AI news", user_id="u1")
        assert sub.original_query == "AI news"

    def test_stores_user_id(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.user_id == "u1"

    def test_default_refresh_interval(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.refresh_interval_minutes == 360

    def test_custom_refresh_interval(self):
        sub = SearchSubscription(
            query="test", user_id="u1", refresh_interval_minutes=120
        )
        assert sub.refresh_interval_minutes == 120

    def test_default_is_active(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.is_active is True

    def test_has_id(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.id is not None
        assert len(sub.id) > 0

    def test_custom_subscription_id(self):
        sub = SearchSubscription(
            query="test", user_id="u1", subscription_id="custom-id"
        )
        assert sub.id == "custom-id"

    def test_has_created_at(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.created_at is not None

    def test_subscription_type_is_search(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.subscription_type == "search"

    def test_get_subscription_type(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.get_subscription_type() == "search_subscription"

    def test_default_transform_enabled(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.transform_to_news_query is True

    def test_transform_disabled(self):
        sub = SearchSubscription(
            query="test", user_id="u1", transform_to_news_query=False
        )
        assert sub.transform_to_news_query is False

    def test_query_history_initialized(self):
        sub = SearchSubscription(query="AI news", user_id="u1")
        assert sub.query_history == ["AI news"]

    def test_current_query_matches_original(self):
        sub = SearchSubscription(query="AI news", user_id="u1")
        assert sub.current_query == "AI news"

    def test_auto_creates_source(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.source is not None
        assert sub.source.type == "user_search"

    def test_metadata_has_subscription_type(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.metadata["subscription_type"] == "search"

    def test_metadata_has_original_query(self):
        sub = SearchSubscription(query="AI news", user_id="u1")
        assert sub.metadata["original_query"] == "AI news"

    def test_metadata_has_transform_enabled(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.metadata["transform_enabled"] is True


# --- BaseSubscription abstract ---


class TestBaseSubscriptionAbstract:
    """Tests for BaseSubscription abstract enforcement."""

    def test_cannot_instantiate_directly(self):
        with pytest.raises(TypeError):
            BaseSubscription(
                user_id="u1",
                source=None,
                query_or_topic="test",
            )


# --- Subscription state ---


class TestSubscriptionState:
    """Tests for subscription state management."""

    def test_initial_refresh_count_zero(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.refresh_count == 0

    def test_initial_error_count_zero(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.error_count == 0

    def test_initial_last_refreshed_none(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.last_refreshed is None

    def test_initial_last_error_none(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.last_error is None

    def test_next_refresh_calculated(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.next_refresh is not None

    def test_next_refresh_after_created_at(self):
        sub = SearchSubscription(query="test", user_id="u1")
        assert sub.next_refresh > sub.created_at

    def test_pause_sets_inactive(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.pause()
        assert sub.is_active is False

    def test_resume_sets_active(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.pause()
        sub.resume()
        assert sub.is_active is True

    def test_resume_resets_errors(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.error_count = 5
        sub.resume()
        assert sub.error_count == 0

    def test_should_refresh_false_when_paused(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.pause()
        assert sub.should_refresh() is False

    def test_is_due_for_refresh_alias(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.pause()
        assert sub.is_due_for_refresh() == sub.should_refresh()

    def test_on_refresh_start_sets_last_refreshed(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.on_refresh_start()
        assert sub.last_refreshed is not None

    def test_on_refresh_success_increments_count(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.on_refresh_success(results=None)
        assert sub.refresh_count == 1

    def test_on_refresh_success_resets_errors(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.error_count = 3
        sub.on_refresh_success(results=None)
        assert sub.error_count == 0

    def test_on_refresh_error_increments_error_count(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.on_refresh_error(Exception("fail"))
        assert sub.error_count == 1

    def test_on_refresh_error_stores_message(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.on_refresh_error(Exception("connection lost"))
        assert sub.last_error == "connection lost"

    def test_ten_errors_disables_subscription(self):
        sub = SearchSubscription(query="test", user_id="u1")
        for _ in range(10):
            sub.on_refresh_error(Exception("fail"))
        assert sub.is_active is False


# --- Update interval ---


class TestUpdateInterval:
    """Tests for update_interval validation."""

    def test_rejects_less_than_60(self):
        sub = SearchSubscription(query="test", user_id="u1")
        with pytest.raises(ValueError, match="at least 60"):
            sub.update_interval(30)

    def test_rejects_more_than_30_days(self):
        sub = SearchSubscription(query="test", user_id="u1")
        with pytest.raises(ValueError, match="cannot exceed"):
            sub.update_interval(60 * 24 * 31)

    def test_accepts_valid_interval(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.update_interval(120)
        assert sub.refresh_interval_minutes == 120

    def test_updates_next_refresh(self):
        sub = SearchSubscription(query="test", user_id="u1")
        old_next = sub.next_refresh
        sub.update_interval(60)
        assert sub.next_refresh != old_next

    def test_accepts_exactly_60(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.update_interval(60)
        assert sub.refresh_interval_minutes == 60

    def test_accepts_exactly_30_days(self):
        sub = SearchSubscription(query="test", user_id="u1")
        sub.update_interval(60 * 24 * 30)
        assert sub.refresh_interval_minutes == 60 * 24 * 30


# --- to_dict ---


class TestSubscriptionToDict:
    """Tests for subscription serialization."""

    def test_has_id(self):
        sub = SearchSubscription(query="test", user_id="u1")
        d = sub.to_dict()
        assert "id" in d

    def test_has_type(self):
        sub = SearchSubscription(query="test", user_id="u1")
        d = sub.to_dict()
        assert d["type"] == "search_subscription"

    def test_has_user_id(self):
        sub = SearchSubscription(query="test", user_id="u1")
        d = sub.to_dict()
        assert d["user_id"] == "u1"

    def test_has_query_or_topic(self):
        sub = SearchSubscription(query="AI news", user_id="u1")
        d = sub.to_dict()
        assert d["query_or_topic"] == "AI news"

    def test_has_refresh_interval_minutes(self):
        sub = SearchSubscription(
            query="test", user_id="u1", refresh_interval_minutes=120
        )
        d = sub.to_dict()
        assert d["refresh_interval_minutes"] == 120

    def test_has_is_active(self):
        sub = SearchSubscription(query="test", user_id="u1")
        d = sub.to_dict()
        assert d["is_active"] is True

    def test_has_refresh_count(self):
        sub = SearchSubscription(query="test", user_id="u1")
        d = sub.to_dict()
        assert d["refresh_count"] == 0

    def test_has_created_at(self):
        sub = SearchSubscription(query="test", user_id="u1")
        d = sub.to_dict()
        assert "created_at" in d

    def test_has_source_section(self):
        sub = SearchSubscription(query="test", user_id="u1")
        d = sub.to_dict()
        assert "source" in d
        assert d["source"]["type"] == "user_search"

    def test_has_original_query(self):
        sub = SearchSubscription(query="AI news", user_id="u1")
        d = sub.to_dict()
        assert d["original_query"] == "AI news"

    def test_has_statistics(self):
        sub = SearchSubscription(query="test", user_id="u1")
        d = sub.to_dict()
        assert "statistics" in d

    def test_has_last_refreshed_none(self):
        sub = SearchSubscription(query="test", user_id="u1")
        d = sub.to_dict()
        assert d["last_refreshed"] is None

    def test_has_error_count(self):
        sub = SearchSubscription(query="test", user_id="u1")
        d = sub.to_dict()
        assert d["error_count"] == 0


# --- Query transformation ---


class TestQueryTransformation:
    """Tests for search query transformation."""

    def test_generates_search_query(self):
        sub = SearchSubscription(query="AI breakthroughs", user_id="u1")
        q = sub.generate_search_query()
        assert "AI breakthroughs" in q

    def test_adds_news_context(self):
        sub = SearchSubscription(query="climate change", user_id="u1")
        q = sub.generate_search_query()
        assert "news" in q.lower() or "developments" in q.lower()

    def test_no_transform_returns_original(self):
        sub = SearchSubscription(
            query="custom query", user_id="u1", transform_to_news_query=False
        )
        q = sub.generate_search_query()
        assert q == "custom query"

    def test_doesnt_double_add_news(self):
        sub = SearchSubscription(query="AI latest news", user_id="u1")
        q = sub.generate_search_query()
        # Should return original since it already has "news" and "latest"
        assert q == "AI latest news"

    def test_tutorial_query_gets_updates(self):
        sub = SearchSubscription(query="how to deploy ML models", user_id="u1")
        q = sub.generate_search_query()
        assert "updates" in q.lower() or "developments" in q.lower()

    def test_security_query_gets_alerts(self):
        sub = SearchSubscription(query="vulnerability in log4j", user_id="u1")
        q = sub.generate_search_query()
        assert "breaking" in q.lower() or "alerts" in q.lower()

    @patch(
        "local_deep_research.news.core.utils.get_local_date_string",
        return_value="2025-06-15",
    )
    def test_replaces_date_placeholder(self, mock_date):
        sub = SearchSubscription(
            query="AI news YYYY-MM-DD",
            user_id="u1",
            transform_to_news_query=False,
        )
        q = sub.generate_search_query()
        assert "2025-06-15" in q
        assert "YYYY-MM-DD" not in q


# --- Query evolution ---


class TestQueryEvolution:
    """Tests for query evolution behavior."""

    def test_evolve_adds_terms(self):
        sub = SearchSubscription(query="AI", user_id="u1")
        sub.evolve_query("GPT-5")
        assert "GPT-5" in sub.current_query

    def test_evolve_preserves_original(self):
        sub = SearchSubscription(query="AI", user_id="u1")
        sub.evolve_query("GPT-5")
        assert sub.original_query == "AI"

    def test_evolve_appends_history(self):
        sub = SearchSubscription(query="AI", user_id="u1")
        sub.evolve_query("GPT-5")
        assert len(sub.query_history) == 2

    def test_evolve_none_does_nothing(self):
        sub = SearchSubscription(query="AI", user_id="u1")
        sub.evolve_query(None)
        assert sub.current_query == "AI"
        assert len(sub.query_history) == 1


# --- Statistics ---


class TestSubscriptionStatistics:
    """Tests for get_statistics."""

    def test_has_original_query(self):
        sub = SearchSubscription(query="AI", user_id="u1")
        stats = sub.get_statistics()
        assert stats["original_query"] == "AI"

    def test_has_current_query(self):
        sub = SearchSubscription(query="AI", user_id="u1")
        stats = sub.get_statistics()
        assert stats["current_query"] == "AI"

    def test_query_evolution_count_zero(self):
        sub = SearchSubscription(query="AI", user_id="u1")
        stats = sub.get_statistics()
        assert stats["query_evolution_count"] == 0

    def test_success_rate_zero_when_no_refreshes(self):
        sub = SearchSubscription(query="AI", user_id="u1")
        stats = sub.get_statistics()
        assert stats["success_rate"] == 0


# --- Subscription equality ---


class TestSubscriptionEquality:
    """Tests for subscription identity behavior."""

    def test_unique_ids(self):
        s1 = SearchSubscription(query="test", user_id="u1")
        s2 = SearchSubscription(query="test", user_id="u1")
        assert s1.id != s2.id

    def test_same_query_different_instances(self):
        s1 = SearchSubscription(query="same query", user_id="u1")
        s2 = SearchSubscription(query="same query", user_id="u1")
        assert s1 is not s2


# --- Next refresh calculation ---


class TestNextRefreshCalculation:
    """Tests for _calculate_next_refresh patterns."""

    def test_initial_next_refresh_uses_created_at(self):
        sub = SearchSubscription(
            query="test", user_id="u1", refresh_interval_minutes=60
        )
        expected = sub.created_at + timedelta(minutes=60)
        assert sub.next_refresh == expected

    def test_refresh_updates_next(self):
        sub = SearchSubscription(
            query="test", user_id="u1", refresh_interval_minutes=60
        )
        sub.on_refresh_start()
        sub.on_refresh_success(results=None)
        assert sub.next_refresh > sub.last_refreshed

    def test_error_backoff(self):
        sub = SearchSubscription(
            query="test", user_id="u1", refresh_interval_minutes=60
        )
        old_next = sub.next_refresh
        sub.on_refresh_error(Exception("fail"))
        # Error should push next_refresh further out
        assert sub.next_refresh > old_next
