"""
Comprehensive tests for news/api.py

Tests cover:
- get_news_feed function
- subscription management functions
- notification functions
"""

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


class TestNotifyScheduler:
    """Tests for the _notify_scheduler_about_subscription_change function."""

    def test_notify_scheduler_success(self):
        """Test successful scheduler notification."""
        from local_deep_research.news.api import (
            _notify_scheduler_about_subscription_change,
        )

        mock_scheduler = Mock()
        mock_scheduler.is_running = True

        mock_session = {"username": "testuser", "session_id": "sess123"}

        # flask_session and get_background_job_scheduler are locally imported inside the function
        with patch(
            "local_deep_research.scheduler.background.get_background_job_scheduler",
            return_value=mock_scheduler,
        ):
            with patch(
                "flask.session",
                mock_session,
            ):
                with patch(
                    "local_deep_research.database.session_passwords.session_password_store"
                ) as mock_store:
                    mock_store.get_session_password.return_value = "password123"

                    _notify_scheduler_about_subscription_change(
                        "created", "testuser"
                    )

                    mock_scheduler.update_user_info.assert_called_once_with(
                        "testuser", "password123"
                    )

    def test_notify_scheduler_not_running(self):
        """Test notification when scheduler is not running."""
        from local_deep_research.news.api import (
            _notify_scheduler_about_subscription_change,
        )

        mock_scheduler = Mock()
        mock_scheduler.is_running = False

        with patch(
            "local_deep_research.scheduler.background.get_background_job_scheduler",
            return_value=mock_scheduler,
        ):
            _notify_scheduler_about_subscription_change("updated")

            mock_scheduler.update_user_info.assert_not_called()

    def test_notify_scheduler_no_password(self):
        """Test notification when no password is available."""
        from local_deep_research.news.api import (
            _notify_scheduler_about_subscription_change,
        )

        mock_scheduler = Mock()
        mock_scheduler.is_running = True

        mock_session = {"username": "testuser", "session_id": "sess123"}

        with patch(
            "local_deep_research.scheduler.background.get_background_job_scheduler",
            return_value=mock_scheduler,
        ):
            with patch(
                "flask.session",
                mock_session,
            ):
                with patch(
                    "local_deep_research.database.session_passwords.session_password_store"
                ) as mock_store:
                    mock_store.get_session_password.return_value = None

                    # Should not raise, should log warning
                    _notify_scheduler_about_subscription_change("deleted")

                    mock_scheduler.update_user_info.assert_not_called()

    def test_notify_scheduler_handles_exception(self):
        """Test that exceptions are handled gracefully."""
        from local_deep_research.news.api import (
            _notify_scheduler_about_subscription_change,
        )

        # Patch the local import of get_background_job_scheduler to raise
        with patch(
            "local_deep_research.scheduler.background.get_background_job_scheduler",
            side_effect=Exception("Test error"),
        ):
            # Should not raise
            _notify_scheduler_about_subscription_change("created")


class TestGetNewsFeed:
    """Tests for the get_news_feed function."""

    def test_get_news_feed_invalid_limit(self):
        """Test that invalid limit raises exception."""
        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="test", limit=0)

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

    def test_get_news_feed_success(self):
        """Test successful news feed retrieval."""
        from local_deep_research.news.api import get_news_feed

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            result = get_news_feed(user_id="testuser", limit=10)

            assert "news_items" in result
            assert "total_items" in result
            assert "generated_at" in result

    def test_get_news_feed_with_subscription_filter(self):
        """Test news feed with subscription filter."""
        from local_deep_research.news.api import get_news_feed

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            result = get_news_feed(
                user_id="testuser", limit=10, subscription_id="sub123"
            )

            assert "news_items" in result

    def test_get_news_feed_subscription_filter_uses_json_dumps_spacing(self):
        """Pin the LIKE pattern's spacing for the subscription_id filter
        in get_news_feed. ``research_meta`` is serialized via
        ``json.dumps`` which emits ``"key": "value"`` (space after colon).
        The LIKE pattern MUST include that space, or the filter silently
        matches zero rows."""
        import json
        from local_deep_research.news.api import get_news_feed

        # Capture every .filter() call. The LIKE-on-research_meta call
        # produces a BinaryExpression whose right-hand side is a
        # BindParameter holding the actual pattern string.
        captured_predicates = []

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query

        def _capture_filter(predicate):
            captured_predicates.append(predicate)
            return mock_query

        mock_query.filter.side_effect = _capture_filter
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            get_news_feed(
                user_id="testuser", limit=10, subscription_id="sub_abc"
            )

        # Pull the LIKE pattern string out of whichever captured
        # predicate carries it. Each predicate is a SQLAlchemy clause —
        # the bind value lives at ``.right.value`` for BinaryExpressions.
        like_patterns = []
        for pred in captured_predicates:
            right = getattr(pred, "right", None)
            value = getattr(right, "value", None)
            if isinstance(value, str) and "subscription_id" in value:
                like_patterns.append(value)

        assert like_patterns, (
            f"No subscription_id LIKE pattern captured among "
            f"{len(captured_predicates)} filter calls."
        )
        expected_fragment = json.dumps({"subscription_id": "sub_abc"})[1:-1]
        assert any(expected_fragment in p for p in like_patterns), (
            f"get_news_feed LIKE patterns {like_patterns!r} do not "
            f"match the json.dumps fragment {expected_fragment!r}."
        )

    def test_get_news_feed_handles_database_error(self):
        """Test that database errors are handled."""
        from local_deep_research.news.api import get_news_feed
        from local_deep_research.news.exceptions import (
            DatabaseAccessException,
            NewsFeedGenerationException,
        )

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.side_effect = Exception("Database error")

            # DB errors inside the inner try raise DatabaseAccessException,
            # which is a NewsAPIException and gets re-raised.
            # Errors outside raise NewsFeedGenerationException.
            with pytest.raises(
                (DatabaseAccessException, NewsFeedGenerationException)
            ):
                get_news_feed(user_id="testuser", limit=10)


class TestSubscriptionFunctions:
    """Tests for subscription management functions."""

    def test_create_subscription_success(self):
        """Test successful subscription creation."""
        from local_deep_research.news.api import create_subscription

        mock_session = MagicMock()
        mock_session.add = MagicMock()
        mock_session.commit = MagicMock()

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            with patch(
                "local_deep_research.news.api._notify_scheduler_about_subscription_change"
            ):
                result = create_subscription(
                    user_id="testuser",
                    query="AI News",
                    subscription_type="search",
                    refresh_minutes=240,
                )

                assert result is not None
                assert result["status"] == "success"
                mock_session.add.assert_called_once()

    def test_create_subscription_missing_query(self):
        """Test subscription creation fails without query."""
        from local_deep_research.news.api import create_subscription

        # create_subscription accepts empty string for query but may fail
        # at DB level. The function doesn't validate empty query explicitly,
        # so we test that passing empty query still goes through to the DB layer.
        mock_session = MagicMock()
        mock_session.add = MagicMock()
        mock_session.commit = MagicMock()

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            with patch(
                "local_deep_research.news.api._notify_scheduler_about_subscription_change"
            ):
                # Empty query is accepted by the function
                result = create_subscription(
                    user_id="testuser",
                    query="",
                    refresh_minutes=240,
                )
                assert result is not None

    def test_get_subscriptions_success(self):
        """Test successful subscription retrieval."""
        from local_deep_research.news.api import get_subscriptions

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.all.return_value = []

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            result = get_subscriptions(user_id="testuser")

            assert isinstance(result, dict)
            assert "subscriptions" in result

    def test_get_subscription_success(self):
        """Test successful single subscription retrieval."""
        from local_deep_research.news.api import get_subscription

        mock_session = MagicMock()
        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.name = "AI News"
        mock_subscription.query_or_topic = "AI News"
        mock_subscription.subscription_type = "search"
        mock_subscription.refresh_interval_minutes = 240
        mock_subscription.status = "active"
        mock_subscription.folder_id = None
        mock_subscription.model_provider = None
        mock_subscription.model = None
        mock_subscription.search_strategy = "news_aggregation"
        mock_subscription.custom_endpoint = None
        mock_subscription.search_engine = None
        mock_subscription.search_iterations = 3
        mock_subscription.questions_per_iteration = 5
        mock_subscription.created_at = datetime.now(timezone.utc)
        mock_subscription.updated_at = datetime.now(timezone.utc)

        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_subscription

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            # get_subscription takes only subscription_id
            result = get_subscription(subscription_id="sub123")

            assert result is not None
            assert result["id"] == "sub123"

    def test_get_subscription_not_found(self):
        """Test subscription retrieval when not found."""
        from local_deep_research.news.api import get_subscription
        from local_deep_research.news.exceptions import (
            SubscriptionNotFoundException,
        )

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            with pytest.raises(SubscriptionNotFoundException):
                get_subscription(subscription_id="nonexistent")

    def test_update_subscription_success(self):
        """Test successful subscription update."""
        from local_deep_research.news.api import update_subscription

        mock_session = MagicMock()
        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.name = "AI News"
        mock_subscription.query_or_topic = "AI News"
        mock_subscription.subscription_type = "search"
        mock_subscription.refresh_interval_minutes = 240
        mock_subscription.status = "active"
        mock_subscription.folder_id = None
        mock_subscription.model_provider = None
        mock_subscription.model = None
        mock_subscription.search_strategy = "news_aggregation"
        mock_subscription.custom_endpoint = None
        mock_subscription.search_engine = None
        mock_subscription.search_iterations = 3
        mock_subscription.questions_per_iteration = 5

        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_subscription

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            with patch(
                "local_deep_research.news.api._notify_scheduler_about_subscription_change"
            ):
                # update_subscription takes (subscription_id, data)
                result = update_subscription(
                    subscription_id="sub123",
                    data={"name": "ML News"},
                )

                assert result is not None
                assert result["status"] == "success"

    def test_delete_subscription_success(self):
        """Test successful subscription deletion."""
        from local_deep_research.news.api import delete_subscription

        mock_session = MagicMock()
        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"

        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_subscription

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            with patch(
                "local_deep_research.news.api._notify_scheduler_about_subscription_change"
            ):
                # delete_subscription takes only subscription_id
                result = delete_subscription(subscription_id="sub123")

                assert result["status"] == "success"
                assert result["deleted"] == "sub123"
                mock_session.delete.assert_called_once()


class TestNewsFeedFormatting:
    """Tests for news feed formatting utilities."""

    def test_format_news_item(self):
        """Test news item formatting."""
        # Test that news items are properly formatted from research history
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "AI advances"
        mock_research.report = "Research report content"
        mock_research.created_at = datetime.now(timezone.utc)
        mock_research.research_meta = '{"subscription_id": "sub123"}'

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = [mock_research]

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            result = get_news_feed(user_id="testuser", limit=10)

            assert (
                len(result["news_items"]) >= 0
            )  # May be empty depending on formatting


class TestNewsExceptions:
    """Tests for news API exception handling."""

    def test_invalid_limit_exception_message(self):
        """Test InvalidLimitException message."""
        from local_deep_research.news.exceptions import InvalidLimitException

        exc = InvalidLimitException(-1)
        assert "-1" in str(exc)

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

        exc = SubscriptionNotFoundException("sub123")
        assert "sub123" in str(exc)

    def test_database_access_exception(self):
        """Test DatabaseAccessException."""
        from local_deep_research.news.exceptions import DatabaseAccessException

        # DatabaseAccessException takes (operation, message)
        exc = DatabaseAccessException("test operation", "test error")
        assert "test operation" in str(exc)
        assert "test error" in str(exc)


class TestVoteFunctions:
    """Tests for vote/feedback functions."""

    def test_submit_feedback_upvote(self):
        """Test submitting an upvote."""
        from local_deep_research.news.api import submit_feedback

        mock_session = MagicMock()
        # No existing rating
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        # Count queries return 0
        mock_session.query.return_value.filter_by.return_value.count.return_value = 0

        with patch("flask.has_request_context", return_value=False):
            with patch(
                "local_deep_research.database.session_context.get_user_db_session"
            ) as mock_get_session:
                mock_get_session.return_value.__enter__ = Mock(
                    return_value=mock_session
                )
                mock_get_session.return_value.__exit__ = Mock(
                    return_value=False
                )

                result = submit_feedback(
                    card_id="card123",
                    user_id="testuser",
                    vote="up",
                )

                assert result["success"] is True
                mock_session.add.assert_called_once()

    def test_submit_feedback_downvote(self):
        """Test submitting a downvote."""
        from local_deep_research.news.api import submit_feedback

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_session.query.return_value.filter_by.return_value.count.return_value = 0

        with patch("flask.has_request_context", return_value=False):
            with patch(
                "local_deep_research.database.session_context.get_user_db_session"
            ) as mock_get_session:
                mock_get_session.return_value.__enter__ = Mock(
                    return_value=mock_session
                )
                mock_get_session.return_value.__exit__ = Mock(
                    return_value=False
                )

                result = submit_feedback(
                    card_id="card123",
                    user_id="testuser",
                    vote="down",
                )

                assert result["success"] is True

    def test_submit_feedback_update_existing(self):
        """Test updating an existing vote."""
        from local_deep_research.news.api import submit_feedback

        existing_rating = MagicMock()
        existing_rating.rating_value = "up"

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = existing_rating
        mock_session.query.return_value.filter_by.return_value.count.return_value = 0

        with patch("flask.has_request_context", return_value=False):
            with patch(
                "local_deep_research.database.session_context.get_user_db_session"
            ) as mock_get_session:
                mock_get_session.return_value.__enter__ = Mock(
                    return_value=mock_session
                )
                mock_get_session.return_value.__exit__ = Mock(
                    return_value=False
                )

                result = submit_feedback(
                    card_id="card123",
                    user_id="testuser",
                    vote="down",
                )

                assert result["success"] is True
                # Should update existing vote
                assert existing_rating.rating_value == "down"

    def test_get_votes_for_cards_empty(self):
        """Test getting votes for cards when none exist."""
        from local_deep_research.news.api import get_votes_for_cards

        mock_session = MagicMock()
        # No existing ratings, 0 counts
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_session.query.return_value.filter_by.return_value.count.return_value = 0

        with patch("flask.has_request_context", return_value=False):
            with patch(
                "local_deep_research.database.session_context.get_user_db_session"
            ) as mock_get_session:
                mock_get_session.return_value.__enter__ = Mock(
                    return_value=mock_session
                )
                mock_get_session.return_value.__exit__ = Mock(
                    return_value=False
                )

                result = get_votes_for_cards(
                    card_ids=["card1", "card2"],
                    user_id="testuser",
                )

                assert result["success"] is True
                assert "card1" in result["votes"]
                assert "card2" in result["votes"]

    def test_get_votes_for_cards_with_data(self):
        """Test getting votes for cards with existing votes."""
        from local_deep_research.news.api import get_votes_for_cards

        mock_user_vote = MagicMock()
        mock_user_vote.rating_value = "up"

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_user_vote
        mock_session.query.return_value.filter_by.return_value.count.return_value = 1

        with patch("flask.has_request_context", return_value=False):
            with patch(
                "local_deep_research.database.session_context.get_user_db_session"
            ) as mock_get_session:
                mock_get_session.return_value.__enter__ = Mock(
                    return_value=mock_session
                )
                mock_get_session.return_value.__exit__ = Mock(
                    return_value=False
                )

                result = get_votes_for_cards(
                    card_ids=["card1"],
                    user_id="testuser",
                )

                assert result["votes"]["card1"]["user_vote"] == "up"


class TestSubscriptionHistory:
    """Tests for subscription history functions."""

    def test_get_subscription_history_success(self):
        """Test getting subscription history."""
        from local_deep_research.news.api import get_subscription_history

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.uuid_id = "research123"
        mock_research.query = "AI News"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc)
        mock_research.completed_at = datetime.now(timezone.utc)
        mock_research.research_meta = '{"subscription_id": "sub123"}'

        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.query_or_topic = "AI News"
        mock_subscription.subscription_type = "search"
        mock_subscription.refresh_interval_minutes = 240
        mock_subscription.refresh_count = 0
        mock_subscription.created_at = datetime.now(timezone.utc)
        mock_subscription.next_refresh = None

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        # First call: subscription lookup via filter_by
        mock_query.filter_by.return_value.first.return_value = mock_subscription
        # Subsequent calls: research history via filter
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = [mock_research]

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            result = get_subscription_history(
                subscription_id="sub123",
                limit=10,
            )

            assert "history" in result
            assert len(result["history"]) == 1

    def test_get_subscription_history_empty(self):
        """Test getting subscription history when empty."""
        from local_deep_research.news.api import get_subscription_history

        mock_session = MagicMock()
        mock_query = MagicMock()
        # Need to handle the subscription lookup (first with block)
        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.query_or_topic = "AI"
        mock_subscription.subscription_type = "search"
        mock_subscription.refresh_interval_minutes = 240
        mock_subscription.refresh_count = 0
        mock_subscription.created_at = datetime.now(timezone.utc)
        mock_subscription.next_refresh = None
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_subscription
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value.first.return_value = mock_subscription
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            result = get_subscription_history(
                subscription_id="sub123",
                limit=10,
            )

            assert "history" in result
            assert len(result["history"]) == 0

    def test_get_subscription_history_uses_flask_session_user(self):
        """Regression guard for the 'anonymous-DB' bug. ``NewsSubscription``
        has no ``user_id`` column, so a previous version that did
        ``subscription_dict.get("user_id", "anonymous")`` ALWAYS opened the
        anonymous DB and silently returned an empty history for every real
        multi-user deployment. The fix calls ``get_user_db_session()`` with
        no argument so it falls back to the Flask session username — same
        resolution path as the first call inside the function."""
        import json
        from local_deep_research.news.api import get_subscription_history

        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.query_or_topic = "AI"
        mock_subscription.subscription_type = "search"
        mock_subscription.refresh_interval_minutes = 240
        mock_subscription.refresh_count = 0
        mock_subscription.created_at = datetime.now(timezone.utc)
        mock_subscription.next_refresh = None

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value.first.return_value = mock_subscription
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            get_subscription_history(subscription_id="sub123", limit=10)

            # Both call sites — the subscription lookup AND the history
            # query — must use the Flask-session-derived username (no
            # positional or kw arg). A regression would re-introduce an
            # explicit username like "anonymous" here.
            assert mock_get_session.call_count >= 2
            for call in mock_get_session.call_args_list:
                assert call.args == (), (
                    f"get_user_db_session called with positional args "
                    f"{call.args} — should be argument-less so Flask "
                    f"session resolves the username."
                )
                assert "username" not in call.kwargs, (
                    f"get_user_db_session called with explicit username "
                    f"{call.kwargs.get('username')!r} — should be "
                    f"argument-less."
                )
                assert "user_id" not in call.kwargs

        # Also pin the json.dumps spacing invariant: the LIKE pattern in
        # the source MUST match the format json.dumps produces for the
        # subscription_id key, or the filter silently matches zero rows.
        sample_meta = json.dumps({"subscription_id": "sub123"})
        # json.dumps emits a space after the colon by default. The fix
        # adds this space to the LIKE pattern.
        assert '"subscription_id": "sub123"' in sample_meta

    def test_get_subscription_history_like_pattern_matches_json_dumps(self):
        """Pin the LIKE pattern's spacing so a future refactor that
        switches to ``json.dumps(separators=(",", ":"))`` (no space) or
        the reverse on the LIKE side would break this test loudly rather
        than silently matching zero rows in production."""
        import json
        from local_deep_research.news.api import get_subscription_history

        mock_subscription = MagicMock()
        mock_subscription.id = "sub_xyz"

        captured_predicates = []

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value.first.return_value = mock_subscription

        def _capture_filter(predicate):
            captured_predicates.append(predicate)
            return mock_query

        mock_query.filter.side_effect = _capture_filter
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_get_session:
            mock_get_session.return_value.__enter__ = Mock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = Mock(return_value=False)

            get_subscription_history(subscription_id="sub_xyz", limit=10)

        like_patterns = []
        for pred in captured_predicates:
            right = getattr(pred, "right", None)
            value = getattr(right, "value", None)
            if isinstance(value, str) and "subscription_id" in value:
                like_patterns.append(value)

        assert like_patterns, (
            f"No subscription_id LIKE pattern captured among "
            f"{len(captured_predicates)} filter calls."
        )
        expected_fragment = json.dumps({"subscription_id": "sub_xyz"})[1:-1]
        assert any(expected_fragment in p for p in like_patterns), (
            f"LIKE patterns {like_patterns!r} do not contain the "
            f"json.dumps fragment {expected_fragment!r} — the spacing "
            f"must match or the filter silently matches zero rows."
        )


class TestTimeFormatting:
    """Tests for time formatting utilities."""

    def test_format_time_ago_recent(self):
        """Test formatting time for recent timestamps."""
        from local_deep_research.news.api import _format_time_ago

        now = datetime.now(timezone.utc)
        # _format_time_ago takes a string, not datetime
        result = _format_time_ago(now.isoformat())

        # Returns "Just now" for recent timestamps
        assert "just now" in result.lower() or "second" in result.lower()

    def test_format_time_ago_hours(self):
        """Test formatting time for hours ago."""
        from local_deep_research.news.api import _format_time_ago

        hours_ago = datetime.now(timezone.utc) - timedelta(hours=3)

        result = _format_time_ago(hours_ago.isoformat())

        assert "hour" in result.lower()

    def test_format_time_ago_days(self):
        """Test formatting time for days ago."""
        from local_deep_research.news.api import _format_time_ago

        days_ago = datetime.now(timezone.utc) - timedelta(days=2)

        result = _format_time_ago(days_ago.isoformat())

        assert "day" in result.lower()

    def test_format_time_ago_none(self):
        """None is not a valid timestamp and raises (caller logs + skips)."""
        from local_deep_research.news.api import _format_time_ago

        with pytest.raises(TypeError):
            _format_time_ago(None)
