"""
Tests for news/subscription_manager/base_subscription.py

Tests cover:
- BaseSubscription initialization
- Refresh scheduling
- Status management
- Metadata handling
"""

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


class TestBaseSubscriptionInit:
    """Tests for BaseSubscription initialization."""

    def test_base_subscription_is_abstract(self):
        """BaseSubscription cannot be instantiated directly."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )
        from abc import ABC

        assert issubclass(BaseSubscription, ABC)

    def test_base_subscription_has_required_abstract_methods(self):
        """BaseSubscription requires abstract methods."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        # Check that abstract methods are defined
        assert hasattr(BaseSubscription, "generate_search_query")
        assert hasattr(BaseSubscription, "get_subscription_type")


class TestConcreteSubscription:
    """Tests using a concrete implementation of BaseSubscription."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription",
            source_id="test-source",
            created_from="test",
        )

    def test_subscription_initialization(self, mock_storage, card_source):
        """Subscription initializes with correct attributes."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        # Create concrete implementation
        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        sub = TestSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="test query",
            refresh_interval_minutes=60,
        )

        assert sub.user_id == "user123"
        assert sub.query_or_topic == "test query"
        assert sub.refresh_interval_minutes == 60
        assert sub.is_active is True
        assert sub.refresh_count == 0
        assert sub.error_count == 0

    def test_subscription_generates_id_if_not_provided(
        self, mock_storage, card_source
    ):
        """Subscription generates ID if not provided."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        sub = TestSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.id is not None
        assert len(sub.id) > 0

    def test_subscription_uses_provided_id(self, mock_storage, card_source):
        """Subscription uses provided ID."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        sub = TestSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="custom-id-123",
        )

        assert sub.id == "custom-id-123"

    def test_default_refresh_interval(self, mock_storage, card_source):
        """Default refresh interval is 240 minutes (4 hours)."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        sub = TestSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.refresh_interval_minutes == 240


class TestRefreshScheduling:
    """Tests for refresh scheduling logic."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_new_subscription_calculates_next_refresh(
        self, mock_storage, card_source, test_subscription_class
    ):
        """New subscription calculates next refresh from created_at."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Next refresh should be approximately 60 minutes after creation
        expected = sub.created_at + timedelta(minutes=60)
        assert sub.next_refresh == expected

    def test_should_refresh_returns_false_for_new_subscription(
        self, mock_storage, card_source, test_subscription_class
    ):
        """New subscription should not need refresh immediately."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # New subscription shouldn't need refresh yet
        assert sub.should_refresh() is False

    def test_should_refresh_returns_false_when_inactive(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Inactive subscription should not refresh."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=0,  # Immediate refresh
        )
        sub.is_active = False

        assert sub.should_refresh() is False

    def test_is_due_for_refresh_alias(
        self, mock_storage, card_source, test_subscription_class
    ):
        """is_due_for_refresh is alias for should_refresh."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.is_due_for_refresh() == sub.should_refresh()


class TestCardSource:
    """Tests for CardSource dataclass."""

    def test_card_source_initialization(self):
        """CardSource initializes with required fields."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="news_item",
            source_id="src-123",
            created_from="test",
        )

        assert source.type == "news_item"
        assert source.source_id == "src-123"
        assert source.created_from == "test"

    def test_card_source_default_metadata(self):
        """CardSource has empty default metadata."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(type="test")

        assert source.metadata == {}

    def test_card_source_with_metadata(self):
        """CardSource accepts custom metadata."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="test",
            metadata={"key": "value"},
        )

        assert source.metadata == {"key": "value"}

    def test_card_source_types(self):
        """CardSource accepts various valid types."""
        from local_deep_research.news.core.base_card import CardSource

        valid_types = [
            "news_item",
            "user_search",
            "subscription",
            "news_research",
        ]

        for type_name in valid_types:
            source = CardSource(type=type_name)
            assert source.type == type_name


# =============================================================================
# Tests for on_refresh_start
# =============================================================================


class TestOnRefreshStart:
    """Tests for the on_refresh_start method."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_sets_last_refreshed_to_current_time(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_start updates last_refreshed timestamp."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Initially last_refreshed is None
        assert sub.last_refreshed is None

        # Capture time before and after
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time

            sub.on_refresh_start()

            assert sub.last_refreshed == mock_time

    def test_logs_refresh_start(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_start logs the refresh starting."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.logger"
        ) as mock_logger:
            sub.on_refresh_start()

            mock_logger.debug.assert_called_once()
            assert sub.id in mock_logger.debug.call_args[0][0]


# =============================================================================
# Tests for on_refresh_success
# =============================================================================


class TestOnRefreshSuccess:
    """Tests for the on_refresh_success method."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_increments_refresh_count(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_success increments the refresh count."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        initial_count = sub.refresh_count
        sub.on_refresh_success(results={"items": []})

        assert sub.refresh_count == initial_count + 1

    def test_recalculates_next_refresh(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_success recalculates next_refresh."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Set last_refreshed to trigger recalculation
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time
            sub.last_refreshed = mock_time

            sub.on_refresh_success(results={"items": []})

            # Next refresh should be last_refreshed + interval
            expected = mock_time + timedelta(minutes=60)
            assert sub.next_refresh == expected

    def test_resets_error_count_to_zero(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_success resets error_count to zero."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Simulate previous errors
        sub.error_count = 5

        sub.on_refresh_success(results={"items": []})

        assert sub.error_count == 0


# =============================================================================
# Tests for on_refresh_error (CRITICAL)
# =============================================================================


class TestOnRefreshError:
    """Tests for the on_refresh_error method - critical error handling logic."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_increments_error_count(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error increments the error count."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        initial_count = sub.error_count
        sub.on_refresh_error(Exception("Test error"))

        assert sub.error_count == initial_count + 1

    def test_sets_last_error_message(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error stores the error message."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        error = Exception("Connection timeout")
        sub.on_refresh_error(error)

        assert sub.last_error == "Connection timeout"

    def test_exponential_backoff_calculation(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error applies exponential backoff for next_refresh."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,  # 1 hour base interval
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time

            # First error: backoff = 60 * 2^1 = 120 minutes
            sub.on_refresh_error(Exception("Error 1"))
            expected_backoff = 60 * (2**1)
            expected = mock_time + timedelta(minutes=expected_backoff)
            assert sub.next_refresh == expected

            # Second error: backoff = 60 * 2^2 = 240 minutes
            sub.on_refresh_error(Exception("Error 2"))
            expected_backoff = 60 * (2**2)
            expected = mock_time + timedelta(minutes=expected_backoff)
            assert sub.next_refresh == expected

    def test_backoff_capped_at_one_week(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error caps backoff at 1 week (10080 minutes)."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=1440,  # 1 day = 1440 minutes
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time

            # Simulate many errors to hit the cap
            for _ in range(10):
                sub.on_refresh_error(Exception("Error"))

            # Max backoff is 7 days = 7 * 24 * 60 = 10080 minutes
            max_backoff = 24 * 60 * 7  # 10080 minutes
            expected = mock_time + timedelta(minutes=max_backoff)
            assert sub.next_refresh == expected

    def test_disables_subscription_after_10_errors(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error disables subscription after 10 consecutive errors."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.is_active is True

        # Simulate 10 errors
        for i in range(10):
            sub.on_refresh_error(Exception(f"Error {i + 1}"))

        assert sub.is_active is False
        assert sub.error_count == 10

    def test_stays_active_with_fewer_than_10_errors(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error keeps subscription active with < 10 errors."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Simulate 9 errors (just under threshold)
        for i in range(9):
            sub.on_refresh_error(Exception(f"Error {i + 1}"))

        assert sub.is_active is True
        assert sub.error_count == 9


# =============================================================================
# Tests for pause and resume
# =============================================================================


class TestPauseResume:
    """Tests for pause and resume methods."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_pause_sets_inactive(
        self, mock_storage, card_source, test_subscription_class
    ):
        """pause() sets is_active to False."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.is_active is True

        sub.pause()

        assert sub.is_active is False

    def test_resume_sets_active(
        self, mock_storage, card_source, test_subscription_class
    ):
        """resume() sets is_active to True."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )
        sub.is_active = False

        sub.resume()

        assert sub.is_active is True

    def test_resume_resets_error_count(
        self, mock_storage, card_source, test_subscription_class
    ):
        """resume() resets error_count to zero."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )
        sub.error_count = 5

        sub.resume()

        assert sub.error_count == 0

    def test_resume_recalculates_next_refresh(
        self, mock_storage, card_source, test_subscription_class
    ):
        """resume() recalculates next_refresh."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )
        sub.is_active = False

        # Set last_refreshed for calculation
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time
            sub.last_refreshed = mock_time

            sub.resume()

            expected = mock_time + timedelta(minutes=60)
            assert sub.next_refresh == expected


# =============================================================================
# Tests for update_interval
# =============================================================================


class TestUpdateInterval:
    """Tests for the update_interval method."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_updates_interval_value(
        self, mock_storage, card_source, test_subscription_class
    ):
        """update_interval stores the new interval value."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        sub.update_interval(120)

        assert sub.refresh_interval_minutes == 120

    def test_recalculates_next_refresh(
        self, mock_storage, card_source, test_subscription_class
    ):
        """update_interval recalculates next_refresh."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time
            sub.last_refreshed = mock_time

            sub.update_interval(180)

            expected = mock_time + timedelta(minutes=180)
            assert sub.next_refresh == expected

    def test_raises_error_below_60_minutes(
        self, mock_storage, card_source, test_subscription_class
    ):
        """update_interval raises ValueError for intervals < 60 minutes."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        with pytest.raises(ValueError) as exc_info:
            sub.update_interval(59)

        assert "at least 60 minutes" in str(exc_info.value)

    def test_raises_error_above_30_days(
        self, mock_storage, card_source, test_subscription_class
    ):
        """update_interval raises ValueError for intervals > 30 days."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        max_minutes = 60 * 24 * 30  # 30 days
        with pytest.raises(ValueError) as exc_info:
            sub.update_interval(max_minutes + 1)

        assert "cannot exceed 30 days" in str(exc_info.value)

    def test_accepts_boundary_values(
        self, mock_storage, card_source, test_subscription_class
    ):
        """update_interval accepts exactly 60 minutes and 30 days."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Minimum boundary: 60 minutes (1 hour)
        sub.update_interval(60)
        assert sub.refresh_interval_minutes == 60

        # Maximum boundary: 30 days
        max_minutes = 60 * 24 * 30
        sub.update_interval(max_minutes)
        assert sub.refresh_interval_minutes == max_minutes


# =============================================================================
# Tests for save
# =============================================================================


class TestSave:
    """Tests for the save method."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock_instance = MagicMock()
            mock_instance.create.return_value = "saved-sub-id"
            mock.return_value = mock_instance
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription",
            source_id="src-123",
            created_from="test_origin",
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test_type"

        return TestSubscription

    def test_creates_storage_with_correct_data(
        self, mock_storage, card_source, test_subscription_class
    ):
        """save() creates storage record with all required fields."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test query",
            refresh_interval_minutes=120,
            subscription_id="sub-123",
        )

        sub.save()

        # Verify storage.create was called
        sub.storage.create.assert_called_once()
        call_data = sub.storage.create.call_args[0][0]

        assert call_data["id"] == "sub-123"
        assert call_data["user_id"] == "user123"
        assert call_data["query_or_topic"] == "test query"
        assert call_data["refresh_interval_minutes"] == 120
        assert call_data["subscription_type"] == "test_type"
        assert call_data["is_active"] is True

    def test_includes_source_attributes(
        self, mock_storage, card_source, test_subscription_class
    ):
        """save() includes all source attributes in the data."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.save()

        call_data = sub.storage.create.call_args[0][0]
        assert call_data["source_type"] == "subscription"
        assert call_data["source_id"] == "src-123"
        assert call_data["created_from"] == "test_origin"

    def test_includes_timestamps(
        self, mock_storage, card_source, test_subscription_class
    ):
        """save() includes created_at and next_refresh timestamps."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.save()

        call_data = sub.storage.create.call_args[0][0]
        assert "created_at" in call_data
        assert "next_refresh" in call_data
        assert call_data["created_at"] == sub.created_at
        assert call_data["next_refresh"] == sub.next_refresh

    def test_returns_subscription_id(
        self, mock_storage, card_source, test_subscription_class
    ):
        """save() returns the subscription ID from storage."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        result = sub.save()

        assert result == "saved-sub-id"


# =============================================================================
# Tests for mark_refreshed
# =============================================================================


class TestMarkRefreshed:
    """Tests for the mark_refreshed method."""

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

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_updates_last_refreshed(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed updates last_refreshed timestamp."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time

            sub.mark_refreshed(results_count=5)

            assert sub.last_refreshed == mock_time

    def test_recalculates_next_refresh(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed recalculates next_refresh."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time

            sub.mark_refreshed(results_count=5)

            expected = mock_time + timedelta(minutes=60)
            assert sub.next_refresh == expected

    def test_increments_refresh_count(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed increments refresh_count."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        initial_count = sub.refresh_count
        sub.mark_refreshed(results_count=5)

        assert sub.refresh_count == initial_count + 1

    def test_calls_storage_methods(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed calls storage.update_refresh_time and increment_stats."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="sub-123",
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time

            sub.mark_refreshed(results_count=10)

            sub.storage.update_refresh_time.assert_called_once_with(
                "sub-123", mock_time, sub.next_refresh
            )
            sub.storage.increment_stats.assert_called_once_with("sub-123", 10)


# =============================================================================
# Tests for to_dict
# =============================================================================


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

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription",
            source_id="src-123",
            created_from="test",
            metadata={"extra": "data"},
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test_type"

        return TestSubscription

    def test_includes_all_fields(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict includes all expected fields."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test query",
            refresh_interval_minutes=120,
            subscription_id="sub-123",
        )
        sub.metadata = {"custom": "value"}

        result = sub.to_dict()

        assert result["id"] == "sub-123"
        assert result["type"] == "test_type"
        assert result["user_id"] == "user123"
        assert result["query_or_topic"] == "test query"
        assert result["refresh_interval_minutes"] == 120
        assert result["is_active"] is True
        assert result["refresh_count"] == 0
        assert result["error_count"] == 0
        assert result["last_error"] is None
        assert result["metadata"] == {"custom": "value"}

        # Source nested dict
        assert result["source"]["type"] == "subscription"
        assert result["source"]["source_id"] == "src-123"
        assert result["source"]["created_from"] == "test"
        assert result["source"]["metadata"] == {"extra": "data"}

    def test_handles_none_last_refreshed(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict handles None last_refreshed correctly."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # last_refreshed is None by default
        result = sub.to_dict()

        assert result["last_refreshed"] is None

    def test_formats_datetimes_as_iso(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict formats datetime fields as ISO strings."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Set a known last_refreshed time
        sub.last_refreshed = datetime(2024, 1, 15, 12, 30, 45)

        result = sub.to_dict()

        # Check ISO format
        assert result["created_at"].endswith("Z") or "T" in result["created_at"]
        assert (
            result["next_refresh"].endswith("Z")
            or "T" in result["next_refresh"]
        )
        assert result["last_refreshed"] == "2024-01-15T12:30:45"


# =============================================================================
# Tests for _calculate_next_refresh
# =============================================================================


class TestCalculateNextRefresh:
    """Tests for the _calculate_next_refresh method."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_uses_created_at_when_never_refreshed(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Next refresh uses created_at when last_refreshed is None."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=120,
        )

        # last_refreshed is None by default
        assert sub.last_refreshed is None

        # next_refresh should be created_at + interval
        expected = sub.created_at + timedelta(minutes=120)
        assert sub.next_refresh == expected

    def test_uses_last_refreshed_when_available(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Next refresh uses last_refreshed when available."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Set last_refreshed
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time
            sub.last_refreshed = mock_time

            # Recalculate
            result = sub._calculate_next_refresh()

            expected = mock_time + timedelta(minutes=60)
            assert result == expected


# =============================================================================
# Tests for should_refresh edge cases
# =============================================================================


class TestShouldRefreshEdgeCases:
    """Additional edge case tests for should_refresh."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_returns_true_when_past_next_refresh(
        self, mock_storage, card_source, test_subscription_class
    ):
        """should_refresh returns True when current time is past next_refresh."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Set next_refresh to the past
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_utc.return_value = datetime(2024, 1, 15, 14, 0, 0)
            sub.next_refresh = datetime(2024, 1, 15, 12, 0, 0)  # 2 hours ago

            assert sub.should_refresh() is True

    def test_returns_true_at_exact_refresh_time(
        self, mock_storage, card_source, test_subscription_class
    ):
        """should_refresh returns True when current time equals next_refresh."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        exact_time = datetime(2024, 1, 15, 12, 0, 0)
        sub.next_refresh = exact_time

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_utc.return_value = exact_time

            assert sub.should_refresh() is True

    def test_returns_false_before_next_refresh(
        self, mock_storage, card_source, test_subscription_class
    ):
        """should_refresh returns False when current time is before next_refresh."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.next_refresh = datetime(2024, 1, 15, 14, 0, 0)

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_utc.return_value = datetime(
                2024, 1, 15, 12, 0, 0
            )  # 2 hours before

            assert sub.should_refresh() is False


# =============================================================================
# Tests for logging behavior
# =============================================================================


class TestLoggingBehavior:
    """Tests for logging in various methods."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_pause_logs_info(
        self, mock_storage, card_source, test_subscription_class
    ):
        """pause() logs an info message."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="sub-123",
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.logger"
        ) as mock_logger:
            sub.pause()

            mock_logger.info.assert_called_once()
            assert "sub-123" in mock_logger.info.call_args[0][0]
            assert "paused" in mock_logger.info.call_args[0][0]

    def test_resume_logs_info(
        self, mock_storage, card_source, test_subscription_class
    ):
        """resume() logs an info message."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="sub-456",
        )
        sub.is_active = False

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.logger"
        ) as mock_logger:
            sub.resume()

            mock_logger.info.assert_called_once()
            assert "sub-456" in mock_logger.info.call_args[0][0]
            assert "resumed" in mock_logger.info.call_args[0][0]

    def test_update_interval_logs_info(
        self, mock_storage, card_source, test_subscription_class
    ):
        """update_interval() logs an info message."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="sub-789",
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.logger"
        ) as mock_logger:
            sub.update_interval(180)

            mock_logger.info.assert_called_once()
            assert "sub-789" in mock_logger.info.call_args[0][0]
            assert "180" in mock_logger.info.call_args[0][0]

    def test_on_refresh_success_logs_debug(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_success() logs a debug message."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="sub-success",
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.logger"
        ) as mock_logger:
            sub.on_refresh_success(results={"items": []})

            mock_logger.debug.assert_called_once()
            assert "sub-success" in mock_logger.debug.call_args[0][0]

    def test_on_refresh_error_logs_error(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error() logs an error message."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="sub-error",
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.logger"
        ) as mock_logger:
            sub.on_refresh_error(Exception("Test failure"))

            mock_logger.error.assert_called_once()
            assert "sub-error" in mock_logger.error.call_args[0][0]

    def test_on_refresh_error_logs_warning_on_disable(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error() logs warning when disabling subscription."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="sub-disable",
        )

        # Pre-set error count to 9
        sub.error_count = 9

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.logger"
        ) as mock_logger:
            sub.on_refresh_error(Exception("Final error"))

            mock_logger.warning.assert_called_once()
            assert "sub-disable" in mock_logger.warning.call_args[0][0]
            assert "disabled" in mock_logger.warning.call_args[0][0]

    def test_mark_refreshed_logs_debug(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed() logs a debug message with results count."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="sub-marked",
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.logger"
        ) as mock_logger:
            sub.mark_refreshed(results_count=42)

            mock_logger.debug.assert_called_once()
            assert "sub-marked" in mock_logger.debug.call_args[0][0]
            assert "42" in mock_logger.debug.call_args[0][0]


# =============================================================================
# Tests for error handling edge cases
# =============================================================================


class TestErrorHandlingEdgeCases:
    """Additional tests for error handling edge cases."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_handles_exception_with_empty_message(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error handles exception with empty message."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_error(Exception(""))

        assert sub.last_error == ""
        assert sub.error_count == 1

    def test_handles_exception_with_unicode_message(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error handles exception with unicode characters."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_error(Exception("Error: 日本語 émoji 🚀"))

        assert "日本語" in sub.last_error
        assert "🚀" in sub.last_error
        assert sub.error_count == 1

    def test_handles_nested_exception(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_error handles nested/chained exceptions."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        try:
            try:
                raise ValueError("Inner error")
            except ValueError as e:
                raise RuntimeError("Outer error") from e
        except RuntimeError as outer:
            sub.on_refresh_error(outer)

        assert "Outer error" in sub.last_error
        assert sub.error_count == 1

    def test_backoff_at_first_error(
        self, mock_storage, card_source, test_subscription_class
    ):
        """First error doubles the interval."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=100,
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time

            sub.on_refresh_error(Exception("Error"))

            # First error: 100 * 2^1 = 200 minutes
            expected = mock_time + timedelta(minutes=200)
            assert sub.next_refresh == expected

    def test_backoff_at_fifth_error(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Fifth error applies 2^5 multiplier."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Pre-set 4 errors
        sub.error_count = 4

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time

            sub.on_refresh_error(Exception("Error 5"))

            # 5th error: 60 * 2^5 = 60 * 32 = 1920 minutes
            expected = mock_time + timedelta(minutes=1920)
            assert sub.next_refresh == expected


# =============================================================================
# Tests for metadata handling
# =============================================================================


class TestMetadataHandling:
    """Tests for metadata dictionary handling."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_metadata_is_empty_dict_by_default(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Metadata is an empty dict by default."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.metadata == {}
        assert isinstance(sub.metadata, dict)

    def test_metadata_can_be_modified(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Metadata can be modified after creation."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.metadata["key1"] = "value1"
        sub.metadata["key2"] = {"nested": "data"}

        assert sub.metadata["key1"] == "value1"
        assert sub.metadata["key2"]["nested"] == "data"

    def test_metadata_included_in_save(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Metadata is included when saving to storage."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )
        sub.metadata = {"custom_field": "custom_value", "count": 42}

        sub.save()

        call_data = sub.storage.create.call_args[0][0]
        assert call_data["metadata"] == {
            "custom_field": "custom_value",
            "count": 42,
        }

    def test_metadata_included_in_to_dict(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Metadata is included in to_dict output."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )
        sub.metadata = {"feature_flag": True}

        result = sub.to_dict()

        assert result["metadata"] == {"feature_flag": True}


# =============================================================================
# Tests for special input handling
# =============================================================================


class TestSpecialInputHandling:
    """Tests for handling special characters and edge case inputs."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_handles_unicode_query(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Handles unicode characters in query_or_topic."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="日本語 news 新闻 émoji 🎉",
        )

        assert sub.query_or_topic == "日本語 news 新闻 émoji 🎉"
        assert sub.generate_search_query() == "日本語 news 新闻 émoji 🎉"

    def test_handles_empty_query(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Handles empty string as query_or_topic."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="",
        )

        assert sub.query_or_topic == ""
        assert sub.generate_search_query() == ""

    def test_handles_very_long_query(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Handles very long query_or_topic."""
        long_query = "a" * 10000

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic=long_query,
        )

        assert sub.query_or_topic == long_query
        assert len(sub.query_or_topic) == 10000

    def test_handles_special_characters_in_user_id(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Handles special characters in user_id."""
        sub = test_subscription_class(
            user_id="user@domain.com",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.user_id == "user@domain.com"

    def test_to_dict_with_complex_metadata(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict handles complex nested metadata."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )
        sub.metadata = {
            "list": [1, 2, 3],
            "nested": {"deep": {"value": True}},
            "null_value": None,
        }

        result = sub.to_dict()

        assert result["metadata"]["list"] == [1, 2, 3]
        assert result["metadata"]["nested"]["deep"]["value"] is True
        assert result["metadata"]["null_value"] is None


# =============================================================================
# Integration tests
# =============================================================================


class TestSubscriptionIntegration:
    """Integration tests for subscription lifecycle."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock_instance = MagicMock()
            mock_instance.create.return_value = "created-id"
            mock.return_value = mock_instance
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription", source_id="src-1")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return f"news:{self.query_or_topic}"

            def get_subscription_type(self):
                return "news_search"

        return TestSubscription

    def test_full_success_lifecycle(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Test complete successful refresh lifecycle."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="AI news",
            refresh_interval_minutes=60,
        )

        # Initial state
        assert sub.refresh_count == 0
        assert sub.error_count == 0
        assert sub.is_active is True

        # Start refresh
        sub.on_refresh_start()
        assert sub.last_refreshed is not None

        # Success
        sub.on_refresh_success(results={"count": 10})
        assert sub.refresh_count == 1
        assert sub.error_count == 0

        # Another success
        sub.on_refresh_success(results={"count": 5})
        assert sub.refresh_count == 2

    def test_error_recovery_lifecycle(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Test error followed by successful recovery."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Initial errors
        sub.on_refresh_error(Exception("Error 1"))
        sub.on_refresh_error(Exception("Error 2"))
        assert sub.error_count == 2
        assert sub.is_active is True

        # Recovery
        sub.on_refresh_success(results={})
        assert sub.error_count == 0  # Reset on success
        assert sub.refresh_count == 1
        assert sub.is_active is True

    def test_pause_resume_lifecycle(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Test pause and resume cycle."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Add some errors
        sub.on_refresh_error(Exception("Error"))
        assert sub.error_count == 1

        # Pause
        sub.pause()
        assert sub.is_active is False
        assert sub.should_refresh() is False

        # Resume resets errors
        sub.resume()
        assert sub.is_active is True
        assert sub.error_count == 0

    def test_generate_search_query_returns_formatted_query(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Test that concrete implementation returns formatted query."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="technology",
        )

        query = sub.generate_search_query()
        assert query == "news:technology"

    def test_save_and_to_dict_consistency(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Verify save and to_dict contain consistent data."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="sub-123",
        )

        sub.save()
        save_data = sub.storage.create.call_args[0][0]
        dict_data = sub.to_dict()

        # Key fields should match
        assert save_data["id"] == dict_data["id"]
        assert save_data["user_id"] == dict_data["user_id"]
        assert save_data["query_or_topic"] == dict_data["query_or_topic"]
        assert save_data["subscription_type"] == dict_data["type"]


# =============================================================================
# Parameterized tests for update_interval
# =============================================================================


class TestUpdateIntervalParameterized:
    """Parameterized tests for update_interval validation."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    @pytest.mark.parametrize(
        "invalid_interval",
        [
            0,
            1,
            30,
            59,
            -1,
            -100,
        ],
    )
    def test_rejects_intervals_below_minimum(
        self,
        mock_storage,
        card_source,
        test_subscription_class,
        invalid_interval,
    ):
        """update_interval rejects intervals below 60 minutes."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        with pytest.raises(ValueError) as exc_info:
            sub.update_interval(invalid_interval)

        assert "at least 60 minutes" in str(exc_info.value)

    @pytest.mark.parametrize(
        "invalid_interval",
        [
            43201,  # 30 days + 1 minute
            50000,
            100000,
            60 * 24 * 31,  # 31 days
            60 * 24 * 365,  # 1 year
        ],
    )
    def test_rejects_intervals_above_maximum(
        self,
        mock_storage,
        card_source,
        test_subscription_class,
        invalid_interval,
    ):
        """update_interval rejects intervals above 30 days."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        with pytest.raises(ValueError) as exc_info:
            sub.update_interval(invalid_interval)

        assert "cannot exceed 30 days" in str(exc_info.value)

    @pytest.mark.parametrize(
        "valid_interval",
        [
            60,  # Minimum: 1 hour
            120,  # 2 hours
            240,  # 4 hours (default)
            480,  # 8 hours
            1440,  # 1 day
            10080,  # 1 week
            20160,  # 2 weeks
            43200,  # Maximum: 30 days
        ],
    )
    def test_accepts_valid_intervals(
        self, mock_storage, card_source, test_subscription_class, valid_interval
    ):
        """update_interval accepts valid intervals within range."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.update_interval(valid_interval)

        assert sub.refresh_interval_minutes == valid_interval


# =============================================================================
# Parameterized tests for exponential backoff
# =============================================================================


class TestExponentialBackoffParameterized:
    """Parameterized tests for exponential backoff calculation."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    @pytest.mark.parametrize(
        "error_num,expected_multiplier",
        [
            (1, 2),  # 2^1 = 2
            (2, 4),  # 2^2 = 4
            (3, 8),  # 2^3 = 8
            (4, 16),  # 2^4 = 16
            (5, 32),  # 2^5 = 32
        ],
    )
    def test_backoff_multiplier_by_error_count(
        self,
        mock_storage,
        card_source,
        test_subscription_class,
        error_num,
        expected_multiplier,
    ):
        """Verify backoff multiplier doubles with each error."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Set error count to N-1, then trigger error N
        sub.error_count = error_num - 1

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time

            sub.on_refresh_error(Exception(f"Error {error_num}"))

            expected_minutes = min(60 * expected_multiplier, 10080)
            expected = mock_time + timedelta(minutes=expected_minutes)
            assert sub.next_refresh == expected


# =============================================================================
# Tests for concurrent state changes
# =============================================================================


class TestStateConsistency:
    """Tests for state consistency during operations."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_multiple_rapid_errors(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Multiple rapid errors accumulate correctly."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        for i in range(5):
            sub.on_refresh_error(Exception(f"Error {i}"))

        assert sub.error_count == 5
        assert sub.is_active is True  # Still under 10

    def test_error_then_success_then_error(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Error count resets on success, then accumulates again."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # First errors
        sub.on_refresh_error(Exception("Error 1"))
        sub.on_refresh_error(Exception("Error 2"))
        assert sub.error_count == 2

        # Success resets
        sub.on_refresh_success(results={})
        assert sub.error_count == 0

        # New errors start from 0
        sub.on_refresh_error(Exception("Error 3"))
        assert sub.error_count == 1

    def test_pause_does_not_reset_errors(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Pause preserves error count."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_error(Exception("Error"))
        sub.on_refresh_error(Exception("Error"))
        assert sub.error_count == 2

        sub.pause()
        assert sub.error_count == 2  # Not reset by pause

    def test_update_interval_preserves_other_state(
        self, mock_storage, card_source, test_subscription_class
    ):
        """update_interval only changes interval, not other state."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Modify state
        sub.on_refresh_error(Exception("Error"))
        sub.refresh_count = 5
        sub.metadata["key"] = "value"

        original_error_count = sub.error_count
        original_refresh_count = sub.refresh_count
        original_metadata = sub.metadata.copy()

        sub.update_interval(120)

        assert sub.error_count == original_error_count
        assert sub.refresh_count == original_refresh_count
        assert sub.metadata == original_metadata
        assert sub.refresh_interval_minutes == 120


# =============================================================================
# Tests for datetime edge cases
# =============================================================================


class TestDatetimeEdgeCases:
    """Tests for datetime handling edge cases."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_created_at_is_set_on_init(
        self, mock_storage, card_source, test_subscription_class
    ):
        """created_at is set during initialization."""
        from datetime import datetime

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 6, 15, 10, 30, 0)
            mock_utc.return_value = mock_time

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
            )

            assert sub.created_at == mock_time

    def test_next_refresh_calculated_from_created_at(
        self, mock_storage, card_source, test_subscription_class
    ):
        """next_refresh is calculated from created_at + interval."""
        from datetime import datetime

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 6, 15, 10, 0, 0)
            mock_utc.return_value = mock_time

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=120,
            )

            expected = datetime(2024, 6, 15, 12, 0, 0)  # +2 hours
            assert sub.next_refresh == expected

    def test_to_dict_datetime_format_consistency(
        self, mock_storage, card_source, test_subscription_class
    ):
        """All datetime fields use consistent ISO format."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )
        sub.last_refreshed = datetime(2024, 1, 15, 12, 0, 0)

        result = sub.to_dict()

        # All datetime fields should contain 'T' separator
        assert "T" in result["created_at"]
        assert "T" in result["next_refresh"]
        assert "T" in result["last_refreshed"]


# =============================================================================
# Tests for source attribute handling
# =============================================================================


class TestSourceAttributeHandling:
    """Tests for CardSource attribute handling in subscriptions."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_source_with_all_fields(
        self, mock_storage, test_subscription_class
    ):
        """Source with all fields is preserved correctly."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="news_item",
            source_id="src-abc-123",
            created_from="user_search",
            metadata={"query": "test", "page": 1},
        )

        sub = test_subscription_class(
            user_id="user123",
            source=source,
            query_or_topic="test",
        )

        assert sub.source.type == "news_item"
        assert sub.source.source_id == "src-abc-123"
        assert sub.source.created_from == "user_search"
        assert sub.source.metadata == {"query": "test", "page": 1}

    def test_source_with_minimal_fields(
        self, mock_storage, test_subscription_class
    ):
        """Source with only required fields works correctly."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(type="subscription")

        sub = test_subscription_class(
            user_id="user123",
            source=source,
            query_or_topic="test",
        )

        assert sub.source.type == "subscription"
        assert sub.source.source_id is None
        # created_from defaults to empty string, not None
        assert sub.source.created_from == ""
        assert sub.source.metadata == {}

    def test_source_in_save_data(self, mock_storage, test_subscription_class):
        """Source attributes are correctly included in save data."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="api_import",
            source_id="api-123",
            created_from="external_api",
        )

        sub = test_subscription_class(
            user_id="user123",
            source=source,
            query_or_topic="test",
        )

        sub.save()

        call_data = sub.storage.create.call_args[0][0]
        assert call_data["source_type"] == "api_import"
        assert call_data["source_id"] == "api-123"
        assert call_data["created_from"] == "external_api"

    def test_source_in_to_dict(self, mock_storage, test_subscription_class):
        """Source is correctly serialized in to_dict."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="recommendation",
            source_id="rec-456",
            created_from="recommender",
            metadata={"algorithm": "collaborative"},
        )

        sub = test_subscription_class(
            user_id="user123",
            source=source,
            query_or_topic="test",
        )

        result = sub.to_dict()

        assert result["source"]["type"] == "recommendation"
        assert result["source"]["source_id"] == "rec-456"
        assert result["source"]["created_from"] == "recommender"
        assert result["source"]["metadata"] == {"algorithm": "collaborative"}


# =============================================================================
# Tests for storage interaction patterns
# =============================================================================


class TestStorageInteractionPatterns:
    """Tests for storage method call patterns."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage with tracking."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock_instance = MagicMock()
            mock_instance.create.return_value = "created-id"
            mock.return_value = mock_instance
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_save_called_once(
        self, mock_storage, card_source, test_subscription_class
    ):
        """save() calls storage.create exactly once."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.save()
        sub.save()  # Second call

        # Each save creates a new record
        assert sub.storage.create.call_count == 2

    def test_mark_refreshed_calls_both_storage_methods(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed calls update_refresh_time and increment_stats."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.mark_refreshed(results_count=10)

        sub.storage.update_refresh_time.assert_called_once()
        sub.storage.increment_stats.assert_called_once()

    def test_mark_refreshed_with_zero_results(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed works with zero results."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="sub-123",
        )

        sub.mark_refreshed(results_count=0)

        sub.storage.increment_stats.assert_called_once_with("sub-123", 0)

    def test_storage_not_called_on_state_changes(
        self, mock_storage, card_source, test_subscription_class
    ):
        """State changes (pause, resume, etc.) don't call storage."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Reset mock to clear init calls
        sub.storage.reset_mock()

        sub.pause()
        sub.resume()
        sub.update_interval(120)
        sub.on_refresh_start()
        sub.on_refresh_success(results={})
        sub.on_refresh_error(Exception("test"))

        # None of these should call create
        sub.storage.create.assert_not_called()


# =============================================================================
# Tests for refresh lifecycle sequences
# =============================================================================


class TestRefreshLifecycleSequences:
    """Tests for complete refresh lifecycle sequences."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_start_then_success_sequence(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_start followed by on_refresh_success is valid."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_start()
        initial_last_refreshed = sub.last_refreshed

        sub.on_refresh_success(results={"items": [1, 2, 3]})

        assert sub.last_refreshed == initial_last_refreshed
        assert sub.refresh_count == 1
        assert sub.error_count == 0

    def test_start_then_error_sequence(
        self, mock_storage, card_source, test_subscription_class
    ):
        """on_refresh_start followed by on_refresh_error is valid."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_start()
        initial_last_refreshed = sub.last_refreshed

        sub.on_refresh_error(Exception("Failed"))

        assert sub.last_refreshed == initial_last_refreshed
        assert sub.refresh_count == 0
        assert sub.error_count == 1

    def test_multiple_success_cycles(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Multiple successful refresh cycles accumulate correctly."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        for i in range(5):
            sub.on_refresh_start()
            sub.on_refresh_success(results={"cycle": i})

        assert sub.refresh_count == 5
        assert sub.error_count == 0

    def test_alternating_success_and_error(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Alternating success and error maintains correct counts."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Success resets errors
        sub.on_refresh_error(Exception("Error 1"))
        assert sub.error_count == 1

        sub.on_refresh_success(results={})
        assert sub.error_count == 0
        assert sub.refresh_count == 1

        sub.on_refresh_error(Exception("Error 2"))
        sub.on_refresh_error(Exception("Error 3"))
        assert sub.error_count == 2

        sub.on_refresh_success(results={})
        assert sub.error_count == 0
        assert sub.refresh_count == 2


# =============================================================================
# Tests for subscription type behavior
# =============================================================================


class TestSubscriptionTypeBehavior:
    """Tests for subscription type handling."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    def test_different_subscription_types(self, mock_storage, card_source):
        """Different subscription types are correctly identified."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class NewsSubscription(BaseSubscription):
            def generate_search_query(self):
                return f"news:{self.query_or_topic}"

            def get_subscription_type(self):
                return "news_feed"

        class TopicSubscription(BaseSubscription):
            def generate_search_query(self):
                return f"topic:{self.query_or_topic}"

            def get_subscription_type(self):
                return "topic_tracker"

        news_sub = NewsSubscription(
            user_id="user123", source=card_source, query_or_topic="tech"
        )
        topic_sub = TopicSubscription(
            user_id="user123", source=card_source, query_or_topic="AI"
        )

        assert news_sub.get_subscription_type() == "news_feed"
        assert topic_sub.get_subscription_type() == "topic_tracker"

    def test_subscription_type_in_save(self, mock_storage, card_source):
        """Subscription type is correctly saved."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class CustomSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "custom_type_v2"

        sub = CustomSubscription(
            user_id="user123", source=card_source, query_or_topic="test"
        )
        sub.save()

        call_data = sub.storage.create.call_args[0][0]
        assert call_data["subscription_type"] == "custom_type_v2"

    def test_subscription_type_in_to_dict(self, mock_storage, card_source):
        """Subscription type appears correctly in to_dict."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class AlertSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "alert_v1"

        sub = AlertSubscription(
            user_id="user123", source=card_source, query_or_topic="test"
        )
        result = sub.to_dict()

        assert result["type"] == "alert_v1"


# =============================================================================
# Tests for edge case combinations
# =============================================================================


class TestEdgeCaseCombinations:
    """Tests for combinations of edge cases."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_disabled_subscription_can_be_resumed(
        self, mock_storage, card_source, test_subscription_class
    ):
        """A subscription disabled by errors can be resumed."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Disable via errors
        for _ in range(10):
            sub.on_refresh_error(Exception("Error"))

        assert sub.is_active is False
        assert sub.error_count == 10

        # Resume resets everything
        sub.resume()

        assert sub.is_active is True
        assert sub.error_count == 0

    def test_paused_subscription_accumulates_no_errors(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Paused subscription can still receive error calls but should_refresh is False."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.pause()
        assert sub.should_refresh() is False

        # Errors can still be recorded
        sub.on_refresh_error(Exception("Error while paused"))
        assert sub.error_count == 1

    def test_interval_update_after_errors(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Updating interval doesn't affect error count."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_error(Exception("Error"))
        sub.on_refresh_error(Exception("Error"))
        assert sub.error_count == 2

        sub.update_interval(120)

        assert sub.error_count == 2  # Unchanged
        assert sub.refresh_interval_minutes == 120

    def test_maximum_errors_with_minimum_interval(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Test 10 errors with minimum interval (60 min)."""
        from datetime import datetime

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            mock_time = datetime(2024, 1, 15, 12, 0, 0)
            mock_utc.return_value = mock_time

            # 10 errors to disable
            for _ in range(10):
                sub.on_refresh_error(Exception("Error"))

            assert sub.is_active is False
            assert sub.error_count == 10

            # Backoff should be capped at 1 week
            max_minutes = 24 * 60 * 7  # 10080
            expected = mock_time + timedelta(minutes=max_minutes)
            assert sub.next_refresh == expected


# =============================================================================
# Stress tests with multiple operations
# =============================================================================


class TestStressScenarios:
    """Stress tests with many operations."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_many_successful_refreshes(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Handle 100 successful refreshes."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        for i in range(100):
            sub.on_refresh_start()
            sub.on_refresh_success(results={"cycle": i})

        assert sub.refresh_count == 100
        assert sub.error_count == 0
        assert sub.is_active is True

    def test_many_interval_updates(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Handle many interval updates."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        intervals = [60, 120, 240, 480, 1440, 720, 360, 180, 90, 60]
        for interval in intervals:
            sub.update_interval(interval)

        assert sub.refresh_interval_minutes == 60  # Last update

    def test_rapid_pause_resume_cycles(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Handle many pause/resume cycles."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        for _ in range(50):
            sub.pause()
            assert sub.is_active is False
            sub.resume()
            assert sub.is_active is True

        assert sub.is_active is True
        assert sub.error_count == 0


# =============================================================================
# Tests for data validation
# =============================================================================


class TestDataValidation:
    """Tests for data validation in subscriptions."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(type="subscription")

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_id_is_string(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Subscription ID is always a string."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert isinstance(sub.id, str)
        assert len(sub.id) > 0

    def test_refresh_count_never_negative(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Refresh count is never negative."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.refresh_count >= 0

        # Even after errors
        for _ in range(5):
            sub.on_refresh_error(Exception("Error"))

        assert sub.refresh_count >= 0

    def test_error_count_never_negative(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Error count is never negative."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.error_count >= 0

        # After success (which resets)
        sub.on_refresh_success(results={})
        assert sub.error_count >= 0

    def test_to_dict_returns_dict(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict always returns a dictionary."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        result = sub.to_dict()

        assert isinstance(result, dict)

    def test_to_dict_has_required_keys(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict contains all required keys."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        result = sub.to_dict()

        required_keys = [
            "id",
            "type",
            "user_id",
            "query_or_topic",
            "source",
            "created_at",
            "last_refreshed",
            "next_refresh",
            "refresh_interval_minutes",
            "is_active",
            "refresh_count",
            "error_count",
            "last_error",
            "metadata",
        ]

        for key in required_keys:
            assert key in result, f"Missing required key: {key}"


# =============================================================================
# Tests for serialization round-trips
# =============================================================================


class TestSerializationRoundTrips:
    """Tests for data consistency in serialization."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription",
            source_id="src-123",
            created_from="test",
            metadata={"key": "value"},
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_to_dict_preserves_all_data(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict preserves all subscription data."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="AI news",
            refresh_interval_minutes=180,
            subscription_id="sub-abc-123",
        )
        sub.metadata = {"custom": "data", "count": 42}

        result = sub.to_dict()

        assert result["id"] == "sub-abc-123"
        assert result["user_id"] == "user123"
        assert result["query_or_topic"] == "AI news"
        assert result["refresh_interval_minutes"] == 180
        assert result["metadata"]["custom"] == "data"
        assert result["metadata"]["count"] == 42

    def test_multiple_to_dict_calls_consistent(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Multiple to_dict calls return consistent data."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        result1 = sub.to_dict()
        result2 = sub.to_dict()

        # All keys should match
        assert result1.keys() == result2.keys()
        for key in result1:
            assert result1[key] == result2[key], f"Mismatch for key: {key}"

    def test_to_dict_after_state_changes(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict reflects state changes."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Initial state
        result1 = sub.to_dict()
        assert result1["is_active"] is True
        assert result1["refresh_count"] == 0

        # After changes
        sub.on_refresh_success(results={})
        sub.pause()

        result2 = sub.to_dict()
        assert result2["is_active"] is False
        assert result2["refresh_count"] == 1


# =============================================================================
# Tests for abstract method enforcement
# =============================================================================


class TestAbstractMethodEnforcement:
    """Tests for abstract method enforcement."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    def test_cannot_instantiate_base_class_directly(self, mock_storage):
        """BaseSubscription cannot be instantiated directly."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(type="test")

        with pytest.raises(TypeError) as exc_info:
            BaseSubscription(
                user_id="user123",
                source=source,
                query_or_topic="test",
            )

        assert "abstract" in str(exc_info.value).lower()

    def test_must_implement_generate_search_query(self, mock_storage):
        """Subclass must implement generate_search_query."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )
        from local_deep_research.news.core.base_card import CardSource

        class IncompleteSubscription(BaseSubscription):
            def get_subscription_type(self):
                return "incomplete"

        source = CardSource(type="test")

        with pytest.raises(TypeError):
            IncompleteSubscription(
                user_id="user123",
                source=source,
                query_or_topic="test",
            )

    def test_must_implement_get_subscription_type(self, mock_storage):
        """Subclass must implement get_subscription_type."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )
        from local_deep_research.news.core.base_card import CardSource

        class IncompleteSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

        source = CardSource(type="test")

        with pytest.raises(TypeError):
            IncompleteSubscription(
                user_id="user123",
                source=source,
                query_or_topic="test",
            )


# =============================================================================
# Tests for state transition validity
# =============================================================================


class TestStateTransitions:
    """Tests for valid and invalid state transitions."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_pause_from_active_state(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Pausing an active subscription works correctly."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.is_active is True
        sub.pause()
        assert sub.is_active is False

    def test_pause_from_already_paused_state(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Pausing an already paused subscription is idempotent."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.pause()
        sub.pause()  # Should not raise
        assert sub.is_active is False

    def test_resume_from_paused_state(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Resuming a paused subscription works correctly."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.pause()
        assert sub.is_active is False
        sub.resume()
        assert sub.is_active is True

    def test_resume_from_already_active_state(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Resuming an already active subscription is idempotent."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.is_active is True
        sub.resume()  # Should not raise
        assert sub.is_active is True

    def test_refresh_when_paused_returns_false(
        self, mock_storage, card_source, test_subscription_class
    ):
        """should_refresh returns False when subscription is paused."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            mock_utc.return_value = datetime(
                2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc
            )

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=60,
            )

            # Move time forward past refresh time
            mock_utc.return_value = datetime(
                2025, 6, 1, 14, 0, 0, tzinfo=timezone.utc
            )
            sub.pause()

            # Even though time has passed, should not refresh when paused
            assert sub.should_refresh() is False

    def test_error_disables_after_threshold_then_resume_reactivates(
        self, mock_storage, card_source, test_subscription_class
    ):
        """After 10 errors, subscription is disabled, but can be resumed."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Simulate 10 errors
        for i in range(10):
            sub.on_refresh_error(Exception(f"Error {i}"))

        assert sub.is_active is False
        assert sub.error_count == 10

        # Resume should reactivate
        sub.resume()
        assert sub.is_active is True
        assert sub.error_count == 0


# =============================================================================
# Tests for concurrent operation patterns
# =============================================================================


class TestConcurrentPatterns:
    """Tests for patterns that might occur in concurrent scenarios."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_multiple_refresh_starts_in_sequence(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Multiple refresh starts in sequence update timestamp each time."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
            )

            # First refresh start
            time1 = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = time1
            sub.on_refresh_start()
            assert sub.last_refreshed == time1

            # Second refresh start (without success/error in between)
            time2 = datetime(2025, 6, 1, 12, 5, 0, tzinfo=timezone.utc)
            mock_utc.return_value = time2
            sub.on_refresh_start()
            assert sub.last_refreshed == time2

    def test_success_after_multiple_errors_resets_count(
        self, mock_storage, card_source, test_subscription_class
    ):
        """A success after multiple errors resets the error count."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Accumulate errors
        for _ in range(5):
            sub.on_refresh_error(Exception("Failed"))

        assert sub.error_count == 5

        # Success resets
        sub.on_refresh_success(results=[])
        assert sub.error_count == 0

    def test_interleaved_success_and_error(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Interleaved success and error calls track state correctly."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_success([])
        assert sub.refresh_count == 1
        assert sub.error_count == 0

        sub.on_refresh_error(Exception("e1"))
        assert sub.refresh_count == 1
        assert sub.error_count == 1

        sub.on_refresh_success([])
        assert sub.refresh_count == 2
        assert sub.error_count == 0

        sub.on_refresh_error(Exception("e2"))
        sub.on_refresh_error(Exception("e3"))
        assert sub.error_count == 2

    def test_pause_during_error_backoff(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Pausing during error backoff preserves backoff state."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Trigger error and backoff
        sub.on_refresh_error(Exception("Failed"))
        next_refresh_before = sub.next_refresh

        sub.pause()
        # Next refresh should still be the backoff time
        assert sub.next_refresh == next_refresh_before

    def test_resume_after_error_backoff_recalculates(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Resuming after error backoff recalculates next refresh."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Trigger error and backoff
        sub.on_refresh_error(Exception("Failed"))
        next_refresh_with_backoff = sub.next_refresh

        sub.pause()
        sub.resume()  # This resets error_count and recalculates

        # After resume, next_refresh should be recalculated
        # Since error_count is reset to 0, normal interval applies
        assert sub.error_count == 0
        # Next refresh should be different from the backoff time
        assert sub.next_refresh != next_refresh_with_backoff


# =============================================================================
# Tests for boundary values in calculations
# =============================================================================


class TestBoundaryCalculations:
    """Tests for boundary values in interval and backoff calculations."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_backoff_at_error_count_1(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Backoff at error count 1 doubles the interval."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Expected backoff: 60 * 2^1 = 120 minutes
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            sub.on_refresh_error(Exception("Failed"))

            expected_next = now + timedelta(minutes=120)
            assert sub.next_refresh == expected_next

    def test_backoff_at_error_count_5(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Backoff at error count 5 is interval * 32."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Simulate 5 errors
        for _ in range(4):
            sub.on_refresh_error(Exception("Failed"))

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            sub.on_refresh_error(Exception("Failed"))

            # error_count is now 5, backoff = 60 * 2^5 = 60 * 32 = 1920 minutes
            expected_next = now + timedelta(minutes=1920)
            assert sub.next_refresh == expected_next

    def test_backoff_hits_cap_at_high_error_count(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Backoff is capped at 1 week (10080 minutes) even with many errors."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Simulate 8 errors (60 * 2^8 = 15360 > 10080)
        for _ in range(7):
            sub.on_refresh_error(Exception("Failed"))

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            sub.on_refresh_error(Exception("Failed"))

            # Should be capped at 7 days = 10080 minutes
            expected_next = now + timedelta(minutes=10080)
            assert sub.next_refresh == expected_next

    def test_interval_at_exactly_60_minutes(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Interval of exactly 60 minutes is accepted."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        # Should not raise
        sub.update_interval(60)
        assert sub.refresh_interval_minutes == 60

    def test_interval_at_exactly_30_days(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Interval of exactly 30 days (43200 minutes) is accepted."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        max_minutes = 60 * 24 * 30  # 43200
        sub.update_interval(max_minutes)
        assert sub.refresh_interval_minutes == max_minutes

    def test_interval_at_59_minutes_raises(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Interval of 59 minutes raises ValueError."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        with pytest.raises(ValueError) as exc_info:
            sub.update_interval(59)

        assert "at least 60 minutes" in str(exc_info.value).lower()

    def test_interval_at_30_days_plus_one_minute_raises(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Interval of 30 days + 1 minute raises ValueError."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        max_minutes = 60 * 24 * 30 + 1  # 43201
        with pytest.raises(ValueError) as exc_info:
            sub.update_interval(max_minutes)

        assert "30 days" in str(exc_info.value).lower()


# =============================================================================
# Tests for save and storage interaction edge cases
# =============================================================================


class TestSaveEdgeCases:
    """Tests for edge cases in the save method."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            storage_instance = MagicMock()
            storage_instance.create.return_value = "returned-id"
            mock.return_value = storage_instance
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription",
            source_id="src-123",
            created_from="test",
            metadata={"key": "value"},
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test_type"

        return TestSubscription

    def test_save_includes_all_required_fields(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Save includes all required fields in the data dict."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test query",
            refresh_interval_minutes=120,
        )

        sub.save()

        # Get the data dict passed to storage.create
        storage_instance = mock_storage.return_value
        call_args = storage_instance.create.call_args
        data = call_args[0][0]

        # Verify all required fields
        assert data["id"] == sub.id
        assert data["user_id"] == "user123"
        assert data["subscription_type"] == "test_type"
        assert data["query_or_topic"] == "test query"
        assert data["refresh_interval_minutes"] == 120
        assert data["source_type"] == "subscription"
        assert data["source_id"] == "src-123"
        assert data["created_from"] == "test"
        assert data["is_active"] is True

    def test_save_after_pause(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Save captures is_active=False after pause."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.pause()
        sub.save()

        storage_instance = mock_storage.return_value
        data = storage_instance.create.call_args[0][0]
        assert data["is_active"] is False

    def test_save_returns_storage_result(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Save returns the ID from storage.create."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        result = sub.save()
        assert result == "returned-id"

    def test_save_with_empty_metadata(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Save handles empty metadata dict."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )
        sub.metadata = {}

        sub.save()

        storage_instance = mock_storage.return_value
        data = storage_instance.create.call_args[0][0]
        assert data["metadata"] == {}

    def test_save_with_complex_metadata(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Save handles complex nested metadata."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )
        sub.metadata = {
            "nested": {"key": "value"},
            "list": [1, 2, 3],
            "number": 42,
        }

        sub.save()

        storage_instance = mock_storage.return_value
        data = storage_instance.create.call_args[0][0]
        assert data["metadata"]["nested"]["key"] == "value"
        assert data["metadata"]["list"] == [1, 2, 3]
        assert data["metadata"]["number"] == 42


# =============================================================================
# Tests for mark_refreshed edge cases
# =============================================================================


class TestMarkRefreshedEdgeCases:
    """Tests for edge cases in the mark_refreshed method."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            storage_instance = MagicMock()
            mock.return_value = storage_instance
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_mark_refreshed_with_zero_results(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed handles zero results."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.mark_refreshed(0)

        storage_instance = mock_storage.return_value
        storage_instance.increment_stats.assert_called_once_with(sub.id, 0)

    def test_mark_refreshed_with_large_result_count(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed handles large result counts."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.mark_refreshed(10000)

        storage_instance = mock_storage.return_value
        storage_instance.increment_stats.assert_called_once_with(sub.id, 10000)

    def test_mark_refreshed_multiple_times(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed can be called multiple times."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.mark_refreshed(5)
        sub.mark_refreshed(10)
        sub.mark_refreshed(3)

        assert sub.refresh_count == 3
        storage_instance = mock_storage.return_value
        assert storage_instance.increment_stats.call_count == 3

    def test_mark_refreshed_updates_timestamps(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed updates last_refreshed and next_refresh."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=60,
            )

            sub.mark_refreshed(5)

            assert sub.last_refreshed == now
            expected_next = now + timedelta(minutes=60)
            assert sub.next_refresh == expected_next


# =============================================================================
# Tests for generate_search_query behavior
# =============================================================================


class TestGenerateSearchQuery:
    """Tests for the generate_search_query abstract method implementation."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    def test_simple_query_passthrough(self, mock_storage, card_source):
        """Simple query is passed through correctly."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class SimpleSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "simple"

        sub = SimpleSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="AI news",
        )

        assert sub.generate_search_query() == "AI news"

    def test_query_transformation(self, mock_storage, card_source):
        """Subclass can transform the query."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TransformingSubscription(BaseSubscription):
            def generate_search_query(self):
                return f"LATEST: {self.query_or_topic}"

            def get_subscription_type(self):
                return "transforming"

        sub = TransformingSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="AI news",
        )

        assert sub.generate_search_query() == "LATEST: AI news"

    def test_query_with_metadata(self, mock_storage, card_source):
        """Subclass can use metadata in query generation."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class MetadataSubscription(BaseSubscription):
            def generate_search_query(self):
                region = self.metadata.get("region", "global")
                return f"{self.query_or_topic} region:{region}"

            def get_subscription_type(self):
                return "metadata"

        sub = MetadataSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="AI news",
        )
        sub.metadata["region"] = "us"

        assert sub.generate_search_query() == "AI news region:us"

    def test_query_empty_string(self, mock_storage, card_source):
        """Empty query string is handled."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class SimpleSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "simple"

        sub = SimpleSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="",
        )

        assert sub.generate_search_query() == ""


# =============================================================================
# Tests for is_due_for_refresh alias
# =============================================================================


class TestIsDueForRefreshAlias:
    """Tests for the is_due_for_refresh alias method."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_is_due_for_refresh_matches_should_refresh_when_true(
        self, mock_storage, card_source, test_subscription_class
    ):
        """is_due_for_refresh returns True when should_refresh is True."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            # Create subscription
            mock_utc.return_value = datetime(
                2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc
            )
            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=60,
            )

            # Move time forward past next_refresh
            mock_utc.return_value = datetime(
                2025, 6, 1, 14, 0, 0, tzinfo=timezone.utc
            )

            assert sub.is_due_for_refresh() is True
            assert sub.should_refresh() is True
            assert sub.is_due_for_refresh() == sub.should_refresh()

    def test_is_due_for_refresh_matches_should_refresh_when_false(
        self, mock_storage, card_source, test_subscription_class
    ):
        """is_due_for_refresh returns False when should_refresh is False."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            # Create subscription
            mock_utc.return_value = datetime(
                2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc
            )
            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=60,
            )

            # Time is still before next_refresh
            mock_utc.return_value = datetime(
                2025, 6, 1, 12, 30, 0, tzinfo=timezone.utc
            )

            assert sub.is_due_for_refresh() is False
            assert sub.should_refresh() is False
            assert sub.is_due_for_refresh() == sub.should_refresh()


# =============================================================================
# Tests for user_id handling
# =============================================================================


class TestUserIdHandling:
    """Tests for various user_id formats and values."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_uuid_user_id(
        self, mock_storage, card_source, test_subscription_class
    ):
        """UUID user_id is stored correctly."""
        user_id = "550e8400-e29b-41d4-a716-446655440000"
        sub = test_subscription_class(
            user_id=user_id,
            source=card_source,
            query_or_topic="test",
        )

        assert sub.user_id == user_id

    def test_email_user_id(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Email-style user_id is stored correctly."""
        user_id = "user@example.com"
        sub = test_subscription_class(
            user_id=user_id,
            source=card_source,
            query_or_topic="test",
        )

        assert sub.user_id == user_id

    def test_numeric_string_user_id(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Numeric string user_id is stored correctly."""
        user_id = "12345"
        sub = test_subscription_class(
            user_id=user_id,
            source=card_source,
            query_or_topic="test",
        )

        assert sub.user_id == user_id

    def test_empty_user_id(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Empty user_id is allowed (no validation)."""
        sub = test_subscription_class(
            user_id="",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.user_id == ""

    def test_unicode_user_id(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Unicode user_id is stored correctly."""
        user_id = "用户123"
        sub = test_subscription_class(
            user_id=user_id,
            source=card_source,
            query_or_topic="test",
        )

        assert sub.user_id == user_id


# =============================================================================
# Tests for query_or_topic handling
# =============================================================================


class TestQueryOrTopicHandling:
    """Tests for various query_or_topic values."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_simple_query(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Simple query is stored correctly."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="AI news",
        )

        assert sub.query_or_topic == "AI news"

    def test_complex_query_with_operators(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Query with search operators is stored correctly."""
        query = 'AI AND "machine learning" OR robotics -autonomous'
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic=query,
        )

        assert sub.query_or_topic == query

    def test_unicode_query(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Unicode query is stored correctly."""
        query = "人工智能新闻 AI ニュース"
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic=query,
        )

        assert sub.query_or_topic == query

    def test_very_long_query(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Very long query is stored correctly."""
        query = "AI " * 1000  # 3000+ characters
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic=query,
        )

        assert sub.query_or_topic == query

    def test_query_with_newlines(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Query with newlines is stored correctly."""
        query = "AI news\nMachine learning\nDeep learning"
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic=query,
        )

        assert sub.query_or_topic == query


# =============================================================================
# Tests for subscription_type attribute
# =============================================================================


class TestSubscriptionTypeAttribute:
    """Tests for the subscription_type attribute."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    def test_subscription_type_initially_none(self, mock_storage, card_source):
        """subscription_type attribute is initially None."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "custom_type"

        sub = TestSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # The attribute is None, but get_subscription_type returns a value
        assert sub.subscription_type is None
        assert sub.get_subscription_type() == "custom_type"

    def test_subscription_type_can_be_set_by_subclass(
        self, mock_storage, card_source
    ):
        """Subclass can set subscription_type in __init__."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class CustomSubscription(BaseSubscription):
            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                self.subscription_type = "custom"

            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return self.subscription_type

        sub = CustomSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.subscription_type == "custom"
        assert sub.get_subscription_type() == "custom"

    def test_different_subscription_types(self, mock_storage, card_source):
        """Different subclasses can have different subscription types."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class KeywordSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "keyword"

        class TopicSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "topic"

        keyword_sub = KeywordSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="AI",
        )

        topic_sub = TopicSubscription(
            user_id="user123",
            source=card_source,
            query_or_topic="technology",
        )

        assert keyword_sub.get_subscription_type() == "keyword"
        assert topic_sub.get_subscription_type() == "topic"


# =============================================================================
# Tests for CardSource variations
# =============================================================================


class TestCardSourceVariations:
    """Tests for different CardSource configurations."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_source_with_all_fields(
        self, mock_storage, test_subscription_class
    ):
        """CardSource with all fields is stored correctly."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="subscription",
            source_id="src-123",
            created_from="web_ui",
            metadata={"campaign": "summer2025", "priority": "high"},
        )

        sub = test_subscription_class(
            user_id="user123",
            source=source,
            query_or_topic="test",
        )

        assert sub.source.type == "subscription"
        assert sub.source.source_id == "src-123"
        assert sub.source.created_from == "web_ui"
        assert sub.source.metadata["campaign"] == "summer2025"

    def test_source_type_variations(
        self, mock_storage, test_subscription_class
    ):
        """Various source types are accepted."""
        from local_deep_research.news.core.base_card import CardSource

        source_types = [
            "subscription",
            "recommendation",
            "search",
            "manual",
            "api",
        ]

        for source_type in source_types:
            source = CardSource(type=source_type)
            sub = test_subscription_class(
                user_id="user123",
                source=source,
                query_or_topic="test",
            )
            assert sub.source.type == source_type

    def test_source_in_to_dict(self, mock_storage, test_subscription_class):
        """Source information is included in to_dict."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(
            type="subscription",
            source_id="src-456",
            created_from="api",
            metadata={"version": "1.0"},
        )

        sub = test_subscription_class(
            user_id="user123",
            source=source,
            query_or_topic="test",
        )

        result = sub.to_dict()

        assert result["source"]["type"] == "subscription"
        assert result["source"]["source_id"] == "src-456"
        assert result["source"]["created_from"] == "api"
        assert result["source"]["metadata"]["version"] == "1.0"


# =============================================================================
# Tests for timestamp edge cases
# =============================================================================


class TestTimestampEdgeCases:
    """Tests for edge cases in timestamp handling."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_next_refresh_after_created_at(
        self, mock_storage, card_source, test_subscription_class
    ):
        """next_refresh is always after created_at for new subscriptions."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=60,
            )

            assert sub.next_refresh > sub.created_at

    def test_next_refresh_exact_interval(
        self, mock_storage, card_source, test_subscription_class
    ):
        """next_refresh is exactly interval minutes after created_at."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=120,
            )

            expected = now + timedelta(minutes=120)
            assert sub.next_refresh == expected

    def test_timestamps_are_timezone_aware(
        self, mock_storage, card_source, test_subscription_class
    ):
        """All timestamps are timezone-aware."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
            )

            assert sub.created_at.tzinfo is not None
            assert sub.next_refresh.tzinfo is not None

    def test_to_dict_timestamps_are_iso_format(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Timestamps in to_dict are ISO format strings."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
            )

            result = sub.to_dict()

            # Should be valid ISO format strings
            assert "2025-06-01" in result["created_at"]
            assert "T" in result["created_at"]  # ISO format has T separator


# =============================================================================
# Tests for default values
# =============================================================================


class TestDefaultValues:
    """Tests for default parameter values."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_default_refresh_interval_is_240(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Default refresh interval is 240 minutes (4 hours)."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.refresh_interval_minutes == 240

    def test_default_is_active_true(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Default is_active is True."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.is_active is True

    def test_default_refresh_count_zero(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Default refresh_count is 0."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.refresh_count == 0

    def test_default_error_count_zero(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Default error_count is 0."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.error_count == 0

    def test_default_last_error_none(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Default last_error is None."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.last_error is None

    def test_default_last_refreshed_none(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Default last_refreshed is None."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.last_refreshed is None

    def test_default_metadata_empty_dict(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Default metadata is empty dict."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.metadata == {}


# =============================================================================
# Tests for error message content
# =============================================================================


class TestErrorMessageContent:
    """Tests for error message storage and content."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_error_message_from_exception(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Error message is extracted from exception."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_error(Exception("Connection timeout"))

        assert sub.last_error == "Connection timeout"

    def test_error_message_from_custom_exception(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Error message from custom exception class."""

        class CustomError(Exception):
            pass

        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_error(CustomError("Custom error occurred"))

        assert sub.last_error == "Custom error occurred"

    def test_latest_error_overwrites_previous(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Latest error message overwrites previous."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_error(Exception("First error"))
        assert sub.last_error == "First error"

        sub.on_refresh_error(Exception("Second error"))
        assert sub.last_error == "Second error"

    def test_error_message_in_to_dict(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Error message is included in to_dict."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_error(Exception("API rate limit exceeded"))

        result = sub.to_dict()
        assert result["last_error"] == "API rate limit exceeded"

    def test_error_message_cleared_implicitly_by_success(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Error message is NOT cleared by success (only error_count is reset)."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_error(Exception("Temporary error"))
        sub.on_refresh_success(results=[])

        # last_error is NOT cleared by success, only error_count is reset
        assert sub.last_error == "Temporary error"
        assert sub.error_count == 0


# =============================================================================
# Tests for storage method calls
# =============================================================================


class TestStorageMethodCalls:
    """Tests for verifying correct storage method invocations."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            storage_instance = MagicMock()
            storage_instance.create.return_value = "new-id"
            mock.return_value = storage_instance
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_storage_instantiated_on_init(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Storage is instantiated when subscription is created."""
        test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        mock_storage.assert_called_once()

    def test_mark_refreshed_calls_update_refresh_time(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed calls storage.update_refresh_time."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.mark_refreshed(5)

        storage_instance = mock_storage.return_value
        storage_instance.update_refresh_time.assert_called_once()
        call_args = storage_instance.update_refresh_time.call_args
        assert call_args[0][0] == sub.id

    def test_mark_refreshed_calls_increment_stats(
        self, mock_storage, card_source, test_subscription_class
    ):
        """mark_refreshed calls storage.increment_stats with correct count."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.mark_refreshed(42)

        storage_instance = mock_storage.return_value
        storage_instance.increment_stats.assert_called_once_with(sub.id, 42)

    def test_save_calls_storage_create(
        self, mock_storage, card_source, test_subscription_class
    ):
        """save calls storage.create with correct data."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test query",
        )

        sub.save()

        storage_instance = mock_storage.return_value
        storage_instance.create.assert_called_once()


# =============================================================================
# Tests for ID generation
# =============================================================================


class TestIdGeneration:
    """Tests for subscription ID generation."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_generated_ids_are_unique(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Generated IDs are unique across multiple subscriptions."""
        ids = set()
        for _ in range(100):
            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
            )
            ids.add(sub.id)

        assert len(ids) == 100  # All unique

    def test_provided_id_used_verbatim(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Provided ID is used exactly as given."""
        custom_id = "my-custom-id-12345"
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id=custom_id,
        )

        assert sub.id == custom_id

    def test_id_is_string_type(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Generated ID is a string type."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert isinstance(sub.id, str)

    def test_id_is_non_empty(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Generated ID is non-empty."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert len(sub.id) > 0


# =============================================================================
# Tests for complex lifecycle scenarios
# =============================================================================


class TestComplexLifecycleScenarios:
    """Tests for complex subscription lifecycle scenarios."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_full_refresh_cycle(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Full refresh cycle: start -> success -> update."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        initial_refresh_count = sub.refresh_count

        sub.on_refresh_start()
        sub.on_refresh_success(results=["result1", "result2"])

        assert sub.refresh_count == initial_refresh_count + 1
        assert sub.error_count == 0
        assert sub.last_refreshed is not None

    def test_error_recovery_cycle(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Error recovery: errors -> recovery -> success."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Multiple errors
        for _ in range(5):
            sub.on_refresh_error(Exception("Failed"))

        assert sub.error_count == 5

        # Recovery via success
        sub.on_refresh_success(results=[])

        assert sub.error_count == 0
        assert sub.refresh_count == 1

    def test_pause_resume_preserves_counts(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Pause/resume preserves refresh_count but resets error_count."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Build up some state
        sub.on_refresh_success([])
        sub.on_refresh_success([])
        sub.on_refresh_error(Exception("e"))
        sub.on_refresh_error(Exception("e"))

        assert sub.refresh_count == 2
        assert sub.error_count == 2

        sub.pause()
        sub.resume()

        # refresh_count preserved, error_count reset
        assert sub.refresh_count == 2
        assert sub.error_count == 0

    def test_interval_update_during_active(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Interval can be updated while subscription is active."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            refresh_interval_minutes=60,
        )

        sub.on_refresh_success([])
        old_next_refresh = sub.next_refresh

        sub.update_interval(120)

        assert sub.refresh_interval_minutes == 120
        assert sub.next_refresh != old_next_refresh

    def test_multiple_pause_resume_cycles(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Multiple pause/resume cycles work correctly."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        for _ in range(10):
            sub.pause()
            assert sub.is_active is False
            sub.resume()
            assert sub.is_active is True

    def test_disabled_by_errors_full_recovery(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Subscription disabled by errors can fully recover."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Disable via errors
        for i in range(10):
            sub.on_refresh_error(Exception(f"Error {i}"))

        assert sub.is_active is False
        assert sub.error_count == 10

        # Full recovery
        sub.resume()
        sub.on_refresh_success([])
        sub.on_refresh_success([])

        assert sub.is_active is True
        assert sub.error_count == 0
        assert sub.refresh_count == 2


# =============================================================================
# Tests for refresh timing precision
# =============================================================================


class TestRefreshTimingPrecision:
    """Tests for precise refresh timing calculations."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_refresh_exactly_at_boundary(
        self, mock_storage, card_source, test_subscription_class
    ):
        """should_refresh returns True exactly at next_refresh time."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            # Create at t=0
            t0 = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = t0

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=60,
            )

            # Check exactly at next_refresh
            mock_utc.return_value = sub.next_refresh

            assert sub.should_refresh() is True

    def test_refresh_one_second_before(
        self, mock_storage, card_source, test_subscription_class
    ):
        """should_refresh returns False one second before next_refresh."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone, timedelta

            # Create at t=0
            t0 = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = t0

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=60,
            )

            # Check 1 second before
            mock_utc.return_value = sub.next_refresh - timedelta(seconds=1)

            assert sub.should_refresh() is False

    def test_refresh_one_second_after(
        self, mock_storage, card_source, test_subscription_class
    ):
        """should_refresh returns True one second after next_refresh."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone, timedelta

            # Create at t=0
            t0 = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = t0

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=60,
            )

            # Check 1 second after
            mock_utc.return_value = sub.next_refresh + timedelta(seconds=1)

            assert sub.should_refresh() is True

    def test_large_time_gap(
        self, mock_storage, card_source, test_subscription_class
    ):
        """should_refresh handles large time gaps correctly."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone, timedelta

            # Create at t=0
            t0 = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = t0

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=60,
            )

            # Check 1 year later
            mock_utc.return_value = t0 + timedelta(days=365)

            assert sub.should_refresh() is True


# =============================================================================
# Tests for metadata manipulation
# =============================================================================


class TestMetadataManipulation:
    """Tests for metadata dictionary manipulation."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_metadata_add_key(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Can add keys to metadata."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.metadata["new_key"] = "new_value"

        assert sub.metadata["new_key"] == "new_value"

    def test_metadata_update_key(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Can update existing metadata keys."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.metadata["key"] = "value1"
        sub.metadata["key"] = "value2"

        assert sub.metadata["key"] == "value2"

    def test_metadata_delete_key(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Can delete metadata keys."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.metadata["key"] = "value"
        del sub.metadata["key"]

        assert "key" not in sub.metadata

    def test_metadata_nested_structures(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Metadata supports nested structures."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.metadata["nested"] = {"level1": {"level2": {"value": 42}}}

        assert sub.metadata["nested"]["level1"]["level2"]["value"] == 42

    def test_metadata_in_to_dict_is_same_reference(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Metadata in to_dict is a reference to the same dict (not a copy)."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.metadata["key1"] = "value1"
        result = sub.to_dict()

        # Verify the metadata is in the result
        assert "key1" in result["metadata"]
        assert result["metadata"]["key1"] == "value1"

        # Since it's a reference, modifying sub.metadata affects result too
        sub.metadata["key2"] = "value2"
        assert "key2" in result["metadata"]

    def test_to_dict_returns_correct_type(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict returns correct type for metadata field."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.metadata["test"] = "value"
        result = sub.to_dict()

        assert isinstance(result["metadata"], dict)


# =============================================================================
# Tests for backoff formula edge cases
# =============================================================================


class TestBackoffFormulaEdgeCases:
    """Tests for edge cases in the exponential backoff formula."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_backoff_with_minimum_interval(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Backoff calculation with minimum interval (60 min)."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=60,
            )

            sub.on_refresh_error(Exception("e"))

            # 60 * 2^1 = 120 minutes
            expected = now + timedelta(minutes=120)
            assert sub.next_refresh == expected

    def test_backoff_with_maximum_interval(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Backoff calculation with maximum interval (30 days)."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            max_interval = 60 * 24 * 30  # 43200 minutes
            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=max_interval,
            )

            sub.on_refresh_error(Exception("e"))

            # 43200 * 2^1 = 86400, but capped at 10080 (1 week)
            expected = now + timedelta(minutes=10080)
            assert sub.next_refresh == expected

    def test_backoff_cap_applies_early_with_large_interval(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Backoff cap kicks in early with large refresh intervals."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.utc_now"
        ) as mock_utc:
            from datetime import datetime, timezone

            now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
            mock_utc.return_value = now

            # Large interval: 1440 min (1 day)
            sub = test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
                refresh_interval_minutes=1440,
            )

            # First error: 1440 * 2 = 2880 (under cap)
            sub.on_refresh_error(Exception("e1"))
            assert sub.next_refresh == now + timedelta(minutes=2880)

            # Second error: 1440 * 4 = 5760 (under cap)
            mock_utc.return_value = now
            sub.on_refresh_error(Exception("e2"))
            assert sub.next_refresh == now + timedelta(minutes=5760)

            # Third error: 1440 * 8 = 11520 (over cap, should be 10080)
            mock_utc.return_value = now
            sub.on_refresh_error(Exception("e3"))
            assert sub.next_refresh == now + timedelta(minutes=10080)

    def test_backoff_after_9_errors_still_active(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Subscription is still active after 9 errors."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        for _ in range(9):
            sub.on_refresh_error(Exception("e"))

        assert sub.error_count == 9
        assert sub.is_active is True

    def test_backoff_exactly_at_10_errors_disables(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Subscription is disabled exactly at 10 errors."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        for _ in range(10):
            sub.on_refresh_error(Exception("e"))

        assert sub.error_count == 10
        assert sub.is_active is False


# =============================================================================
# Tests for to_dict completeness
# =============================================================================


class TestToDictCompleteness:
    """Tests for to_dict method completeness."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription",
            source_id="src-123",
            created_from="test",
            metadata={"source_meta": "value"},
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test_type"

        return TestSubscription

    def test_to_dict_contains_all_top_level_keys(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict contains all expected top-level keys."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test query",
        )

        result = sub.to_dict()

        expected_keys = {
            "id",
            "type",
            "user_id",
            "query_or_topic",
            "source",
            "created_at",
            "last_refreshed",
            "next_refresh",
            "refresh_interval_minutes",
            "is_active",
            "refresh_count",
            "error_count",
            "last_error",
            "metadata",
        }

        assert set(result.keys()) == expected_keys

    def test_to_dict_source_contains_all_keys(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict source field contains all expected keys."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        result = sub.to_dict()

        expected_source_keys = {"type", "source_id", "created_from", "metadata"}
        assert set(result["source"].keys()) == expected_source_keys

    def test_to_dict_after_error(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict reflects state after error."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_error(Exception("Test error"))

        result = sub.to_dict()

        assert result["error_count"] == 1
        assert result["last_error"] == "Test error"

    def test_to_dict_after_success(
        self, mock_storage, card_source, test_subscription_class
    ):
        """to_dict reflects state after success."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_success(results=["r1", "r2"])

        result = sub.to_dict()

        assert result["refresh_count"] == 1
        assert result["error_count"] == 0


# =============================================================================
# Tests for special characters in fields
# =============================================================================


class TestSpecialCharactersInFields:
    """Tests for handling special characters in various fields."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_query_with_sql_injection_chars(
        self, mock_storage, test_subscription_class
    ):
        """Query with SQL-like characters is stored correctly."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(type="test")
        query = "'; DROP TABLE users; --"

        sub = test_subscription_class(
            user_id="user123",
            source=source,
            query_or_topic=query,
        )

        assert sub.query_or_topic == query
        assert sub.generate_search_query() == query

    def test_query_with_html_tags(self, mock_storage, test_subscription_class):
        """Query with HTML tags is stored correctly."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(type="test")
        query = "<script>alert('xss')</script>"

        sub = test_subscription_class(
            user_id="user123",
            source=source,
            query_or_topic=query,
        )

        assert sub.query_or_topic == query

    def test_user_id_with_special_chars(
        self, mock_storage, test_subscription_class
    ):
        """User ID with special characters is stored correctly."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(type="test")
        user_id = "user+test@domain.com/path?query=1&other=2"

        sub = test_subscription_class(
            user_id=user_id,
            source=source,
            query_or_topic="test",
        )

        assert sub.user_id == user_id

    def test_metadata_with_special_chars(
        self, mock_storage, test_subscription_class
    ):
        """Metadata with special characters is stored correctly."""
        from local_deep_research.news.core.base_card import CardSource

        source = CardSource(type="test")

        sub = test_subscription_class(
            user_id="user123",
            source=source,
            query_or_topic="test",
        )

        sub.metadata["special"] = "line1\nline2\ttab"
        sub.metadata["unicode"] = "日本語🎉"
        sub.metadata["quotes"] = 'single\' and "double"'

        assert sub.metadata["special"] == "line1\nline2\ttab"
        assert sub.metadata["unicode"] == "日本語🎉"
        assert sub.metadata["quotes"] == 'single\' and "double"'


# =============================================================================
# Tests for subscription equality and identity
# =============================================================================


class TestSubscriptionIdentity:
    """Tests for subscription identity and comparison."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_same_id_same_subscription(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Two subscriptions with same ID are logically the same."""
        sub1 = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="same-id",
        )

        sub2 = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="same-id",
        )

        assert sub1.id == sub2.id

    def test_different_ids_different_subscriptions(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Two subscriptions with different IDs are different."""
        sub1 = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="id-1",
        )

        sub2 = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
            subscription_id="id-2",
        )

        assert sub1.id != sub2.id

    def test_auto_generated_ids_are_different(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Auto-generated IDs are unique across instances."""
        subs = [
            test_subscription_class(
                user_id="user123",
                source=card_source,
                query_or_topic="test",
            )
            for _ in range(50)
        ]

        ids = [sub.id for sub in subs]
        assert len(set(ids)) == 50  # All unique


# =============================================================================
# Tests for refresh count bounds
# =============================================================================


class TestRefreshCountBounds:
    """Tests for refresh count behavior at boundaries."""

    @pytest.fixture
    def mock_storage(self):
        """Mock the storage."""
        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ) as mock:
            mock.return_value = MagicMock()
            yield mock

    @pytest.fixture
    def card_source(self):
        """Create a mock CardSource."""
        from local_deep_research.news.core.base_card import CardSource

        return CardSource(
            type="subscription", source_id="src-123", created_from="test"
        )

    @pytest.fixture
    def test_subscription_class(self):
        """Create a concrete subscription class for testing."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        class TestSubscription(BaseSubscription):
            def generate_search_query(self):
                return self.query_or_topic

            def get_subscription_type(self):
                return "test"

        return TestSubscription

    def test_refresh_count_starts_at_zero(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Refresh count starts at zero."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        assert sub.refresh_count == 0

    def test_refresh_count_increments_on_success(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Refresh count increments on each success."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        for i in range(5):
            sub.on_refresh_success([])
            assert sub.refresh_count == i + 1

    def test_refresh_count_not_affected_by_errors(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Refresh count is not affected by errors."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        sub.on_refresh_success([])
        sub.on_refresh_error(Exception("e"))
        sub.on_refresh_error(Exception("e"))

        assert sub.refresh_count == 1

    def test_refresh_count_large_value(
        self, mock_storage, card_source, test_subscription_class
    ):
        """Refresh count handles large values."""
        sub = test_subscription_class(
            user_id="user123",
            source=card_source,
            query_or_topic="test",
        )

        # Simulate many refreshes
        for _ in range(1000):
            sub.on_refresh_success([])

        assert sub.refresh_count == 1000
