"""
Comprehensive tests for news/api.py

Tests cover:
- get_recommender function
- 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 TestGetRecommender:
    """Tests for the get_recommender function."""

    def test_get_recommender_creates_instance(self):
        """Test that get_recommender creates an instance."""
        import local_deep_research.news.api as api_module

        # Reset the global recommender
        api_module._recommender = None

        with patch(
            "local_deep_research.news.api.TopicBasedRecommender"
        ) as mock_recommender:
            mock_instance = Mock()
            mock_recommender.return_value = mock_instance

            result = api_module.get_recommender()

            assert result == mock_instance
            mock_recommender.assert_called_once()

    def test_get_recommender_returns_cached_instance(self):
        """Test that get_recommender returns cached instance."""
        import local_deep_research.news.api as api_module

        mock_instance = Mock()
        api_module._recommender = mock_instance

        with patch(
            "local_deep_research.news.api.TopicBasedRecommender"
        ) as mock_recommender:
            result = api_module.get_recommender()

            assert result == mock_instance
            mock_recommender.assert_not_called()

        # Reset for other tests
        api_module._recommender = None


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_news_scheduler are locally imported inside the function
        with patch(
            "local_deep_research.news.subscription_manager.scheduler.get_news_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.news.subscription_manager.scheduler.get_news_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.news.subscription_manager.scheduler.get_news_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_news_scheduler to raise
        with patch(
            "local_deep_research.news.subscription_manager.scheduler.get_news_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_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


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):
        """Test formatting time with None input."""
        from local_deep_research.news.api import _format_time_ago

        # None will cause an error, returns "Recently" on exception
        result = _format_time_ago(None)

        assert result == "Recently"
