"""
Extended tests for news/subscription_manager/base_subscription.py

Tests cover:
- BaseSubscription abstract class
- Initialization and defaults
- _calculate_next_refresh() method
- should_refresh() and is_due_for_refresh() methods
- on_refresh_start(), on_refresh_success(), on_refresh_error() methods
- pause(), resume(), deactivate() methods
- to_dict() serialization
- Error counting and exponential backoff
- Auto-deactivation after errors
"""

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


@pytest.fixture(autouse=True)
def mock_storage():
    """Mock SQLSubscriptionStorage for all tests."""
    with patch(
        "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
    ) as mock:
        mock.return_value = MagicMock()
        yield mock


class ConcreteSubscription:
    """Factory for creating concrete subscription instances for testing."""

    @staticmethod
    def create(**kwargs):
        """Create a concrete subscription instance."""
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )
        from local_deep_research.news.core.base_card import CardSource

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

            def get_subscription_type(self):
                return "test"

        source = kwargs.pop("source", None) or CardSource(
            type="test", source_id="test-source"
        )

        return TestSubscription(
            user_id=kwargs.pop("user_id", "user123"),
            source=source,
            query_or_topic=kwargs.pop("query_or_topic", "test query"),
            **kwargs,
        )


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

    def test_generates_id_when_not_provided(self):
        """Generates unique ID when not provided."""
        sub1 = ConcreteSubscription.create()
        sub2 = ConcreteSubscription.create()

        assert sub1.id != sub2.id
        assert len(sub1.id) == 36  # UUID format

    def test_uses_provided_id(self):
        """Uses provided subscription ID."""
        sub = ConcreteSubscription.create(subscription_id="custom-id-123")

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

    def test_stores_user_id(self):
        """Stores user ID."""
        sub = ConcreteSubscription.create(user_id="test_user")

        assert sub.user_id == "test_user"

    def test_stores_query_or_topic(self):
        """Stores query or topic."""
        sub = ConcreteSubscription.create(query_or_topic="climate change news")

        assert sub.query_or_topic == "climate change news"

    def test_default_refresh_interval(self):
        """Default refresh interval is 240 minutes (4 hours)."""
        sub = ConcreteSubscription.create()

        assert sub.refresh_interval_minutes == 240

    def test_custom_refresh_interval(self):
        """Accepts custom refresh interval."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=60)

        assert sub.refresh_interval_minutes == 60

    def test_initial_is_active(self):
        """Subscription is active by default."""
        sub = ConcreteSubscription.create()

        assert sub.is_active is True

    def test_initial_refresh_count_zero(self):
        """Initial refresh count is zero."""
        sub = ConcreteSubscription.create()

        assert sub.refresh_count == 0

    def test_initial_error_count_zero(self):
        """Initial error count is zero."""
        sub = ConcreteSubscription.create()

        assert sub.error_count == 0

    def test_initial_last_refreshed_none(self):
        """Initial last_refreshed is None."""
        sub = ConcreteSubscription.create()

        assert sub.last_refreshed is None

    def test_initial_last_error_none(self):
        """Initial last_error is None."""
        sub = ConcreteSubscription.create()

        assert sub.last_error is None

    def test_metadata_empty_dict(self):
        """Initial metadata is empty dict."""
        sub = ConcreteSubscription.create()

        assert sub.metadata == {}


class TestCalculateNextRefresh:
    """Tests for _calculate_next_refresh() method."""

    def test_new_subscription_next_refresh(self):
        """New subscription next_refresh is created_at + interval."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=60)

        expected = sub.created_at + timedelta(minutes=60)
        assert abs((sub.next_refresh - expected).total_seconds()) < 1

    def test_after_refresh_next_refresh(self):
        """After refresh, next_refresh is last_refreshed + interval."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=30)

        # Simulate a refresh
        sub.on_refresh_start()
        sub.on_refresh_success({})

        expected = sub.last_refreshed + timedelta(minutes=30)
        assert abs((sub.next_refresh - expected).total_seconds()) < 1


class TestShouldRefresh:
    """Tests for should_refresh() method."""

    def test_returns_true_when_past_next_refresh(self):
        """Returns True when current time is past next_refresh."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=1)

        # Set next_refresh to the past
        sub.next_refresh = sub.created_at - timedelta(hours=1)

        assert sub.should_refresh() is True

    def test_returns_false_when_inactive(self):
        """Returns False when subscription is inactive."""
        sub = ConcreteSubscription.create()
        sub.is_active = False
        sub.next_refresh = sub.created_at - timedelta(hours=1)

        assert sub.should_refresh() is False

    def test_returns_false_when_not_due(self):
        """Returns False when not yet due for refresh."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=9999)

        assert sub.should_refresh() is False

    def test_is_due_for_refresh_alias(self):
        """is_due_for_refresh is alias for should_refresh."""
        sub = ConcreteSubscription.create()

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


class TestOnRefreshStart:
    """Tests for on_refresh_start() method."""

    def test_updates_last_refreshed(self):
        """Updates last_refreshed timestamp."""
        sub = ConcreteSubscription.create()
        assert sub.last_refreshed is None

        sub.on_refresh_start()

        assert sub.last_refreshed is not None

    def test_last_refreshed_is_recent(self):
        """last_refreshed is set to current time."""
        sub = ConcreteSubscription.create()

        sub.on_refresh_start()

        now = datetime.now(timezone.utc)
        assert abs((now - sub.last_refreshed).total_seconds()) < 2


class TestOnRefreshSuccess:
    """Tests for on_refresh_success() method."""

    def test_increments_refresh_count(self):
        """Increments refresh_count."""
        sub = ConcreteSubscription.create()
        assert sub.refresh_count == 0

        sub.on_refresh_start()
        sub.on_refresh_success({})

        assert sub.refresh_count == 1

    def test_resets_error_count(self):
        """Resets error_count to zero."""
        sub = ConcreteSubscription.create()
        sub.error_count = 5

        sub.on_refresh_start()
        sub.on_refresh_success({})

        assert sub.error_count == 0

    def test_updates_next_refresh(self):
        """Updates next_refresh."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=30)
        old_next = sub.next_refresh

        sub.on_refresh_start()
        sub.on_refresh_success({})

        assert sub.next_refresh != old_next


class TestOnRefreshError:
    """Tests for on_refresh_error() method."""

    def test_increments_error_count(self):
        """Increments error_count."""
        sub = ConcreteSubscription.create()
        assert sub.error_count == 0

        sub.on_refresh_error(Exception("Test error"))

        assert sub.error_count == 1

    def test_stores_last_error(self):
        """Stores last error message."""
        sub = ConcreteSubscription.create()

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

        assert sub.last_error == "Connection failed"

    def test_applies_exponential_backoff(self):
        """Applies exponential backoff to next_refresh."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=60)
        original_next = sub.next_refresh

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

        # Next refresh should be further out due to backoff
        assert sub.next_refresh > original_next

    def test_backoff_increases_with_errors(self):
        """Backoff increases with each error."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=60)

        sub.on_refresh_error(Exception("Error 1"))
        first_backoff = sub.next_refresh

        sub.on_refresh_error(Exception("Error 2"))
        second_backoff = sub.next_refresh

        assert second_backoff > first_backoff

    def test_backoff_capped_at_one_week(self):
        """Backoff is capped at one week."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=60)

        # Generate many errors to hit cap
        for i in range(20):
            sub.on_refresh_error(Exception(f"Error {i}"))

        from local_deep_research.news.core.utils import utc_now

        max_backoff = utc_now() + timedelta(days=7, minutes=1)
        assert sub.next_refresh <= max_backoff

    def test_disables_after_10_errors(self):
        """Disables subscription after 10 consecutive errors."""
        sub = ConcreteSubscription.create()

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

        assert sub.is_active is False

    def test_stays_active_below_10_errors(self):
        """Stays active with less than 10 errors."""
        sub = ConcreteSubscription.create()

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

        assert sub.is_active is True


class TestPauseResumeDeactivate:
    """Tests for pause(), resume(), deactivate() methods."""

    def test_pause_sets_inactive(self):
        """pause() sets is_active to False."""
        sub = ConcreteSubscription.create()

        sub.pause()

        assert sub.is_active is False

    def test_resume_sets_active(self):
        """resume() sets is_active to True."""
        sub = ConcreteSubscription.create()
        sub.is_active = False

        sub.resume()

        assert sub.is_active is True

    def test_resume_resets_error_count(self):
        """resume() resets error_count."""
        sub = ConcreteSubscription.create()
        sub.error_count = 5

        sub.resume()

        assert sub.error_count == 0

    def test_resume_recalculates_next_refresh(self):
        """resume() recalculates next_refresh."""
        sub = ConcreteSubscription.create()
        _old_next = sub.next_refresh  # noqa: F841

        sub.resume()

        # next_refresh should be recalculated
        assert sub.next_refresh is not None

    def test_pause_can_be_called_multiple_times(self):
        """pause() can be called multiple times."""
        sub = ConcreteSubscription.create()

        sub.pause()
        sub.pause()  # Should not raise

        assert sub.is_active is False


class TestToDict:
    """Tests for to_dict() serialization."""

    def test_includes_id(self):
        """to_dict includes id."""
        sub = ConcreteSubscription.create()

        result = sub.to_dict()

        assert "id" in result
        assert result["id"] == sub.id

    def test_includes_user_id(self):
        """to_dict includes user_id."""
        sub = ConcreteSubscription.create(user_id="test_user")

        result = sub.to_dict()

        assert result["user_id"] == "test_user"

    def test_includes_query_or_topic(self):
        """to_dict includes query_or_topic."""
        sub = ConcreteSubscription.create(query_or_topic="test topic")

        result = sub.to_dict()

        assert result["query_or_topic"] == "test topic"

    def test_includes_is_active(self):
        """to_dict includes is_active."""
        sub = ConcreteSubscription.create()

        result = sub.to_dict()

        assert "is_active" in result
        assert result["is_active"] is True

    def test_includes_refresh_interval(self):
        """to_dict includes refresh_interval_minutes."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=120)

        result = sub.to_dict()

        assert result["refresh_interval_minutes"] == 120

    def test_includes_refresh_count(self):
        """to_dict includes refresh_count."""
        sub = ConcreteSubscription.create()
        sub.refresh_count = 5

        result = sub.to_dict()

        assert result["refresh_count"] == 5

    def test_includes_error_count(self):
        """to_dict includes error_count."""
        sub = ConcreteSubscription.create()
        sub.error_count = 3

        result = sub.to_dict()

        assert result["error_count"] == 3


class TestSubscriptionEdgeCases:
    """Edge case tests for BaseSubscription."""

    def test_very_short_refresh_interval(self):
        """Handles very short refresh interval."""
        sub = ConcreteSubscription.create(refresh_interval_minutes=1)

        assert sub.refresh_interval_minutes == 1

    def test_very_long_refresh_interval(self):
        """Handles very long refresh interval."""
        sub = ConcreteSubscription.create(
            refresh_interval_minutes=10080
        )  # 1 week

        assert sub.refresh_interval_minutes == 10080

    def test_empty_user_id(self):
        """Handles empty user_id."""
        sub = ConcreteSubscription.create(user_id="")

        assert sub.user_id == ""

    def test_unicode_query(self):
        """Handles unicode in query_or_topic."""
        sub = ConcreteSubscription.create(query_or_topic="日本語のニュース")

        assert sub.query_or_topic == "日本語のニュース"

    def test_multiple_pause_resume_cycles(self):
        """Handles multiple pause/resume cycles."""
        sub = ConcreteSubscription.create()

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

    def test_error_then_success_resets(self):
        """Success after errors resets error count."""
        sub = ConcreteSubscription.create()

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

        assert sub.error_count == 5

        # Successful refresh
        sub.on_refresh_start()
        sub.on_refresh_success({})

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