"""
Extended tests for news/api.py.

Tests cover:
- Error handling in get_news_feed
- Metadata extraction failures
- Malformed research_meta JSON handling
- Concurrent subscription operations
- Subscription scheduling logic
- News item filtering logic
- Focus area and search strategy parameters
- _format_time_ago edge cases
- Scheduler notification failure handling
- Rate limiting and pagination
"""

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

import pytest


class TestGetNewsFeedErrorHandling:
    """Tests for error handling in get_news_feed."""

    def test_invalid_limit_zero(self):
        """Test invalid limit of 0 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)

    def test_invalid_limit_negative(self):
        """Test negative 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=-10)

    def test_database_connection_error(self):
        """Test database connection error is handled."""
        from local_deep_research.news.api import get_news_feed

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_session:
            mock_session.side_effect = Exception("Database connection failed")

            with pytest.raises(Exception):
                get_news_feed(user_id="testuser", limit=10)

    def test_query_execution_error(self):
        """Test query execution error is handled."""
        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.side_effect = Exception("Query failed")

        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(Exception):
                get_news_feed(user_id="testuser", limit=10)


class TestMetadataExtractionFailures:
    """Tests for metadata extraction failure handling."""

    def test_malformed_json_in_research_meta(self):
        """Test handling of malformed JSON in research_meta."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "breaking news today"  # News-like query
        mock_research.title = None
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Some content"
        mock_research.research_meta = "{invalid json"  # Malformed JSON

        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)

            # Should not raise, should handle gracefully
            result = get_news_feed(user_id="testuser", limit=10)

            assert "news_items" in result

    def test_none_research_meta(self):
        """Test handling of None research_meta."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "latest news stories"
        mock_research.title = "News Title"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Some content"
        mock_research.research_meta = None

        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 "news_items" in result

    def test_empty_string_research_meta(self):
        """Test handling of empty string research_meta."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "breaking news update"
        mock_research.title = "Breaking News"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Content here"
        mock_research.research_meta = ""

        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 "news_items" in result

    def test_dict_research_meta(self):
        """Test handling of dict research_meta (already parsed)."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "breaking news today"
        mock_research.title = "News Title"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Content"
        mock_research.research_meta = {
            "is_news_search": True,
            "generated_headline": "Test Headline",
        }

        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 "news_items" in result


class TestSubscriptionSchedulingLogic:
    """Tests for subscription scheduling logic."""

    def test_next_refresh_calculation(self):
        """Test next refresh time calculation."""
        from local_deep_research.news.api import create_subscription

        mock_session = 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",
                    refresh_minutes=60,
                )

                assert result is not None
                # Verify add was called with subscription object
                mock_session.add.assert_called_once()

    def test_subscription_interval_update(self):
        """Test subscription interval update recalculates next_refresh."""
        from local_deep_research.news.api import update_subscription

        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.refresh_interval_minutes = 60
        mock_subscription.next_refresh = datetime.now(timezone.utc)

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.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("sub123", {"refresh_interval_minutes": 120})

                # Verify next_refresh was updated
                assert mock_subscription.refresh_interval_minutes == 120


class TestNewsItemFilteringLogic:
    """Tests for news item filtering logic."""

    def test_filter_by_subscription_id(self):
        """Test filtering by subscription_id."""
        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)

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

            # Filter should have been called for subscription_id
            assert mock_query.filter.called

    def test_filter_all_subscriptions(self):
        """Test 'all' subscription filter doesn't add extra 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="all"
            )

            assert "news_items" in result

    def test_news_query_detection_breaking_news(self):
        """Test news query detection for 'breaking news'."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "breaking news about technology"
        mock_research.title = "Tech Breaking News"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Breaking tech news content"
        mock_research.research_meta = "{}"

        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 "news_items" in result

    def test_news_query_detection_latest_news(self):
        """Test news query detection for 'latest news'."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "latest news in AI"
        mock_research.title = "Latest AI News"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "AI news content"
        mock_research.research_meta = "{}"

        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 "news_items" in result


class TestFormatTimeAgo:
    """Tests for _format_time_ago edge cases."""

    def test_format_just_now(self):
        """Test formatting for just now (< 60 seconds)."""
        from local_deep_research.news.api import _format_time_ago

        now = datetime.now(timezone.utc)
        result = _format_time_ago(now.isoformat())

        assert "now" in result.lower() or "second" in result.lower()

    def test_format_minutes_ago(self):
        """Test formatting for minutes ago."""
        from local_deep_research.news.api import _format_time_ago

        minutes_ago = datetime.now(timezone.utc) - timedelta(minutes=30)
        result = _format_time_ago(minutes_ago.isoformat())

        assert "minute" in result.lower()

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

        hours_ago = datetime.now(timezone.utc) - timedelta(hours=5)
        result = _format_time_ago(hours_ago.isoformat())

        assert "hour" in result.lower()

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

        days_ago = datetime.now(timezone.utc) - timedelta(days=3)
        result = _format_time_ago(days_ago.isoformat())

        assert "day" in result.lower()

    def test_format_singular_day(self):
        """Test singular 'day' for 1 day ago."""
        from local_deep_research.news.api import _format_time_ago

        one_day_ago = datetime.now(timezone.utc) - timedelta(days=1)
        result = _format_time_ago(one_day_ago.isoformat())

        assert "1 day ago" in result

    def test_format_singular_hour(self):
        """Test singular 'hour' for slightly more than 1 hour ago."""
        from local_deep_research.news.api import _format_time_ago

        # Use 1 hour + 1 second to trigger the hour branch (> 3600)
        one_hour_plus = datetime.now(timezone.utc) - timedelta(
            hours=1, seconds=1
        )
        result = _format_time_ago(one_hour_plus.isoformat())

        assert "1 hour ago" in result

    def test_format_singular_minute(self):
        """Test singular 'minute' for slightly more than 1 minute ago."""
        from local_deep_research.news.api import _format_time_ago

        # Use 1 minute + 1 second to trigger the minute branch (> 60)
        one_minute_plus = datetime.now(timezone.utc) - timedelta(
            minutes=1, seconds=1
        )
        result = _format_time_ago(one_minute_plus.isoformat())

        assert "1 minute ago" in result

    def test_format_invalid_timestamp(self):
        """Test formatting with invalid timestamp."""
        from local_deep_research.news.api import _format_time_ago

        result = _format_time_ago("invalid-timestamp")

        assert result == "Recently"

    def test_format_naive_datetime(self):
        """Test formatting with naive datetime string assumes UTC."""
        from local_deep_research.news.api import _format_time_ago

        # Naive datetime string (no timezone) - code assumes UTC
        # Use a time that definitely falls in the hour range
        naive_dt = datetime.now(timezone.utc) - timedelta(hours=2, seconds=1)
        # Strip timezone for the test to simulate naive datetime
        naive_str = naive_dt.replace(tzinfo=None).isoformat()
        result = _format_time_ago(naive_str)

        # Should return "2 hours ago" (naive dt assumed to be UTC)
        assert "hours" in result.lower() or "hour" in result.lower()


class TestSchedulerNotificationFailures:
    """Tests for scheduler notification failure handling."""

    def test_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,
        ):
            # Should not raise
            _notify_scheduler_about_subscription_change("created")

            mock_scheduler.update_user_info.assert_not_called()

    def test_scheduler_exception_handled(self):
        """Test scheduler exception is handled gracefully."""
        from local_deep_research.news.api import (
            _notify_scheduler_about_subscription_change,
        )

        with patch(
            "local_deep_research.scheduler.background.get_background_job_scheduler",
            side_effect=Exception("Scheduler error"),
        ):
            # Should not raise
            _notify_scheduler_about_subscription_change("updated")

    def test_no_password_available(self):
        """Test notification when no password 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
                    _notify_scheduler_about_subscription_change("deleted")

                    mock_scheduler.update_user_info.assert_not_called()

    def test_fallback_to_user_id(self):
        """Test fallback to user_id when username not in session."""
        from local_deep_research.news.api import (
            _notify_scheduler_about_subscription_change,
        )

        mock_scheduler = Mock()
        mock_scheduler.is_running = True

        mock_session = {"session_id": "sess123"}  # No username

        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 = "password"

                    _notify_scheduler_about_subscription_change(
                        "created", user_id="fallback_user"
                    )

                    # Should use fallback_user
                    mock_scheduler.update_user_info.assert_called_once_with(
                        "fallback_user", "password"
                    )


class TestFocusAreaAndSearchStrategy:
    """Tests for focus area and search strategy parameters."""

    def test_focus_parameter_in_response(self):
        """Test focus parameter is included in response."""
        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, focus="technology"
            )

            assert result["focus"] == "technology"

    def test_search_strategy_parameter_in_response(self):
        """Test search strategy parameter is included in response."""
        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, search_strategy="news_aggregation"
            )

            assert result["search_strategy"] == "news_aggregation"

    def test_default_search_strategy(self):
        """Test default search strategy when not specified."""
        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 result["search_strategy"] == "default"


class TestSubscriptionOperations:
    """Tests for subscription CRUD operations."""

    def test_create_subscription_all_parameters(self):
        """Test subscription creation with all parameters."""
        from local_deep_research.news.api import create_subscription

        mock_session = 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=120,
                    model_provider="openai",
                    model="gpt-4",
                    search_strategy="deep_analysis",
                    name="My AI Subscription",
                    folder_id="folder123",
                    is_active=True,
                    search_engine="google",
                    search_iterations=5,
                    questions_per_iteration=3,
                )

                assert result["status"] == "success"
                mock_session.add.assert_called_once()
                mock_session.commit.assert_called_once()

    def test_update_subscription_name(self):
        """Test updating subscription name."""
        from local_deep_research.news.api import update_subscription

        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.name = "Old Name"

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.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("sub123", {"name": "New Name"})

                assert mock_subscription.name == "New Name"

    def test_update_subscription_status(self):
        """Test updating subscription status."""
        from local_deep_research.news.api import update_subscription

        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.status = "active"

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.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("sub123", {"is_active": False})

                assert mock_subscription.status == "paused"

    def test_delete_subscription_not_found(self):
        """Test deleting nonexistent subscription."""
        from local_deep_research.news.api import delete_subscription
        from local_deep_research.news.exceptions import (
            SubscriptionNotFoundException,
        )

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.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):
                delete_subscription("nonexistent")


class TestGetSubscription:
    """Tests for get_subscription functionality."""

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

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.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("nonexistent")


class TestGetSubscriptions:
    """Tests for get_subscriptions functionality."""

    def test_get_subscriptions_empty(self):
        """Test get_subscriptions returns empty list."""
        from local_deep_research.news.api import get_subscriptions

        mock_session = MagicMock()
        mock_session.query.return_value.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 result["subscriptions"] == []
            assert result["total"] == 0


class TestSubscriptionHistory:
    """Tests for get_subscription_history functionality."""

    def test_subscription_history_not_found(self):
        """Test subscription history raises when subscription not found."""
        from local_deep_research.news.api import get_subscription_history
        from local_deep_research.news.exceptions import (
            SubscriptionNotFoundException,
        )

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.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_history("nonexistent")


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

    def test_submit_feedback_invalid_vote(self):
        """Test submit_feedback rejects invalid vote type."""
        from local_deep_research.news.api import submit_feedback

        # Vote validation happens before has_request_context check
        with pytest.raises(ValueError, match="Invalid vote type"):
            submit_feedback(
                card_id="card123", user_id="testuser", vote="invalid"
            )

    def test_get_votes_no_username(self):
        """Test get_votes_for_cards raises when no username and no context."""
        from local_deep_research.news.api import get_votes_for_cards

        # Mock flask.has_request_context since it's imported inside the function
        with patch("flask.has_request_context", return_value=False):
            with pytest.raises(ValueError, match="No username provided"):
                get_votes_for_cards(card_ids=["card1"], user_id=None)


class TestNotImplementedFunctions:
    """Tests for not-implemented functions."""

    def test_research_news_item_raises(self):
        """Test research_news_item raises NotImplementedException."""
        from local_deep_research.news.api import research_news_item
        from local_deep_research.news.exceptions import NotImplementedException

        with pytest.raises(NotImplementedException):
            research_news_item("card123", "detailed")

    def test_save_news_preferences_raises(self):
        """Test save_news_preferences raises NotImplementedException."""
        from local_deep_research.news.api import save_news_preferences
        from local_deep_research.news.exceptions import NotImplementedException

        with pytest.raises(NotImplementedException):
            save_news_preferences("testuser", {"theme": "dark"})

    def test_get_news_categories_raises(self):
        """Test get_news_categories raises NotImplementedException."""
        from local_deep_research.news.api import get_news_categories
        from local_deep_research.news.exceptions import NotImplementedException

        with pytest.raises(NotImplementedException):
            get_news_categories()


class TestLinkExtraction:
    """Tests for link extraction from report content."""

    def test_extract_links_from_content(self):
        """Test links are extracted from report content."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "breaking news today"
        mock_research.title = "Breaking News"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = """
        [1] First Source
        URL: https://example.com/article1

        [2] Second Source
        URL: https://example.com/article2
        """
        mock_research.research_meta = json.dumps(
            {"is_news_search": True, "generated_headline": "Test"}
        )

        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)

            # Should have extracted links
            if result["news_items"]:
                news_item = result["news_items"][0]
                assert "links" in news_item


class TestResponseStructure:
    """Tests for response structure."""

    def test_news_feed_response_structure(self):
        """Test news feed response has correct structure."""
        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 "generated_at" in result
            assert "focus" in result
            assert "search_strategy" in result
            assert "total_items" in result
            assert "source" in result


class TestFormatTimeAgoExtended:
    """Extended tests for _format_time_ago edge cases."""

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

        days_ago = datetime.now(timezone.utc) - timedelta(days=5)
        result = _format_time_ago(days_ago.isoformat())

        assert "5 days ago" in result

    def test_format_many_hours(self):
        """Test formatting for many hours ago (not yet days)."""
        from local_deep_research.news.api import _format_time_ago

        # Just under 24 hours
        hours_ago = datetime.now(timezone.utc) - timedelta(hours=23)
        result = _format_time_ago(hours_ago.isoformat())

        assert "hour" in result.lower()

    def test_format_exactly_one_hour(self):
        """Test formatting for exactly one hour boundary."""
        from local_deep_research.news.api import _format_time_ago

        # Use 3601 seconds to just cross the hour boundary
        one_hour = datetime.now(timezone.utc) - timedelta(seconds=3601)
        result = _format_time_ago(one_hour.isoformat())

        assert "1 hour ago" in result

    def test_format_with_datetime_object(self):
        """Test _format_time_ago with datetime object (if supported)."""
        from local_deep_research.news.api import _format_time_ago

        # The function uses dateutil.parser.parse which handles strings
        # Test with valid ISO format string
        dt = datetime.now(timezone.utc) - timedelta(minutes=45)
        result = _format_time_ago(dt.isoformat())

        assert "minute" in result.lower()

    def test_format_future_timestamp(self):
        """Test formatting for future timestamp (edge case)."""
        from local_deep_research.news.api import _format_time_ago

        future = datetime.now(timezone.utc) + timedelta(hours=1)
        result = _format_time_ago(future.isoformat())

        # Negative diff.days would be -1, so it's not > 0
        # Future times should show "Just now" or "Recently"
        assert result is not None

    def test_format_very_old_timestamp(self):
        """Test formatting for very old timestamp."""
        from local_deep_research.news.api import _format_time_ago

        old = datetime.now(timezone.utc) - timedelta(days=365)
        result = _format_time_ago(old.isoformat())

        assert "365 days ago" in result or "day" in result.lower()


class TestNewsItemFiltering:
    """Tests for news item filtering logic."""

    def test_skip_in_progress_items(self):
        """Test that in_progress items are skipped."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "breaking news today"
        mock_research.title = "News Title"
        mock_research.status = "in_progress"  # Should be skipped
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Content"
        mock_research.research_meta = json.dumps({"is_news_search": True})

        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)

            # In-progress items should be skipped
            assert len(result["news_items"]) == 0

    def test_skip_suspended_items(self):
        """Test that suspended items are skipped."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "breaking news"
        mock_research.title = "News"
        mock_research.status = "suspended"  # Should be skipped
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Content"
        mock_research.research_meta = json.dumps({"is_news_search": True})

        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

    def test_skip_items_without_content(self):
        """Test that items without content are skipped."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "breaking news"
        mock_research.title = "News Title"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = None  # No content
        mock_research.research_meta = json.dumps({"is_news_search": True})

        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


class TestSubscriptionUpdates:
    """Tests for subscription update edge cases."""

    def test_update_subscription_query(self):
        """Test updating subscription query."""
        from local_deep_research.news.api import update_subscription

        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.query_or_topic = "Old Query"

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.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("sub123", {"query_or_topic": "New Query"})

                assert mock_subscription.query_or_topic == "New Query"

    def test_update_subscription_folder_id(self):
        """Test updating subscription folder_id."""
        from local_deep_research.news.api import update_subscription

        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.folder_id = None

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.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("sub123", {"folder_id": "folder456"})

                assert mock_subscription.folder_id == "folder456"

    def test_update_subscription_model_settings(self):
        """Test updating subscription model settings."""
        from local_deep_research.news.api import update_subscription

        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.model_provider = "openai"
        mock_subscription.model = "gpt-3.5-turbo"

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.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(
                    "sub123",
                    {"model_provider": "anthropic", "model": "claude-3"},
                )

                assert mock_subscription.model_provider == "anthropic"
                assert mock_subscription.model == "claude-3"

    def test_update_subscription_search_settings(self):
        """Test updating subscription search settings."""
        from local_deep_research.news.api import update_subscription

        mock_subscription = MagicMock()
        mock_subscription.id = "sub123"
        mock_subscription.search_engine = "google"
        mock_subscription.search_iterations = 3

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.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(
                    "sub123",
                    {
                        "search_engine": "bing",
                        "search_iterations": 5,
                        "questions_per_iteration": 10,
                    },
                )

                assert mock_subscription.search_engine == "bing"
                assert mock_subscription.search_iterations == 5
                assert mock_subscription.questions_per_iteration == 10


class TestCreateSubscriptionDefaults:
    """Tests for subscription creation default handling."""

    def test_create_subscription_default_refresh_minutes(self):
        """Test subscription creation with default refresh_minutes."""
        from local_deep_research.news.api import create_subscription

        mock_session = 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"
            ):
                # Don't provide refresh_minutes
                result = create_subscription(
                    user_id="testuser",
                    query="AI News",
                )

                assert result["status"] == "success"
                # Default should be 240
                assert result["refresh_minutes"] == 240

    def test_create_subscription_inactive(self):
        """Test creating inactive subscription."""
        from local_deep_research.news.api import create_subscription

        mock_session = 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",
                    refresh_minutes=60,
                    is_active=False,
                )

                assert result["status"] == "success"


class TestNewsQueryDetection:
    """Tests for news query detection patterns."""

    def test_detection_today_news_pattern(self):
        """Test 'today' + 'news' pattern detection."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "what happened today in news"
        mock_research.title = "Today News"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "News content here"
        mock_research.research_meta = "{}"

        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)

            # Should be recognized as news query
            assert "news_items" in result

    def test_detection_news_stories_pattern(self):
        """Test 'news stories' pattern detection."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "top news stories this week"
        mock_research.title = "Top Stories"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Story content"
        mock_research.research_meta = "{}"

        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 "news_items" in result

    def test_detection_search_type_metadata(self):
        """Test search_type metadata detection."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "some query"
        mock_research.title = "Title"
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Content"
        mock_research.research_meta = json.dumps(
            {"search_type": "news_analysis"}
        )

        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 "news_items" in result


class TestHeadlineGeneration:
    """Tests for headline generation/fallback logic."""

    def test_headline_from_subscription_name(self):
        """Test headline generation from subscription name."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "some search"
        mock_research.title = None  # No title
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Content here"
        mock_research.research_meta = json.dumps(
            {
                "is_news_search": True,
                "subscription_name": "AI Daily Update",
            }
        )

        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)

            if result["news_items"]:
                assert "AI Daily Update" in result["news_items"][0]["headline"]

    def test_headline_from_query_fallback(self):
        """Test headline generation falls back to query."""
        from local_deep_research.news.api import get_news_feed

        mock_research = MagicMock()
        mock_research.id = "research123"
        mock_research.query = "AI developments in healthcare sector analysis"
        mock_research.title = None
        mock_research.status = "completed"
        mock_research.created_at = datetime.now(timezone.utc).isoformat()
        mock_research.completed_at = None
        mock_research.duration_seconds = None
        mock_research.report_path = None
        mock_research.report_content = "Content"
        mock_research.research_meta = json.dumps({"is_news_search": True})

        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)

            if result["news_items"]:
                # Should contain truncated query
                assert "News:" in result["news_items"][0]["headline"]
