"""
Coverage tests for local_deep_research/news/api.py

Tests focus on branches and helpers not covered by existing test_news_api.py /
test_news_api_extended.py:
- get_recommender() singleton behaviour
- _notify_scheduler_about_subscription_change edge paths
- get_news_feed() limit validation and subscription_id filter
- InvalidLimitException propagation
- get_subscriptions / create_subscription / update_subscription / delete_subscription
- get_subscription_cards / get_news_card / rate_card (exception paths)
- _classify_subscription_type / _validate_subscription_data helpers (if present)
"""

from unittest.mock import MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------


def _make_db_session_ctx(rows=None):
    """Return a context-manager mock that yields a session with .query()."""
    if rows is None:
        rows = []
    session = MagicMock()
    query_obj = MagicMock()
    query_obj.filter.return_value = query_obj
    query_obj.order_by.return_value = query_obj
    query_obj.limit.return_value = query_obj
    query_obj.all.return_value = rows
    session.query.return_value = query_obj
    ctx = MagicMock()
    ctx.__enter__ = MagicMock(return_value=session)
    ctx.__exit__ = MagicMock(return_value=False)
    return ctx, session


# ---------------------------------------------------------------------------
# get_recommender
# ---------------------------------------------------------------------------


class TestGetRecommender:
    def test_returns_same_instance_on_repeated_calls(self):
        """get_recommender() creates once and reuses."""
        import local_deep_research.news.api as api_module

        orig = api_module._recommender
        try:
            api_module._recommender = None
            mock_rec = MagicMock()
            with patch(
                "local_deep_research.news.api.TopicBasedRecommender",
                return_value=mock_rec,
            ) as MockRec:
                r1 = api_module.get_recommender()
                r2 = api_module.get_recommender()
                assert r1 is r2
                MockRec.assert_called_once()
        finally:
            api_module._recommender = orig

    def test_existing_instance_not_replaced(self):
        """If _recommender already set, constructor is not called again."""
        import local_deep_research.news.api as api_module

        orig = api_module._recommender
        sentinel = MagicMock()
        try:
            api_module._recommender = sentinel
            with patch(
                "local_deep_research.news.api.TopicBasedRecommender"
            ) as MockRec:
                result = api_module.get_recommender()
                MockRec.assert_not_called()
                assert result is sentinel
        finally:
            api_module._recommender = orig


# ---------------------------------------------------------------------------
# _notify_scheduler_about_subscription_change
# ---------------------------------------------------------------------------


class TestNotifyScheduler:
    def test_no_exception_when_import_fails(self):
        """Should swallow any exception silently."""
        from local_deep_research.news.api import (
            _notify_scheduler_about_subscription_change,
        )

        with patch(
            "local_deep_research.news.api.logger"
        ):  # suppress log output
            # Raises inside because flask.session not available – should be caught
            _notify_scheduler_about_subscription_change("created")

    def test_scheduler_not_running_skips_update(self):
        """If scheduler.is_running is False no update_user_info call is made."""
        from local_deep_research.news.api import (
            _notify_scheduler_about_subscription_change,
        )

        mock_scheduler = MagicMock()
        mock_scheduler.is_running = False

        mock_session = {"username": "alice", "session_id": "s1"}

        with (
            patch("local_deep_research.news.api.logger"),
            patch(
                "flask.session",
                mock_session,
                create=True,
            ),
            patch(
                "local_deep_research.news.api.get_news_scheduler",
                return_value=mock_scheduler,
                create=True,
            ),
        ):
            _notify_scheduler_about_subscription_change("updated")

        mock_scheduler.update_user_info.assert_not_called()


# ---------------------------------------------------------------------------
# get_news_feed – limit validation
# ---------------------------------------------------------------------------


class TestGetNewsFeedLimitValidation:
    def test_zero_limit_raises_invalid_limit(self):
        """limit < 1 must raise InvalidLimitException."""
        from local_deep_research.news.api import get_news_feed
        from local_deep_research.news.exceptions import InvalidLimitException

        with pytest.raises(InvalidLimitException):
            get_news_feed(user_id="user1", limit=0)

    def test_negative_limit_raises_invalid_limit(self):
        """Negative limit triggers the validation guard."""
        from local_deep_research.news.api import get_news_feed
        from local_deep_research.news.exceptions import InvalidLimitException

        with pytest.raises(InvalidLimitException):
            get_news_feed(user_id="user1", limit=-5)

    def test_valid_limit_does_not_raise(self):
        """A valid positive limit proceeds past validation."""
        from local_deep_research.news.api import get_news_feed

        ctx, session = _make_db_session_ctx(rows=[])
        with patch(
            "local_deep_research.database.session_context.get_user_db_session",
            return_value=ctx,
        ):
            result = get_news_feed(user_id="user1", limit=5)
        # Should return a dict (possibly empty news_items) without exception
        assert isinstance(result, dict)


# ---------------------------------------------------------------------------
# get_news_feed – subscription_id filter
# ---------------------------------------------------------------------------


class TestGetNewsFeedSubscriptionFilter:
    def test_subscription_id_all_skips_filter(self):
        """subscription_id='all' should not apply extra query filter."""
        from local_deep_research.news.api import get_news_feed

        ctx, session = _make_db_session_ctx(rows=[])
        with patch(
            "local_deep_research.database.session_context.get_user_db_session",
            return_value=ctx,
        ):
            get_news_feed(user_id="u1", limit=10, subscription_id="all")

        # The filter for a specific subscription_id should not have been called
        # (We can only check .filter was called the 'status' filter only – no additional
        #  subscription_like filter)
        call_count = session.query.return_value.filter.call_count
        # status filter always applied = 1; subscription filter not applied
        assert call_count == 1

    def test_specific_subscription_id_applies_extra_filter(self):
        """A non-'all' subscription_id should add a second .filter call."""
        from local_deep_research.news.api import get_news_feed

        ctx, session = _make_db_session_ctx(rows=[])
        with patch(
            "local_deep_research.database.session_context.get_user_db_session",
            return_value=ctx,
        ):
            get_news_feed(user_id="u1", limit=10, subscription_id="sub-123")

        # Both the status filter and the subscription filter applied
        assert session.query.return_value.filter.call_count >= 2


# ---------------------------------------------------------------------------
# get_news_feed – database exception handling
# ---------------------------------------------------------------------------


class TestGetNewsFeedExceptionHandling:
    def test_db_error_raises_database_access_exception(self):
        """If database access fails get_news_feed raises DatabaseAccessException."""
        from local_deep_research.news.api import get_news_feed
        from local_deep_research.news.exceptions import DatabaseAccessException

        with patch(
            "local_deep_research.database.session_context.get_user_db_session",
            side_effect=Exception("DB down"),
        ):
            with pytest.raises(DatabaseAccessException):
                get_news_feed(user_id="u1", limit=10)


# ---------------------------------------------------------------------------
# Exception classes
# ---------------------------------------------------------------------------


class TestNewsExceptions:
    def test_invalid_limit_exception_message(self):
        from local_deep_research.news.exceptions import InvalidLimitException

        exc = InvalidLimitException(-1)
        assert "-1" in str(exc) or "limit" in str(exc).lower()

    def test_subscription_not_found_exception(self):
        from local_deep_research.news.exceptions import (
            SubscriptionNotFoundException,
        )

        exc = SubscriptionNotFoundException("sub-abc")
        assert "sub-abc" in str(exc)

    def test_news_api_exception_is_base(self):
        from local_deep_research.news.exceptions import (
            NewsAPIException,
            DatabaseAccessException,
        )

        exc = DatabaseAccessException("read", "fail")
        assert isinstance(exc, NewsAPIException)

    def test_news_feed_generation_exception(self):
        from local_deep_research.news.exceptions import (
            NewsFeedGenerationException,
        )

        exc = NewsFeedGenerationException("reason")
        assert isinstance(exc, Exception)
