"""
Comprehensive tests for news/flask_api.py

Tests cover:
- safe_error_message function
- get_user_id function
- News feed endpoint
- Subscription endpoints
- Folder management endpoints
- Error handling
"""

import pytest
from unittest.mock import patch, MagicMock
from flask import Flask, jsonify


@pytest.fixture
def app():
    """Create a Flask app for testing."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret-key"
    app.config["WTF_CSRF_ENABLED"] = False
    app.config["TESTING"] = True

    # Import and register the blueprint
    from local_deep_research.news.flask_api import news_api_bp

    app.register_blueprint(news_api_bp, url_prefix="/news/api")

    return app


@pytest.fixture
def client(app):
    """Create a test client."""
    return app.test_client()


class TestSafeErrorMessage:
    """Tests for safe_error_message function."""

    def test_value_error(self):
        """Test handling of ValueError."""
        from local_deep_research.news.flask_api import safe_error_message

        error = ValueError("sensitive internal message")
        result = safe_error_message(error, "test context")

        assert result == "Invalid input provided"
        assert "sensitive" not in result

    def test_key_error(self):
        """Test handling of KeyError."""
        from local_deep_research.news.flask_api import safe_error_message

        error = KeyError("missing_key")
        result = safe_error_message(error, "test context")

        assert result == "Required data missing"

    def test_type_error(self):
        """Test handling of TypeError."""
        from local_deep_research.news.flask_api import safe_error_message

        error = TypeError("type mismatch")
        result = safe_error_message(error, "test context")

        assert result == "Invalid data format"

    def test_generic_error(self):
        """Test handling of generic error."""
        from local_deep_research.news.flask_api import safe_error_message

        error = RuntimeError("internal error")
        result = safe_error_message(error, "doing something")

        assert "An error occurred" in result
        assert "doing something" in result

    def test_generic_error_no_context(self):
        """Test handling of generic error without context."""
        from local_deep_research.news.flask_api import safe_error_message

        error = RuntimeError("internal error")
        result = safe_error_message(error)

        assert result == "An error occurred"


class TestGetUserId:
    """Tests for get_user_id function."""

    def test_get_user_id_authenticated(self, app):
        """Test getting user ID when authenticated."""
        from local_deep_research.news.flask_api import get_user_id

        with app.app_context():
            # current_user is imported inside get_user_id, so patch at source
            with patch(
                "local_deep_research.web.auth.decorators.current_user",
                return_value="testuser",
            ):
                result = get_user_id()
                assert result == "testuser"

    def test_get_user_id_not_authenticated(self, app):
        """Test getting user ID when not authenticated."""
        from local_deep_research.news.flask_api import get_user_id

        with app.app_context():
            with patch(
                "local_deep_research.web.auth.decorators.current_user",
                return_value=None,
            ):
                result = get_user_id()
                assert result is None


class TestNewsBlueprintImport:
    """Tests for news blueprint import."""

    def test_blueprint_exists(self):
        """Test that news API blueprint exists."""
        from local_deep_research.news.flask_api import news_api_bp

        assert news_api_bp is not None
        assert news_api_bp.name == "news_api"
        assert news_api_bp.url_prefix == "/api"


class TestNewsFeedEndpoint:
    """Tests for news feed endpoint."""

    def test_feed_endpoint_exists(self, client):
        """Test that feed endpoint exists."""
        try:
            response = client.get("/news/api/feed")
            # Route exists - any response is valid (may require auth)
            assert response.status_code == 401, response.status_code
        except Exception:
            # If app context fails, that's okay - we're testing route existence
            pass


class TestSubscriptionEndpoints:
    """Tests for subscription endpoints."""

    def test_subscribe_no_data(self, client):
        """Test subscribe endpoint without data."""
        try:
            response = client.post(
                "/news/api/subscribe",
                content_type="application/json",
            )
            # Route exists - any response is valid
            assert response.status_code == 401, response.status_code
        except Exception:
            pass

    def test_subscribe_invalid_json(self, client):
        """Test subscribe endpoint with invalid JSON."""
        try:
            response = client.post(
                "/news/api/subscribe",
                data="not json",
                content_type="application/json",
            )
            # Route exists - any response is valid
            assert response.status_code == 401, response.status_code
        except Exception:
            pass


class TestFolderEndpoints:
    """Tests for folder management endpoints."""

    def test_folders_endpoint_requires_auth(self, client):
        """Test that folders endpoint requires authentication."""
        response = client.get("/news/api/subscription/folders")

        # Should require auth
        assert response.status_code == 401, response.status_code

    def test_create_folder_requires_auth(self, client):
        """Test that create folder endpoint requires authentication."""
        response = client.post(
            "/news/api/subscription/folders",
            json={"name": "Test Folder"},
            content_type="application/json",
        )

        # Should require auth
        assert response.status_code == 401, response.status_code


class TestSchedulerEndpoints:
    """Tests for scheduler endpoints."""

    def test_scheduler_status_requires_auth(self, client):
        """Test that scheduler status endpoint exists."""
        try:
            response = client.get("/news/api/scheduler/status")
            # Route may or may not exist - any response is valid
            assert response.status_code == 401, response.status_code
        except Exception:
            pass


class TestRecommenderEndpoints:
    """Tests for recommender endpoints."""

    def test_recommender_status_requires_auth(self, client):
        """Test that recommender status endpoint requires authentication."""
        response = client.get("/news/api/recommender/status")

        # Should require auth
        assert response.status_code == 404, response.status_code


class TestSubscriptionRunEndpoint:
    """Tests for subscription run endpoint."""

    def test_run_subscription_requires_auth(self, client):
        """Test that run subscription endpoint requires authentication."""
        response = client.post("/news/api/subscription/sub123/run")

        # Should require auth
        assert response.status_code == 404, response.status_code


class TestGetSubscription:
    """Tests for get subscription endpoint."""

    def test_get_subscription_requires_auth(self, client):
        """Test that get subscription endpoint requires authentication."""
        response = client.get("/news/api/subscription/sub123")

        # Should require auth
        assert response.status_code == 404, response.status_code


class TestUpdateSubscription:
    """Tests for update subscription endpoint."""

    def test_update_subscription_requires_auth(self, client):
        """Test that update subscription endpoint requires authentication."""
        response = client.put(
            "/news/api/subscription/sub123",
            json={"name": "Updated"},
            content_type="application/json",
        )

        # Should require auth
        assert response.status_code == 404, response.status_code


class TestDeleteSubscription:
    """Tests for delete subscription endpoint."""

    def test_delete_subscription_requires_auth(self, client):
        """Test that delete subscription endpoint requires authentication."""
        response = client.delete("/news/api/subscription/sub123")

        # Should require auth
        assert response.status_code == 404, response.status_code


class TestNewsCardInteractions:
    """Tests for news card interaction endpoints."""

    def test_dismiss_card_requires_auth(self, client):
        """Test that dismiss card endpoint requires authentication."""
        response = client.post("/news/api/card/card123/dismiss")

        # Should require auth
        assert response.status_code == 404, response.status_code

    def test_bookmark_card_requires_auth(self, client):
        """Test that bookmark card endpoint requires authentication."""
        response = client.post("/news/api/card/card123/bookmark")

        # Should require auth
        assert response.status_code == 404, response.status_code

    def test_rate_card_requires_auth(self, client):
        """Test that rate card endpoint requires authentication."""
        response = client.post(
            "/news/api/card/card123/rate",
            json={"rating": 5},
            content_type="application/json",
        )

        # Should require auth
        assert response.status_code == 404, response.status_code


class TestSubscriptionsList:
    """Tests for subscriptions list endpoint."""

    def test_subscriptions_list_requires_auth(self, client):
        """Test that subscriptions list endpoint requires authentication."""
        response = client.get("/news/api/subscriptions")

        # Should require auth
        assert response.status_code == 404, response.status_code


class TestNotificationEndpoints:
    """Tests for notification endpoints."""

    def test_test_notification_requires_auth(self, client):
        """Test that test notification endpoint requires authentication."""
        response = client.post(
            "/news/api/notifications/test",
            json={"service_url": "mailto://test@example.com"},
            content_type="application/json",
        )

        # Should require auth
        assert response.status_code == 404, response.status_code


class TestRefreshEndpoint:
    """Tests for refresh endpoint."""

    def test_refresh_feed_requires_auth(self, client):
        """Test that refresh feed endpoint requires authentication."""
        response = client.post("/news/api/refresh")

        # Should require auth
        assert response.status_code == 404, response.status_code


class TestSchedulerControlRequired:
    """Tests for scheduler_control_required decorator (PR #2035).

    The decorator gates global scheduler control endpoints behind
    the news.scheduler.allow_api_control setting, returning 403
    when disabled.
    """

    def test_decorator_allows_when_setting_enabled(self, app):
        """Decorated function executes when setting is True."""
        from local_deep_research.news.flask_api import (
            scheduler_control_required,
        )

        with app.app_context():
            with patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=True,
            ):

                @scheduler_control_required
                def dummy_view():
                    return jsonify({"status": "ok"}), 200

                response, status_code = dummy_view()
                assert status_code == 200

    def test_decorator_blocks_when_setting_disabled(self, app):
        """Decorated function returns 403 when setting is False."""
        from local_deep_research.news.flask_api import (
            scheduler_control_required,
        )

        with app.test_request_context():
            with patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ):

                @scheduler_control_required
                def dummy_view():
                    return jsonify({"status": "ok"}), 200

                response, status_code = dummy_view()
                assert status_code == 403

    def test_decorator_returns_error_message_when_disabled(self, app):
        """403 response includes informative error message."""
        from local_deep_research.news.flask_api import (
            scheduler_control_required,
        )

        with app.test_request_context():
            with patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ):

                @scheduler_control_required
                def dummy_view():
                    return jsonify({"status": "ok"}), 200

                response, status_code = dummy_view()
                data = response.get_json()
                assert "error" in data
                assert "disabled" in data["error"].lower()

    def test_decorator_checks_correct_setting_key(self, app):
        """Decorator checks news.scheduler.allow_api_control setting."""
        from local_deep_research.news.flask_api import (
            scheduler_control_required,
        )

        with app.app_context():
            with patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=True,
            ) as mock_setting:

                @scheduler_control_required
                def dummy_view():
                    return jsonify({"status": "ok"}), 200

                dummy_view()
                mock_setting.assert_called_once_with(
                    "news.scheduler.allow_api_control", False
                )

    def test_decorator_preserves_function_name(self):
        """Decorator preserves the wrapped function's name."""
        from local_deep_research.news.flask_api import (
            scheduler_control_required,
        )

        @scheduler_control_required
        def my_endpoint():
            pass

        assert my_endpoint.__name__ == "my_endpoint"

    def test_decorator_passes_args_to_wrapped_function(self, app):
        """Decorator forwards positional and keyword arguments."""
        from local_deep_research.news.flask_api import (
            scheduler_control_required,
        )

        with app.app_context():
            with patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=True,
            ):

                @scheduler_control_required
                def dummy_view(a, b, key=None):
                    return jsonify({"a": a, "b": b, "key": key}), 200

                response, status_code = dummy_view(1, 2, key="val")
                assert status_code == 200
                data = response.get_json()
                assert data["a"] == 1
                assert data["b"] == 2
                assert data["key"] == "val"

    def test_decorator_does_not_call_wrapped_when_disabled(self, app):
        """Wrapped function is never invoked when setting is disabled."""
        from local_deep_research.news.flask_api import (
            scheduler_control_required,
        )

        call_tracker = {"called": False}

        with app.test_request_context():
            with patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ):

                @scheduler_control_required
                def dummy_view():
                    call_tracker["called"] = True
                    return jsonify({"status": "ok"}), 200

                dummy_view()
                assert call_tracker["called"] is False


class TestSchedulerEndpointGating:
    """Integration tests verifying scheduler endpoints are properly gated.

    Tests that mutating scheduler endpoints (start, stop, check-now,
    cleanup-now) return 403 when the allow_api_control setting is
    disabled, while read-only endpoints (status, users, stats) remain
    accessible.
    """

    GATED_ENDPOINTS = [
        "/news/api/scheduler/start",
        "/news/api/scheduler/stop",
        "/news/api/scheduler/check-now",
        "/news/api/scheduler/cleanup-now",
    ]

    NON_GATED_ENDPOINTS = [
        "/news/api/scheduler/status",
        "/news/api/scheduler/users",
        "/news/api/scheduler/stats",
    ]

    def _auth_session(self, client):
        """Inject a valid session so login_required passes."""
        with client.session_transaction() as sess:
            sess["username"] = "testuser"

    def test_gated_endpoints_return_403_when_disabled(self, client, app):
        """Mutating scheduler endpoints return 403 when control disabled."""
        self._auth_session(client)
        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
        ):
            mock_db.connections = {"testuser": True}

            for endpoint in self.GATED_ENDPOINTS:
                response = client.post(endpoint)
                assert response.status_code == 403, (
                    f"{endpoint} should return 403 when control disabled, "
                    f"got {response.status_code}"
                )

    def test_read_only_endpoints_not_gated(self, client, app):
        """Read-only scheduler endpoints are not blocked by the setting."""
        self._auth_session(client)
        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
        ):
            mock_db.connections = {"testuser": True}

            for endpoint in self.NON_GATED_ENDPOINTS:
                response = client.get(endpoint)
                # Should NOT be 403 — these are read-only and not gated
                assert response.status_code != 403, (
                    f"{endpoint} should not be gated by allow_api_control, "
                    f"but got 403"
                )


class TestSchedulerStatsScoping:
    """Tests for scheduler stats endpoint data scoping (follow-up to PR #2035).

    Verifies that GET /scheduler/stats and /scheduler/users only expose
    the current user's data unless allow_api_control is enabled.
    """

    def _auth_session(self, client, username="testuser"):
        with client.session_transaction() as sess:
            sess["username"] = username

    def _mock_scheduler(self):
        """Create a mock scheduler with multi-user data."""
        scheduler = MagicMock()
        scheduler.is_running = True
        scheduler.user_sessions = {
            "alice": {
                "last_activity": MagicMock(
                    isoformat=lambda: "2026-01-01T00:00:00"
                ),
                "scheduled_jobs": {"job1"},
            },
            "bob": {
                "last_activity": MagicMock(
                    isoformat=lambda: "2026-01-02T00:00:00"
                ),
                "scheduled_jobs": {"job2", "job3"},
            },
        }
        scheduler._credential_store.retrieve.return_value = "secret"

        # Mock APScheduler jobs with username as first arg
        alice_job = MagicMock()
        alice_job.id = "alice_check_42"
        alice_job.name = "Alice check"
        alice_job.args = ("alice", 42)
        alice_job.next_run_time = None
        alice_job.trigger = "interval"

        bob_job = MagicMock()
        bob_job.id = "bob_document_processing"
        bob_job.name = "Bob doc"
        bob_job.args = ("bob", 1)
        bob_job.next_run_time = None
        bob_job.trigger = "interval"

        overdue_alice = MagicMock()
        overdue_alice.id = "overdue_alice_99_ts"
        overdue_alice.name = "Overdue Alice"
        overdue_alice.args = ("alice", 99)
        overdue_alice.next_run_time = None
        overdue_alice.trigger = "date"

        scheduler.scheduler = MagicMock()
        scheduler.scheduler.get_jobs.return_value = [
            alice_job,
            bob_job,
            overdue_alice,
        ]

        return scheduler

    def test_stats_no_side_effect(self, client, app):
        """GET /scheduler/stats must NOT call _schedule_user_subscriptions."""
        self._auth_session(client, "alice")
        mock_sched = self._mock_scheduler()

        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_sched,
            ),
        ):
            mock_db.connections = {"alice": True}
            response = client.get("/news/api/scheduler/stats")
            assert response.status_code == 200
            mock_sched._schedule_user_subscriptions.assert_not_called()

    def test_stats_scoped_to_current_user(self, client, app):
        """Stats returns only current user's session when control disabled."""
        self._auth_session(client, "alice")
        mock_sched = self._mock_scheduler()

        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_sched,
            ),
        ):
            mock_db.connections = {"alice": True}
            response = client.get("/news/api/scheduler/stats")
            data = response.get_json()

            # Only alice's session, not bob's
            assert "alice" in data["user_sessions"]
            assert "bob" not in data["user_sessions"]

    def test_stats_scoped_jobs_to_current_user(self, client, app):
        """Stats returns only current user's APScheduler jobs when control disabled."""
        self._auth_session(client, "alice")
        mock_sched = self._mock_scheduler()

        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_sched,
            ),
        ):
            mock_db.connections = {"alice": True}
            response = client.get("/news/api/scheduler/stats")
            data = response.get_json()

            job_ids = [j["id"] for j in data["apscheduler_jobs"]]
            assert "alice_check_42" in job_ids
            assert "overdue_alice_99_ts" in job_ids
            assert "bob_document_processing" not in job_ids

    def test_stats_shows_all_when_control_enabled(self, client, app):
        """Stats returns all users and jobs when allow_api_control is True."""
        self._auth_session(client, "alice")
        mock_sched = self._mock_scheduler()

        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=True,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_sched,
            ),
        ):
            mock_db.connections = {"alice": True}
            response = client.get("/news/api/scheduler/stats")
            data = response.get_json()

            assert "alice" in data["user_sessions"]
            assert "bob" in data["user_sessions"]

            job_ids = [j["id"] for j in data["apscheduler_jobs"]]
            assert "bob_document_processing" in job_ids

    def test_users_scoped_to_current_user(self, client, app):
        """GET /scheduler/users returns only current user when control disabled."""
        self._auth_session(client, "alice")
        mock_sched = self._mock_scheduler()
        mock_sched.get_user_sessions_summary.return_value = [
            {"user_id": "alice", "last_activity": "2026-01-01T00:00:00"},
            {"user_id": "bob", "last_activity": "2026-01-02T00:00:00"},
        ]

        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_sched,
            ),
        ):
            mock_db.connections = {"alice": True}
            response = client.get("/news/api/scheduler/users")
            data = response.get_json()

            assert data["active_users"] == 1
            user_ids = [u["user_id"] for u in data["users"]]
            assert "alice" in user_ids
            assert "bob" not in user_ids

    def test_users_shows_all_when_control_enabled(self, client, app):
        """GET /scheduler/users returns all users when control enabled."""
        self._auth_session(client, "alice")
        mock_sched = self._mock_scheduler()
        mock_sched.get_user_sessions_summary.return_value = [
            {"user_id": "alice", "last_activity": "2026-01-01T00:00:00"},
            {"user_id": "bob", "last_activity": "2026-01-02T00:00:00"},
        ]

        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=True,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_sched,
            ),
        ):
            mock_db.connections = {"alice": True}
            response = client.get("/news/api/scheduler/users")
            data = response.get_json()

            assert data["active_users"] == 2
            user_ids = [u["user_id"] for u in data["users"]]
            assert "alice" in user_ids
            assert "bob" in user_ids

    def test_error_message_no_env_var_name(self, app):
        """403 error message must not expose internal env var names."""
        from local_deep_research.news.flask_api import (
            scheduler_control_required,
        )

        with app.test_request_context():
            with app.app_context():
                with patch(
                    "local_deep_research.news.flask_api.get_env_setting",
                    return_value=False,
                ):
                    with patch(
                        "local_deep_research.news.flask_api.request",
                    ) as mock_req:
                        mock_req.remote_addr = "127.0.0.1"

                        @scheduler_control_required
                        def dummy_view():
                            return jsonify({"status": "ok"}), 200

                        response, status_code = dummy_view()
                        data = response.get_json()
                        assert "LDR_NEWS_SCHEDULER" not in data["error"]
                        assert "administrator" in data["error"].lower()


class TestIsJobOwnedByUser:
    """Tests for _is_job_owned_by_user helper."""

    def test_match_by_args(self):
        """Job with matching username in args is owned."""
        from local_deep_research.news.flask_api import _is_job_owned_by_user

        job = MagicMock()
        job.args = ("alice", 1)
        job.id = "alice_sub_1"
        scheduler = MagicMock(spec=[])  # no user_sessions attr
        assert _is_job_owned_by_user(job, "alice", scheduler) is True

    def test_no_match_different_user(self):
        """Job with different username in args is not owned."""
        from local_deep_research.news.flask_api import _is_job_owned_by_user

        job = MagicMock()
        job.args = ("bob", 1)
        job.id = "bob_sub_1"
        scheduler = MagicMock(spec=[])
        assert _is_job_owned_by_user(job, "alice", scheduler) is False

    def test_prefix_collision_prevented(self):
        """User 'alice' cannot see jobs for 'alice_admin'."""
        from local_deep_research.news.flask_api import _is_job_owned_by_user

        job = MagicMock()
        job.args = ("alice_admin", 42)
        job.id = "alice_admin_42"
        scheduler = MagicMock(spec=[])
        assert _is_job_owned_by_user(job, "alice", scheduler) is False

    def test_match_by_scheduled_jobs_set(self):
        """Job in user's scheduled_jobs set is matched even without args."""
        from local_deep_research.news.flask_api import _is_job_owned_by_user

        job = MagicMock()
        job.args = ()
        job.id = "some_job_id"
        scheduler = MagicMock()
        scheduler.user_sessions = {
            "alice": {"scheduled_jobs": {"some_job_id"}},
        }
        assert _is_job_owned_by_user(job, "alice", scheduler) is True

    def test_overdue_job_matched_by_args(self):
        """Overdue job with username in args is matched."""
        from local_deep_research.news.flask_api import _is_job_owned_by_user

        job = MagicMock()
        job.args = ("alice", 1)
        job.id = "overdue_alice_1"
        scheduler = MagicMock(spec=[])
        assert _is_job_owned_by_user(job, "alice", scheduler) is True

    def test_system_job_no_match(self):
        """System job with empty args is not owned by any user."""
        from local_deep_research.news.flask_api import _is_job_owned_by_user

        job = MagicMock()
        job.args = ()
        job.id = "cleanup_inactive_users"
        scheduler = MagicMock()
        scheduler.user_sessions = {
            "alice": {"scheduled_jobs": set()},
        }
        assert _is_job_owned_by_user(job, "alice", scheduler) is False


class TestSchedulerStatusScoping:
    """Integration tests for /scheduler/status endpoint scoping."""

    def _auth_session(self, client, username):
        """Inject a valid session so login_required passes."""
        with client.session_transaction() as sess:
            sess["username"] = username

    def _mock_scheduler(self):
        """Create a multi-user mock scheduler with jobs."""
        scheduler = MagicMock()
        scheduler.is_running = True
        scheduler.config = {"check_interval": 300}

        # Two users with sessions
        scheduler.user_sessions = {
            "alice": {"scheduled_jobs": {"alice_sub_1", "alice_sub_2"}},
            "bob": {"scheduled_jobs": {"bob_sub_1"}},
        }

        # APScheduler jobs with explicit .args
        alice_job1 = MagicMock()
        alice_job1.id = "alice_sub_1"
        alice_job1.name = "Alice Job 1"
        alice_job1.args = ("alice", 1)
        alice_job1.next_run_time = None

        alice_job2 = MagicMock()
        alice_job2.id = "alice_sub_2"
        alice_job2.name = "Alice Job 2"
        alice_job2.args = ("alice", 2)
        alice_job2.next_run_time = None

        bob_job = MagicMock()
        bob_job.id = "bob_sub_1"
        bob_job.name = "Bob Job 1"
        bob_job.args = ("bob", 1)
        bob_job.next_run_time = None

        system_job = MagicMock()
        system_job.id = "cleanup_inactive_users"
        system_job.name = "Cleanup"
        system_job.args = ()
        system_job.next_run_time = None

        scheduler.scheduler.get_jobs.return_value = [
            alice_job1,
            alice_job2,
            bob_job,
            system_job,
        ]

        return scheduler

    def test_status_scoped_to_current_user(self, client, app):
        """Alice sees only her own data when allow_api_control is False."""
        self._auth_session(client, "alice")
        mock_sched = self._mock_scheduler()

        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_sched,
            ),
        ):
            mock_db.connections = {"alice": True}

            response = client.get("/news/api/scheduler/status")
            assert response.status_code == 200
            data = response.get_json()

            # Alice sees only her own data
            assert data["active_users"] == 1
            assert data["total_scheduled_jobs"] == 2
            assert data["scheduled_jobs"] == 2
            # Only alice's jobs in apscheduler_jobs
            job_ids = [j["id"] for j in data.get("apscheduler_jobs", [])]
            assert "alice_sub_1" in job_ids
            assert "alice_sub_2" in job_ids
            assert "bob_sub_1" not in job_ids
            assert "cleanup_inactive_users" not in job_ids

    def test_status_shows_all_when_admin(self, client, app):
        """All data visible when allow_api_control is True."""
        self._auth_session(client, "alice")
        mock_sched = self._mock_scheduler()

        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=True,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_sched,
            ),
        ):
            mock_db.connections = {"alice": True}

            response = client.get("/news/api/scheduler/status")
            assert response.status_code == 200
            data = response.get_json()

            # Admin sees all users
            assert data["active_users"] == 2
            assert data["total_scheduled_jobs"] == 3
            # All jobs visible
            job_ids = [j["id"] for j in data.get("apscheduler_jobs", [])]
            assert "bob_sub_1" in job_ids
            assert "cleanup_inactive_users" in job_ids

    def test_status_unknown_user_sees_zeros(self, client, app):
        """User not in sessions sees zero counts and empty jobs."""
        self._auth_session(client, "charlie")
        mock_sched = self._mock_scheduler()

        with (
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=False,
            ),
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_sched,
            ),
        ):
            mock_db.connections = {"charlie": True}

            response = client.get("/news/api/scheduler/status")
            assert response.status_code == 200
            data = response.get_json()

            assert data["active_users"] == 0
            assert data["total_scheduled_jobs"] == 0
            assert data["scheduled_jobs"] == 0
            assert data.get("apscheduler_jobs", []) == []


class TestErrorHandling:
    """Tests for error handling in endpoints."""

    def test_endpoints_handle_exceptions(self, client):
        """Test that endpoints exist and handle requests."""
        # Test that routes are registered
        endpoints = [
            ("/news/api/feed", "GET"),
            ("/news/api/subscriptions", "GET"),
            ("/news/api/subscription/folders", "GET"),
        ]

        for endpoint, method in endpoints:
            try:
                if method == "GET":
                    response = client.get(endpoint)
                else:
                    response = client.post(endpoint)

                # Any response is acceptable
                assert response.status_code == 401, response.status_code
            except Exception:
                # If dependencies fail, that's okay - routes may exist
                pass
