"""
Deep behavioral tests for BaseSubscription lifecycle.
Tests refresh logic, backoff, pause/resume, interval validation, and serialization.
"""

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

import pytest

from local_deep_research.news.core.base_card import CardSource


def _make_source():
    return CardSource(type="user_search", source_id="s1")


def _make_subscription(**kwargs):
    """Create a BaseSubscription subclass for testing."""
    from local_deep_research.news.subscription_manager.base_subscription import (
        BaseSubscription,
    )

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

        def get_subscription_type(self):
            return "test_subscription"

    defaults = {
        "user_id": "user1",
        "source": _make_source(),
        "query_or_topic": "AI news",
    }
    defaults.update(kwargs)

    with patch(
        "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
    ):
        return TestSub(**defaults)


# --- Init ---


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

    def test_id_generated_when_not_provided(self):
        sub = _make_subscription()
        assert sub.id is not None
        assert len(sub.id) > 0

    def test_custom_id_used(self):
        sub = _make_subscription(subscription_id="custom-123")
        assert sub.id == "custom-123"

    def test_user_id_stored(self):
        sub = _make_subscription(user_id="alice")
        assert sub.user_id == "alice"

    def test_source_stored(self):
        source = _make_source()
        sub = _make_subscription(source=source)
        assert sub.source is source

    def test_query_stored(self):
        sub = _make_subscription(query_or_topic="climate change")
        assert sub.query_or_topic == "climate change"

    def test_default_interval_240(self):
        sub = _make_subscription()
        assert sub.refresh_interval_minutes == 240

    def test_custom_interval(self):
        sub = _make_subscription(refresh_interval_minutes=60)
        assert sub.refresh_interval_minutes == 60

    def test_created_at_set(self):
        sub = _make_subscription()
        assert sub.created_at is not None
        assert sub.created_at.tzinfo is not None

    def test_last_refreshed_none(self):
        sub = _make_subscription()
        assert sub.last_refreshed is None

    def test_is_active_by_default(self):
        sub = _make_subscription()
        assert sub.is_active is True

    def test_refresh_count_zero(self):
        sub = _make_subscription()
        assert sub.refresh_count == 0

    def test_error_count_zero(self):
        sub = _make_subscription()
        assert sub.error_count == 0

    def test_last_error_none(self):
        sub = _make_subscription()
        assert sub.last_error is None

    def test_metadata_empty(self):
        sub = _make_subscription()
        assert sub.metadata == {}


# --- Next refresh calculation ---


class TestCalculateNextRefresh:
    """Tests for _calculate_next_refresh logic."""

    def test_new_subscription_refresh_based_on_created_at(self):
        sub = _make_subscription(refresh_interval_minutes=60)
        expected = sub.created_at + timedelta(minutes=60)
        assert sub.next_refresh == expected

    def test_after_refresh_uses_last_refreshed(self):
        sub = _make_subscription(refresh_interval_minutes=120)
        now = datetime.now(timezone.utc)
        sub.last_refreshed = now
        next_ref = sub._calculate_next_refresh()
        assert next_ref == now + timedelta(minutes=120)


# --- Should refresh ---


class TestShouldRefresh:
    """Tests for should_refresh logic."""

    def test_inactive_never_refreshes(self):
        sub = _make_subscription()
        sub.is_active = False
        assert sub.should_refresh() is False

    def test_not_yet_due(self):
        sub = _make_subscription(refresh_interval_minutes=60)
        # next_refresh is in the future
        sub.next_refresh = datetime.now(timezone.utc) + timedelta(hours=1)
        assert sub.should_refresh() is False

    def test_past_due(self):
        sub = _make_subscription()
        sub.next_refresh = datetime.now(timezone.utc) - timedelta(minutes=1)
        assert sub.should_refresh() is True

    def test_is_due_for_refresh_alias(self):
        sub = _make_subscription()
        sub.next_refresh = datetime.now(timezone.utc) - timedelta(minutes=1)
        assert sub.is_due_for_refresh() is True


# --- Refresh lifecycle ---


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

    def test_sets_last_refreshed(self):
        sub = _make_subscription()
        assert sub.last_refreshed is None
        sub.on_refresh_start()
        assert sub.last_refreshed is not None


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

    def test_increments_refresh_count(self):
        sub = _make_subscription()
        sub.on_refresh_start()
        sub.on_refresh_success({"results": []})
        assert sub.refresh_count == 1

    def test_recalculates_next_refresh(self):
        sub = _make_subscription()
        old_next = sub.next_refresh
        sub.on_refresh_start()
        sub.on_refresh_success({})
        assert sub.next_refresh != old_next

    def test_resets_error_count(self):
        sub = _make_subscription()
        sub.error_count = 5
        sub.on_refresh_start()
        sub.on_refresh_success({})
        assert sub.error_count == 0


class TestOnRefreshError:
    """Tests for on_refresh_error with exponential backoff."""

    def test_increments_error_count(self):
        sub = _make_subscription()
        sub.on_refresh_error(Exception("fail"))
        assert sub.error_count == 1

    def test_stores_last_error(self):
        sub = _make_subscription()
        sub.on_refresh_error(Exception("network timeout"))
        assert sub.last_error == "network timeout"

    def test_exponential_backoff(self):
        sub = _make_subscription(refresh_interval_minutes=60)
        # First error: 60 * 2^1 = 120 minutes
        sub.on_refresh_error(Exception("e1"))
        first_next = sub.next_refresh

        # Second error: 60 * 2^2 = 240 minutes
        sub.on_refresh_error(Exception("e2"))
        second_next = sub.next_refresh

        assert second_next > first_next

    def test_backoff_capped_at_one_week(self):
        sub = _make_subscription(refresh_interval_minutes=60)
        # Simulate many errors
        for i in range(20):
            sub.on_refresh_error(Exception(f"e{i}"))

        max_minutes = 24 * 60 * 7  # 1 week
        now = datetime.now(timezone.utc)
        delta = (sub.next_refresh - now).total_seconds() / 60
        assert delta <= max_minutes + 1  # +1 for timing tolerance

    def test_disables_after_10_errors(self):
        sub = _make_subscription()
        for i in range(10):
            sub.on_refresh_error(Exception(f"error {i}"))
        assert sub.is_active is False

    def test_still_active_at_9_errors(self):
        sub = _make_subscription()
        for i in range(9):
            sub.on_refresh_error(Exception(f"error {i}"))
        assert sub.is_active is True


# --- Pause/Resume ---


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

    def test_pause_deactivates(self):
        sub = _make_subscription()
        sub.pause()
        assert sub.is_active is False

    def test_resume_activates(self):
        sub = _make_subscription()
        sub.pause()
        sub.resume()
        assert sub.is_active is True

    def test_resume_resets_error_count(self):
        sub = _make_subscription()
        sub.error_count = 5
        sub.resume()
        assert sub.error_count == 0

    def test_resume_recalculates_next_refresh(self):
        sub = _make_subscription()
        far_future = datetime.now(timezone.utc) + timedelta(days=365)
        sub.next_refresh = far_future
        sub.resume()
        assert sub.next_refresh < far_future


# --- Update interval ---


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

    def test_updates_interval(self):
        sub = _make_subscription()
        sub.update_interval(120)
        assert sub.refresh_interval_minutes == 120

    def test_recalculates_next_refresh(self):
        sub = _make_subscription()
        old = sub.next_refresh
        sub.update_interval(480)
        assert sub.next_refresh != old

    def test_rejects_under_60_minutes(self):
        sub = _make_subscription()
        with pytest.raises(ValueError, match="at least 60 minutes"):
            sub.update_interval(30)

    def test_rejects_over_30_days(self):
        sub = _make_subscription()
        with pytest.raises(ValueError, match="cannot exceed 30 days"):
            sub.update_interval(60 * 24 * 31)

    def test_accepts_exactly_60(self):
        sub = _make_subscription()
        sub.update_interval(60)
        assert sub.refresh_interval_minutes == 60

    def test_accepts_exactly_30_days(self):
        sub = _make_subscription()
        sub.update_interval(60 * 24 * 30)
        assert sub.refresh_interval_minutes == 60 * 24 * 30


# --- to_dict ---


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

    def test_includes_id(self):
        sub = _make_subscription(subscription_id="sub-1")
        d = sub.to_dict()
        assert d["id"] == "sub-1"

    def test_includes_type(self):
        sub = _make_subscription()
        d = sub.to_dict()
        assert d["type"] == "test_subscription"

    def test_includes_user_id(self):
        sub = _make_subscription(user_id="bob")
        d = sub.to_dict()
        assert d["user_id"] == "bob"

    def test_includes_query_or_topic(self):
        sub = _make_subscription(query_or_topic="quantum computing")
        d = sub.to_dict()
        assert d["query_or_topic"] == "quantum computing"

    def test_source_info(self):
        sub = _make_subscription()
        d = sub.to_dict()
        assert d["source"]["type"] == "user_search"

    def test_created_at_isoformat(self):
        sub = _make_subscription()
        d = sub.to_dict()
        datetime.fromisoformat(d["created_at"])

    def test_last_refreshed_none(self):
        sub = _make_subscription()
        d = sub.to_dict()
        assert d["last_refreshed"] is None

    def test_last_refreshed_after_refresh(self):
        sub = _make_subscription()
        sub.on_refresh_start()
        d = sub.to_dict()
        assert d["last_refreshed"] is not None
        datetime.fromisoformat(d["last_refreshed"])

    def test_next_refresh_isoformat(self):
        sub = _make_subscription()
        d = sub.to_dict()
        datetime.fromisoformat(d["next_refresh"])

    def test_includes_is_active(self):
        sub = _make_subscription()
        d = sub.to_dict()
        assert d["is_active"] is True

    def test_includes_refresh_count(self):
        sub = _make_subscription()
        d = sub.to_dict()
        assert d["refresh_count"] == 0

    def test_includes_error_count(self):
        sub = _make_subscription()
        d = sub.to_dict()
        assert d["error_count"] == 0

    def test_includes_last_error(self):
        sub = _make_subscription()
        d = sub.to_dict()
        assert d["last_error"] is None


# --- Abstract enforcement ---


class TestBaseSubscriptionAbstract:
    """Tests that BaseSubscription is properly abstract."""

    def test_cannot_instantiate_directly(self):
        from local_deep_research.news.subscription_manager.base_subscription import (
            BaseSubscription,
        )

        with patch(
            "local_deep_research.news.subscription_manager.base_subscription.SQLSubscriptionStorage"
        ):
            with pytest.raises(TypeError):
                BaseSubscription(
                    user_id="u1",
                    source=_make_source(),
                    query_or_topic="test",
                )
