"""
Tests for the news API routes.

Tests cover:
- News feed retrieval
- Subscription management
- Feedback submission
- News preferences
- Categories
"""

from unittest.mock import patch

import pytest


class TestGetNewsFeed:
    """Tests for get_news_feed endpoint."""

    def test_get_news_feed_success(self, authenticated_client):
        """Get news feed returns feed items."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.get_news_feed"
        ) as mock_get_feed:
            mock_get_feed.return_value = {"items": [], "total": 0}

            response = authenticated_client.get("/api/news/feed")

            assert response.status_code == 200
            data = response.get_json()
            assert "items" in data

    def test_get_news_feed_with_params(self, authenticated_client):
        """Get news feed with query parameters."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.get_news_feed"
        ) as mock_get_feed:
            mock_get_feed.return_value = {"items": [], "total": 0}

            response = authenticated_client.get(
                "/api/news/feed?limit=10&use_cache=false&focus=technology"
            )

            assert response.status_code == 200
            mock_get_feed.assert_called_once()
            call_kwargs = mock_get_feed.call_args[1]
            assert call_kwargs["limit"] == 10
            assert call_kwargs["use_cache"] is False
            assert call_kwargs["focus"] == "technology"

    def test_get_news_feed_rejects_wildcard_subscription_id(
        self, authenticated_client
    ):
        """A wildcard subscription_id=% would expand the LIKE pattern in
        news/api.py to match every subscription. The route must reject
        it with 400 before the query runs.
        """
        response = authenticated_client.get("/api/news/feed?subscription_id=%")
        assert response.status_code == 400

    def test_get_news_feed_accepts_valid_uuid_subscription_id(
        self, authenticated_client
    ):
        """A real UUID passes the format check and reaches the api layer."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.get_news_feed"
        ) as mock_get_feed:
            mock_get_feed.return_value = {"items": [], "total": 0}
            response = authenticated_client.get(
                "/api/news/feed?subscription_id=11111111-2222-3333-4444-555555555555"
            )
            assert response.status_code == 200
            mock_get_feed.assert_called_once()

    def test_get_subscription_history_rejects_wildcard(
        self, authenticated_client
    ):
        """Same wildcard guard at the path-parameter route."""
        # `%25` is the URL-encoded `%`; without encoding, Flask's URL
        # router treats `%` as a literal that fails to route.
        response = authenticated_client.get(
            "/api/news/subscriptions/%25/history"
        )
        assert response.status_code == 400

    def test_get_news_feed_exception(self, authenticated_client):
        """Get news feed handles exceptions."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.get_news_feed"
        ) as mock_get_feed:
            mock_get_feed.side_effect = Exception("Database error")

            response = authenticated_client.get("/api/news/feed")

            assert response.status_code == 500
            data = response.get_json()
            assert "error" in data


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

    def test_get_subscriptions_success(self, authenticated_client):
        """Get subscriptions returns list."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.get_subscriptions"
        ) as mock_get:
            mock_get.return_value = {"subscriptions": []}

            response = authenticated_client.get("/api/news/subscriptions")

            assert response.status_code == 200
            data = response.get_json()
            assert "subscriptions" in data


class TestCreateSubscription:
    """Tests for create_subscription endpoint."""

    def test_create_subscription_success(self, authenticated_client):
        """Create subscription succeeds."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.create_subscription"
        ) as mock_create:
            mock_create.return_value = {"id": "sub-123", "query": "Test"}

            response = authenticated_client.post(
                "/api/news/subscriptions",
                json={"query": "Test query", "type": "search"},
                content_type="application/json",
            )

            assert response.status_code == 201
            data = response.get_json()
            assert "id" in data

    def test_create_subscription_with_all_params(self, authenticated_client):
        """Create subscription with all parameters."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.create_subscription"
        ) as mock_create:
            mock_create.return_value = {"id": "sub-123"}

            response = authenticated_client.post(
                "/api/news/subscriptions",
                json={
                    "query": "Test",
                    "type": "search",
                    "refresh_minutes": 60,
                    "model_provider": "ollama",
                    "model": "llama3",
                    "search_strategy": "standard",
                    "name": "My Subscription",
                    "is_active": True,
                    "search_engine": "searxng",
                    "search_iterations": 3,
                    "questions_per_iteration": 2,
                },
                content_type="application/json",
            )

            assert response.status_code == 201
            mock_create.assert_called_once()


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

    def test_get_subscription_success(self, authenticated_client):
        """Get single subscription returns data."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.get_subscription"
        ) as mock_get:
            mock_get.return_value = {"id": "sub-123", "query": "Test"}

            response = authenticated_client.get(
                "/api/news/subscriptions/sub-123"
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["id"] == "sub-123"


class TestUpdateSubscription:
    """Tests for update_subscription endpoint."""

    def test_update_subscription_put(self, authenticated_client):
        """Update subscription via PUT."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.update_subscription"
        ) as mock_update:
            mock_update.return_value = {"id": "sub-123", "query": "Updated"}

            response = authenticated_client.put(
                "/api/news/subscriptions/sub-123",
                json={"query": "Updated query"},
                content_type="application/json",
            )

            assert response.status_code == 200

    def test_update_subscription_patch(self, authenticated_client):
        """Update subscription via PATCH."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.update_subscription"
        ) as mock_update:
            mock_update.return_value = {"id": "sub-123"}

            response = authenticated_client.patch(
                "/api/news/subscriptions/sub-123",
                json={"is_active": False},
                content_type="application/json",
            )

            assert response.status_code == 200


class TestCustomEndpointSSRF:
    """create_subscription and update_subscription must reject custom_endpoint
    URLs that target cloud metadata endpoints or otherwise fail SSRF validation.
    This route-layer check rejects early (before any work is queued) as
    fail-fast defense-in-depth; the OpenAI-compatible provider re-validates the
    same URL via assert_base_url_safe before the client is built.
    """

    _AWS_METADATA = (
        "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
    )

    def test_create_rejects_metadata_endpoint(self, authenticated_client):
        """Cloud metadata endpoints are always blocked."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.create_subscription"
        ) as mock_create:
            response = authenticated_client.post(
                "/api/news/subscriptions",
                json={"query": "q", "custom_endpoint": self._AWS_METADATA},
                content_type="application/json",
            )
            assert response.status_code == 400
            mock_create.assert_not_called()

    def test_create_rejects_garbage_url(self, authenticated_client):
        """Malformed URLs are rejected before reaching news_api."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.create_subscription"
        ) as mock_create:
            response = authenticated_client.post(
                "/api/news/subscriptions",
                json={"query": "q", "custom_endpoint": "not-a-url"},
                content_type="application/json",
            )
            assert response.status_code == 400
            mock_create.assert_not_called()

    def test_create_accepts_localhost_endpoint(self, authenticated_client):
        """Local LLM endpoints on localhost must pass — local providers
        like Ollama / LM Studio / vLLM live there."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.create_subscription"
        ) as mock_create:
            mock_create.return_value = {"id": "sub-1"}
            response = authenticated_client.post(
                "/api/news/subscriptions",
                json={
                    "query": "q",
                    "custom_endpoint": "http://localhost:11434/v1",
                },
                content_type="application/json",
            )
            assert response.status_code == 201
            mock_create.assert_called_once()

    def test_update_rejects_metadata_endpoint(self, authenticated_client):
        """update_subscription has the same SSRF guard."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.update_subscription"
        ) as mock_update:
            response = authenticated_client.put(
                "/api/news/subscriptions/sub-1",
                json={"custom_endpoint": self._AWS_METADATA},
                content_type="application/json",
            )
            assert response.status_code == 400
            mock_update.assert_not_called()

    def test_create_accepts_schemeless_local_endpoint(
        self, authenticated_client
    ):
        """Scheme-less local endpoints (e.g. ``localhost:11434``) must pass.

        The provider normalizes the URL before use (adds ``http://``), so the
        route guard must normalize too — otherwise the exact local-LLM users
        the guard is meant to allow get a spurious 400. Regression test for
        the validate-the-raw-value bug.
        """
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.create_subscription"
        ) as mock_create:
            mock_create.return_value = {"id": "sub-1"}
            response = authenticated_client.post(
                "/api/news/subscriptions",
                json={"query": "q", "custom_endpoint": "localhost:11434"},
                content_type="application/json",
            )
            assert response.status_code == 201
            mock_create.assert_called_once()

    def test_update_accepts_public_endpoint(self, authenticated_client):
        """Public endpoints pass validation.

        validate_url is patched so the assertion never depends on real DNS
        resolution of the public host (avoids offline-runner flake)."""
        with (
            patch(
                "local_deep_research.web.routes.news_routes.news_api.update_subscription"
            ) as mock_update,
            patch(
                "local_deep_research.utilities.url_utils.validate_url",
                return_value=True,
            ),
        ):
            mock_update.return_value = {"id": "sub-1"}
            response = authenticated_client.put(
                "/api/news/subscriptions/sub-1",
                json={"custom_endpoint": "https://api.openai.com/v1"},
                content_type="application/json",
            )
            assert response.status_code == 200
            mock_update.assert_called_once()


class TestDeleteSubscription:
    """Tests for delete_subscription endpoint."""

    def test_delete_subscription_success(self, authenticated_client):
        """Delete subscription succeeds."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.delete_subscription"
        ) as mock_delete:
            mock_delete.return_value = {"deleted": True}

            response = authenticated_client.delete(
                "/api/news/subscriptions/sub-123"
            )

            assert response.status_code == 200


class TestGetSubscriptionHistory:
    """Tests for get_subscription_history endpoint."""

    # Use a real UUID for these tests — the route now validates the
    # subscription_id format and 400s anything that isn't a UUID. The
    # wildcard-rejection regression is covered in TestGetNewsFeed above.
    _SUBSCRIPTION_UUID = "11111111-2222-3333-4444-555555555555"

    def test_get_subscription_history_success(self, authenticated_client):
        """Get subscription history returns history."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.get_subscription_history"
        ) as mock_get:
            mock_get.return_value = {"history": []}

            response = authenticated_client.get(
                f"/api/news/subscriptions/{self._SUBSCRIPTION_UUID}/history"
            )

            assert response.status_code == 200

    def test_get_subscription_history_with_limit(self, authenticated_client):
        """Get subscription history with limit parameter."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.get_subscription_history"
        ) as mock_get:
            mock_get.return_value = {"history": []}

            response = authenticated_client.get(
                f"/api/news/subscriptions/{self._SUBSCRIPTION_UUID}/history?limit=10"
            )

            assert response.status_code == 200
            mock_get.assert_called_once_with(self._SUBSCRIPTION_UUID, 10)


class TestSubmitFeedback:
    """Tests for submit_feedback endpoint."""

    def test_submit_feedback_upvote(self, authenticated_client):
        """Submit upvote feedback."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.submit_feedback"
        ) as mock_submit:
            mock_submit.return_value = {"success": True}

            response = authenticated_client.post(
                "/api/news/feedback",
                json={"card_id": "card-123", "vote": "up"},
                content_type="application/json",
            )

            assert response.status_code == 200

    def test_submit_feedback_downvote(self, authenticated_client):
        """Submit downvote feedback."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.submit_feedback"
        ) as mock_submit:
            mock_submit.return_value = {"success": True}

            response = authenticated_client.post(
                "/api/news/feedback",
                json={"card_id": "card-123", "vote": "down"},
                content_type="application/json",
            )

            assert response.status_code == 200

    def test_submit_feedback_invalid_vote(self, authenticated_client):
        """Submit feedback with invalid vote."""
        response = authenticated_client.post(
            "/api/news/feedback",
            json={"card_id": "card-123", "vote": "invalid"},
            content_type="application/json",
        )

        assert response.status_code == 400

    def test_submit_feedback_missing_card_id(self, authenticated_client):
        """Submit feedback without card_id."""
        response = authenticated_client.post(
            "/api/news/feedback",
            json={"vote": "up"},
            content_type="application/json",
        )

        assert response.status_code == 400


class TestResearchNewsItem:
    """Tests for research_news_item endpoint."""

    def test_research_news_item_success(self, authenticated_client):
        """Research news item succeeds."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.research_news_item"
        ) as mock_research:
            mock_research.return_value = {"research_id": "res-123"}

            response = authenticated_client.post(
                "/api/news/research",
                json={"card_id": "card-123"},
                content_type="application/json",
            )

            assert response.status_code == 200

    def test_research_news_item_with_depth(self, authenticated_client):
        """Research news item with custom depth."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.research_news_item"
        ) as mock_research:
            mock_research.return_value = {"research_id": "res-123"}

            response = authenticated_client.post(
                "/api/news/research",
                json={"card_id": "card-123", "depth": "detailed"},
                content_type="application/json",
            )

            assert response.status_code == 200
            mock_research.assert_called_once_with("card-123", "detailed")

    def test_research_news_item_missing_card_id(self, authenticated_client):
        """Research news item without card_id."""
        response = authenticated_client.post(
            "/api/news/research",
            json={},
            content_type="application/json",
        )

        assert response.status_code == 400


class TestSavePreferences:
    """Tests for save_preferences endpoint."""

    def test_save_preferences_success(self, authenticated_client):
        """Save preferences succeeds."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.save_news_preferences"
        ) as mock_save:
            mock_save.return_value = {"saved": True}

            response = authenticated_client.post(
                "/api/news/preferences",
                json={"categories": ["tech", "science"]},
                content_type="application/json",
            )

            assert response.status_code == 200


class TestGetCategories:
    """Tests for get_categories endpoint."""

    def test_get_categories_success(self, authenticated_client):
        """Get categories returns category list."""
        with patch(
            "local_deep_research.web.routes.news_routes.news_api.get_news_categories"
        ) as mock_get:
            mock_get.return_value = {
                "categories": [
                    {"name": "Technology", "count": 10},
                    {"name": "Science", "count": 5},
                ]
            }

            response = authenticated_client.get("/api/news/categories")

            assert response.status_code == 200
            data = response.get_json()
            assert "categories" in data


class TestNewsAPIExceptionHandler:
    """Tests for NewsAPIException error handler."""

    def test_news_api_exception_handled(self, authenticated_client):
        """NewsAPIException is handled properly."""
        from local_deep_research.news.exceptions import NewsAPIException

        with patch(
            "local_deep_research.web.routes.news_routes.news_api.get_news_feed"
        ) as mock_get:
            mock_get.side_effect = NewsAPIException(
                message="Test error",
                error_code="TEST_ERROR",
                status_code=400,
            )

            response = authenticated_client.get("/api/news/feed")

            assert response.status_code == 400
            data = response.get_json()
            assert data["error_code"] == "TEST_ERROR"


class TestHandleApiErrorsDecorator:
    """Tests for handle_api_errors decorator (PR #1996).

    The decorator catches generic exceptions and returns 500,
    but re-raises NewsAPIException for the error handler.
    """

    def test_decorator_returns_500_on_generic_exception(self):
        """Generic exceptions return 500 JSON response."""
        from flask import Flask

        from local_deep_research.web.routes.news_routes import (
            handle_api_errors,
        )

        app = Flask(__name__)
        app.config["TESTING"] = True

        with app.app_context():

            @handle_api_errors
            def failing_view():
                raise RuntimeError("Unexpected error")

            response, status = failing_view()
            assert status == 500
            data = response.get_json()
            assert "error" in data

    def test_decorator_reraises_news_api_exception(self):
        """NewsAPIException is re-raised, not caught."""
        from local_deep_research.news.exceptions import NewsAPIException
        from local_deep_research.web.routes.news_routes import (
            handle_api_errors,
        )

        @handle_api_errors
        def news_error_view():
            raise NewsAPIException("Test news error")

        with pytest.raises(NewsAPIException):
            news_error_view()

    def test_decorator_passes_through_on_success(self):
        """Successful function result is passed through."""
        from flask import Flask, jsonify

        from local_deep_research.web.routes.news_routes import (
            handle_api_errors,
        )

        app = Flask(__name__)
        app.config["TESTING"] = True

        with app.app_context():

            @handle_api_errors
            def ok_view():
                return jsonify({"status": "ok"}), 200

            response, status = ok_view()
            assert status == 200

    def test_decorator_preserves_function_name(self):
        """Decorator preserves wrapped function name."""
        from local_deep_research.web.routes.news_routes import (
            handle_api_errors,
        )

        @handle_api_errors
        def my_news_endpoint():
            pass

        assert my_news_endpoint.__name__ == "my_news_endpoint"
