"""
Tests covering uncovered lines in news/flask_api.py.

Targets:
- scheduler_control_required decorator 403 path with session username logging
- _is_job_owned_by_user() fallback via scheduler.user_sessions
- run_subscription_now() subscription lookup, config extraction, response paths
- check_subscriptions_now() overdue query + threading
- trigger_cleanup() APScheduler job scheduling
- update_subscription_folder() dynamic field update + next_refresh recalculation
- get_search_history() / clear_search_history() unauthenticated return paths
- add_search_history() data validation
"""

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


@pytest.fixture
def app():
    """Create a Flask app with the news blueprint registered."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret-key"
    app.config["WTF_CSRF_ENABLED"] = False
    app.config["TESTING"] = True

    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()


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


def _session_ctx_returning(sub):
    """Build a mocked get_user_db_session() context manager whose
    ``query(...).filter(...).first()`` yields ``sub`` (a real NewsSubscription
    or None). run_subscription_now reads the ORM row directly, so tests supply
    a real model instance rather than the trimmed api.get_subscriptions dict.
    """
    session = MagicMock()
    session.query.return_value.filter.return_value.first.return_value = sub
    ctx = MagicMock()
    ctx.__enter__ = MagicMock(return_value=session)
    ctx.__exit__ = MagicMock(return_value=False)
    return ctx


class TestSchedulerControlRequiredLogging:
    """Cover the 403 branch that reads session username and remote_addr."""

    def test_403_body_includes_session_username_in_log(self, app):
        """When blocked, the decorator reads session['username'] for logging."""
        from local_deep_research.news.flask_api import (
            scheduler_control_required,
        )

        with app.test_request_context():
            # Put a username in the Flask session so the decorator can read it
            from flask import session as flask_session

            flask_session["username"] = "alice"

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

                @scheduler_control_required
                def dummy():
                    return jsonify({"ok": True}), 200

                response, status = dummy()
                assert status == 403
                data = response.get_json()
                assert "disabled" in data["error"].lower()


class TestIsJobOwnedByUser:
    """Cover _is_job_owned_by_user fallback through scheduler.user_sessions."""

    def test_primary_match_via_job_args(self):
        """Job is owned when job.args[0] matches the username."""
        from local_deep_research.news.flask_api import _is_job_owned_by_user

        job = MagicMock()
        job.args = ("alice", 42)
        scheduler = MagicMock(spec=[])  # no user_sessions attribute

        assert _is_job_owned_by_user(job, "alice", scheduler) is True

    def test_fallback_match_via_user_sessions(self):
        """Job is owned when its id appears in scheduler.user_sessions."""
        from local_deep_research.news.flask_api import _is_job_owned_by_user

        job = MagicMock()
        job.args = ("other_user",)  # primary check fails
        job.id = "job_123"

        scheduler = MagicMock()
        scheduler.user_sessions = {
            "alice": {"scheduled_jobs": {"job_123", "job_456"}},
        }

        assert _is_job_owned_by_user(job, "alice", scheduler) is True

    def test_no_match_returns_false(self):
        """Returns False when neither primary nor fallback matches."""
        from local_deep_research.news.flask_api import _is_job_owned_by_user

        job = MagicMock()
        job.args = ("other_user",)
        job.id = "job_999"

        scheduler = MagicMock()
        scheduler.user_sessions = {
            "alice": {"scheduled_jobs": {"job_123"}},
        }

        assert _is_job_owned_by_user(job, "alice", scheduler) is False


class TestRunSubscriptionNow:
    """Cover run_subscription_now route: subscription lookup, config, responses."""

    def _setup_auth(self, client):
        _auth_session(client)

    def test_subscription_not_found_returns_404(self, client):
        """Returns 404 when the subscription id is not in the user's DB."""
        self._setup_auth(client)
        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.database.session_context.get_user_db_session",
                return_value=_session_ctx_returning(None),
            ),
            patch(
                "local_deep_research.news.core.utils.get_local_date_string",
                return_value="2026-03-20",
            ),
            patch("local_deep_research.settings.manager.SettingsManager"),
        ):
            mock_db.is_user_connected.return_value = True
            resp = client.post("/news/api/subscriptions/nonexistent/run")
            assert resp.status_code == 404
            assert "not found" in resp.get_json()["error"].lower()

    def test_successful_run_returns_research_id(self, client, app):
        """Successful run returns status=success and a research_id."""
        self._setup_auth(client)

        from local_deep_research.database.models.news import NewsSubscription

        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.json.return_value = {
            "status": "success",
            "research_id": "res_42",
        }

        sub = NewsSubscription(
            id="sub_1",
            query_or_topic="AI news YYYY-MM-DD",
            subscription_type="topic",
            model_provider="OPENAI",
            model="gpt-4",
            search_strategy="news_aggregation",
            name="AI Digest",
            refresh_interval_minutes=60,
        )

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.database.session_context.get_user_db_session",
                return_value=_session_ctx_returning(sub),
            ),
            patch(
                "local_deep_research.news.flask_api.safe_post",
                return_value=mock_response,
            ),
            patch(
                "local_deep_research.news.core.utils.get_local_date_string",
                return_value="2026-03-20",
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager",
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.post("/news/api/subscriptions/sub_1/run")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["status"] == "success"
            assert data["research_id"] == "res_42"

    def test_unset_model_config_falls_back_to_user_settings(self, client):
        """A subscription without model_provider/model must NOT force
        "ollama"/"llama3" onto the research request. The values are sent
        unset (None) so start_research falls back to the user's configured
        llm.provider / llm.model. Regression test: hardcoding the defaults
        here overrode the LLM for any subscription created without an
        explicit model.
        """
        self._setup_auth(client)

        from local_deep_research.database.models.news import NewsSubscription

        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.json.return_value = {
            "status": "success",
            "research_id": "res_99",
        }

        # Subscription with no saved model_provider/model.
        sub = NewsSubscription(
            id="sub_1",
            query_or_topic="AI news",
            subscription_type="topic",
            name="AI Digest",
            refresh_interval_minutes=60,
        )

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.database.session_context.get_user_db_session",
                return_value=_session_ctx_returning(sub),
            ),
            patch(
                "local_deep_research.news.flask_api.safe_post",
                return_value=mock_response,
            ) as mock_post,
            patch(
                "local_deep_research.news.core.utils.get_local_date_string",
                return_value="2026-03-20",
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager",
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.post("/news/api/subscriptions/sub_1/run")
            assert resp.status_code == 200

            sent = mock_post.call_args.kwargs["json"]
            # Unset → falsy, so the backend resolves from user settings.
            assert not sent["model_provider"]
            assert not sent["model"]
            assert sent["model_provider"] != "ollama"
            assert sent["model"] != "llama3"

    def test_explicit_model_config_is_passed_through(self, client):
        """When the subscription carries an explicit provider/model, those
        exact values reach the research request unchanged."""
        self._setup_auth(client)

        from local_deep_research.database.models.news import NewsSubscription

        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.json.return_value = {
            "status": "success",
            "research_id": "res_100",
        }

        sub = NewsSubscription(
            id="sub_1",
            query_or_topic="AI news",
            subscription_type="topic",
            model_provider="openai",
            model="gpt-4o",
            name="AI Digest",
            refresh_interval_minutes=60,
        )

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.database.session_context.get_user_db_session",
                return_value=_session_ctx_returning(sub),
            ),
            patch(
                "local_deep_research.news.flask_api.safe_post",
                return_value=mock_response,
            ) as mock_post,
            patch(
                "local_deep_research.news.core.utils.get_local_date_string",
                return_value="2026-03-20",
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager",
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.post("/news/api/subscriptions/sub_1/run")
            assert resp.status_code == 200

            sent = mock_post.call_args.kwargs["json"]
            assert sent["model_provider"] == "openai"
            assert sent["model"] == "gpt-4o"

    def test_failed_response_returns_error(self, client):
        """Non-ok response from research API returns the status code."""
        self._setup_auth(client)

        from local_deep_research.database.models.news import NewsSubscription

        mock_response = MagicMock()
        mock_response.ok = False
        mock_response.status_code = 503

        sub = NewsSubscription(
            id="sub_1",
            query_or_topic="test",
            subscription_type="topic",
            refresh_interval_minutes=60,
        )

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.database.session_context.get_user_db_session",
                return_value=_session_ctx_returning(sub),
            ),
            patch(
                "local_deep_research.news.flask_api.safe_post",
                return_value=mock_response,
            ),
            patch(
                "local_deep_research.news.core.utils.get_local_date_string",
                return_value="2026-03-20",
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager",
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.post("/news/api/subscriptions/sub_1/run")
            assert resp.status_code == 503
            assert "error" in resp.get_json()

    def test_successful_run_advances_refresh_schedule(self, client):
        """A successful run advances the subscription's refresh schedule.

        Regression guard for the PR's headline behavior: without the
        advance_refresh_schedule() call, a manually-run subscription that was
        also overdue would be immediately re-run by the scheduler. Deleting the
        advance must fail this test.
        """
        from datetime import datetime, timedelta, timezone

        from local_deep_research.database.models.news import NewsSubscription

        self._setup_auth(client)

        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.json.return_value = {
            "status": "success",
            "research_id": "res_adv",
        }

        sub = NewsSubscription(
            id="sub_1",
            query_or_topic="AI news",
            subscription_type="topic",
            refresh_interval_minutes=60,
            last_refresh=None,
            next_refresh=None,
        )

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.database.session_context.get_user_db_session",
                return_value=_session_ctx_returning(sub),
            ),
            patch(
                "local_deep_research.news.flask_api.safe_post",
                return_value=mock_response,
            ),
            patch(
                "local_deep_research.news.core.utils.get_local_date_string",
                return_value="2026-03-20",
            ),
            patch("local_deep_research.settings.manager.SettingsManager"),
        ):
            mock_db.is_user_connected.return_value = True

            before = datetime.now(timezone.utc)
            resp = client.post("/news/api/subscriptions/sub_1/run")
            after = datetime.now(timezone.utc)
            assert resp.status_code == 200

        assert sub.last_refresh is not None, "schedule was not advanced"
        assert before <= sub.last_refresh <= after
        assert sub.next_refresh == sub.last_refresh + timedelta(minutes=60)

    def test_advance_skipped_when_failure_already_reset(self, client):
        """Compare-and-set guard: if a fast-failing run already reset
        next_refresh (worker thread) between the pre-spawn read and the
        post-POST advance, the advance must NOT clobber it and re-hide the
        failed subscription. Regression guard for the race in fix #1.
        """
        from datetime import datetime, timezone

        from local_deep_research.database.models.news import NewsSubscription

        self._setup_auth(client)

        t0 = datetime(2026, 6, 10, 12, 0, tzinfo=timezone.utc)
        # Worker's failure handler reset next_refresh to a different value
        # before we reopen to advance.
        t1 = datetime(2026, 6, 10, 12, 5, tzinfo=timezone.utc)
        sub_read = NewsSubscription(
            id="sub_1",
            query_or_topic="q",
            subscription_type="topic",
            refresh_interval_minutes=60,
            next_refresh=t0,
        )
        sub_after = NewsSubscription(
            id="sub_1",
            query_or_topic="q",
            subscription_type="topic",
            refresh_interval_minutes=60,
            next_refresh=t1,
            last_refresh=None,
        )

        session = MagicMock()
        session.query.return_value.filter.return_value.first.side_effect = [
            sub_read,
            sub_after,
        ]
        ctx = MagicMock()
        ctx.__enter__ = MagicMock(return_value=session)
        ctx.__exit__ = MagicMock(return_value=False)

        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.json.return_value = {
            "status": "success",
            "research_id": "res_x",
        }

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.database.session_context.get_user_db_session",
                return_value=ctx,
            ),
            patch(
                "local_deep_research.news.flask_api.safe_post",
                return_value=mock_response,
            ),
            patch(
                "local_deep_research.news.core.utils.get_local_date_string",
                return_value="2026-06-10",
            ),
            patch("local_deep_research.settings.manager.SettingsManager"),
        ):
            mock_db.is_user_connected.return_value = True
            resp = client.post("/news/api/subscriptions/sub_1/run")
            assert resp.status_code == 200

        # CAS skipped the advance: the worker's reset (t1) is preserved.
        assert sub_after.last_refresh is None, "advance clobbered the reset"
        assert sub_after.next_refresh == t1


class TestCheckOverdueSubscriptions:
    """Cover check_overdue_subscriptions: the per-overdue-sub run path
    builds request_data from the ORM object directly."""

    def _make_sub(self, **overrides):
        from types import SimpleNamespace

        defaults = dict(
            id="sub_overdue_1",
            name="Overdue Digest",
            query_or_topic="AI news",
            model_provider=None,
            model=None,
            search_strategy=None,
            search_engine=None,
            custom_endpoint=None,
            refresh_interval_minutes=60,
            last_refresh=None,
            next_refresh=None,
        )
        defaults.update(overrides)
        return SimpleNamespace(**defaults)

    def _run(self, client, sub):
        """POST /check-overdue with the DB query returning [sub]; return the
        request_data dict sent to the research backend."""
        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.json.return_value = {
            "status": "success",
            "research_id": "res_overdue",
        }

        db_mock = MagicMock()
        db_mock.query.return_value.filter.return_value.all.return_value = [sub]

        mock_ctx_mgr = MagicMock()
        mock_ctx_mgr.__enter__ = MagicMock(return_value=db_mock)
        mock_ctx_mgr.__exit__ = MagicMock(return_value=False)

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.database.session_context.get_user_db_session",
                return_value=mock_ctx_mgr,
            ),
            patch(
                "local_deep_research.news.flask_api.safe_post",
                return_value=mock_response,
            ) as mock_post,
            patch(
                "local_deep_research.news.core.utils.get_local_date_string",
                return_value="2026-03-20",
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager",
            ),
        ):
            mock_db.is_user_connected.return_value = True
            resp = client.post("/news/api/check-overdue")
            assert resp.status_code == 200, resp.get_json()
            return mock_post.call_args.kwargs["json"]

    def test_unset_model_config_falls_back_to_user_settings(self, client):
        """Overdue sub without provider/model must not force ollama/llama3."""
        _auth_session(client)
        sent = self._run(client, self._make_sub())
        assert not sent["model_provider"]
        assert not sent["model"]
        assert sent["model_provider"] != "ollama"
        assert sent["model"] != "llama3"

    def test_explicit_model_config_is_passed_through(self, client):
        """An overdue sub's explicit provider/model reach the request."""
        _auth_session(client)
        sent = self._run(
            client,
            self._make_sub(model_provider="anthropic", model="claude-3"),
        )
        assert sent["model_provider"] == "anthropic"
        assert sent["model"] == "claude-3"

    def test_successful_overdue_run_advances_schedule(self, client):
        """The overdue sweep advances each run subscription's schedule.

        Regression guard: without advance_refresh_schedule() the same overdue
        subscription would be picked up and re-run on every sweep.
        """
        from datetime import timedelta

        _auth_session(client)
        sub = self._make_sub(refresh_interval_minutes=60)
        self._run(client, sub)

        assert sub.last_refresh is not None, "schedule was not advanced"
        assert sub.next_refresh == sub.last_refresh + timedelta(minutes=60)

    def test_overdue_run_skips_advance_when_reset_during_run(self, client):
        """Compare-and-set in the sweep: if a fast-failing run already reset
        next_refresh (worker thread, observed via db.refresh), the advance is
        skipped so the reset is not clobbered. Mirrors the run-now CAS guard.
        """
        from datetime import datetime, timezone

        _auth_session(client)

        t0 = datetime(2026, 6, 10, 12, 0, tzinfo=timezone.utc)
        t1 = datetime(2026, 6, 10, 12, 5, tzinfo=timezone.utc)  # worker reset
        sub = self._make_sub(next_refresh=t0, last_refresh=None)

        mock_response = MagicMock()
        mock_response.ok = True
        mock_response.json.return_value = {
            "status": "success",
            "research_id": "res_overdue",
        }

        db_mock = MagicMock()
        db_mock.query.return_value.filter.return_value.all.return_value = [sub]

        # Simulate the worker's failure reset becoming visible on db.refresh().
        def _refresh(obj):
            obj.next_refresh = t1

        db_mock.refresh.side_effect = _refresh

        mock_ctx_mgr = MagicMock()
        mock_ctx_mgr.__enter__ = MagicMock(return_value=db_mock)
        mock_ctx_mgr.__exit__ = MagicMock(return_value=False)

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.database.session_context.get_user_db_session",
                return_value=mock_ctx_mgr,
            ),
            patch(
                "local_deep_research.news.flask_api.safe_post",
                return_value=mock_response,
            ),
            patch(
                "local_deep_research.news.core.utils.get_local_date_string",
                return_value="2026-03-20",
            ),
            patch("local_deep_research.settings.manager.SettingsManager"),
        ):
            mock_db.is_user_connected.return_value = True
            resp = client.post("/news/api/check-overdue")
            assert resp.status_code == 200, resp.get_json()

        # CAS skipped the advance: the worker's reset (t1) is preserved.
        assert sub.last_refresh is None, "advance clobbered the reset"
        assert sub.next_refresh == t1


class TestUpdateSubscriptionFolderStatus:
    """Cover update_subscription_folder: the is_active->status translation
    (so the PUT route keeps status authoritative) and the response
    serialization (NewsSubscription has no to_dict)."""

    def _ctx_for(self, sub):
        """get_user_db_session() context whose query(...).filter_by(...).first()
        yields ``sub`` (this route looks the row up via filter_by)."""
        session = MagicMock()
        session.query.return_value.filter_by.return_value.first.return_value = (
            sub
        )
        ctx = MagicMock()
        ctx.__enter__ = MagicMock(return_value=session)
        ctx.__exit__ = MagicMock(return_value=False)
        return ctx

    def _put(self, client, sub, body):
        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.news.flask_api.get_user_db_session",
                return_value=self._ctx_for(sub),
            ),
        ):
            mock_db.is_user_connected.return_value = True
            return client.put(
                "/news/api/subscription/subscriptions/sub_1", json=body
            )

    def test_is_active_false_translates_to_paused_status(self, client):
        """A {"is_active": false} body must pause via status, not just flip the
        legacy column -- otherwise the status-keyed scheduler keeps running it.
        """
        from local_deep_research.database.models.news import NewsSubscription

        _auth_session(client)
        sub = NewsSubscription(
            id="sub_1",
            query_or_topic="q",
            subscription_type="topic",
            status="active",
            refresh_interval_minutes=60,
        )

        resp = self._put(client, sub, {"is_active": False})

        assert resp.status_code == 200, resp.get_json()
        assert sub.status == "paused"
        body = resp.get_json()
        assert body["status"] == "paused"
        assert body["is_active"] is False

    def test_is_active_true_translates_to_active_status(self, client):
        from local_deep_research.database.models.news import NewsSubscription

        _auth_session(client)
        sub = NewsSubscription(
            id="sub_1",
            query_or_topic="q",
            subscription_type="topic",
            status="paused",
            refresh_interval_minutes=60,
        )

        resp = self._put(client, sub, {"is_active": True})

        assert resp.status_code == 200, resp.get_json()
        assert sub.status == "active"
        assert resp.get_json()["is_active"] is True

    def test_plain_field_update_returns_200_not_500(self, client):
        """A non-status field update must serialize a response without raising
        (NewsSubscription has no to_dict; the route builds the dict explicitly).
        """
        from local_deep_research.database.models.news import NewsSubscription

        _auth_session(client)
        sub = NewsSubscription(
            id="sub_1",
            query_or_topic="q",
            subscription_type="topic",
            status="active",
            refresh_interval_minutes=60,
        )

        resp = self._put(client, sub, {"folder_id": "folder_9"})

        assert resp.status_code == 200, resp.get_json()
        assert sub.folder_id == "folder_9"
        assert resp.get_json()["id"] == "sub_1"


class TestCheckSubscriptionsNow:
    """Cover check_subscriptions_now: DB query for overdue subs + threading."""

    def test_scheduler_not_initialized_returns_503(self, client, app):
        """Returns 503 when news_scheduler is missing from current_app."""
        _auth_session(client)

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=True,
            ),
        ):
            mock_db.is_user_connected.return_value = True
            # Ensure current_app has no background_job_scheduler
            if hasattr(app, "background_job_scheduler"):
                delattr(app, "background_job_scheduler")

            resp = client.post("/news/api/scheduler/check-now")
            assert resp.status_code == 503

    def test_scheduler_not_running_returns_503(self, client, app):
        """Returns 503 when scheduler exists but is_running is False."""
        _auth_session(client)
        app.background_job_scheduler = MagicMock()
        app.background_job_scheduler.is_running = False

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=True,
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.post("/news/api/scheduler/check-now")
            assert resp.status_code == 503

        # Clean up
        delattr(app, "background_job_scheduler")


class TestTriggerCleanup:
    """Cover trigger_cleanup: scheduler.add_job scheduling."""

    def test_cleanup_not_running_returns_400(self, client):
        """Returns 400 when scheduler is not running."""
        _auth_session(client)
        mock_scheduler = MagicMock()
        mock_scheduler.is_running = False

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=True,
            ),
            patch(
                "local_deep_research.news.flask_api.get_background_job_scheduler",
                return_value=mock_scheduler,
                create=True,
            ),
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_scheduler,
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.post("/news/api/scheduler/cleanup-now")
            assert resp.status_code == 400
            assert "not running" in resp.get_json()["error"].lower()

    def test_cleanup_triggered_returns_success(self, client):
        """Returns triggered status when scheduler is running."""
        _auth_session(client)
        mock_scheduler = MagicMock()
        mock_scheduler.is_running = True
        mock_scheduler.scheduler = MagicMock()

        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.news.flask_api.get_env_setting",
                return_value=True,
            ),
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_scheduler,
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.post("/news/api/scheduler/cleanup-now")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["status"] == "triggered"
            # Verify add_job was called
            mock_scheduler.scheduler.add_job.assert_called_once()


class TestSearchHistoryUnauthenticated:
    """Cover unauthenticated return paths in search history endpoints."""

    def test_get_search_history_unauthenticated_returns_empty(self, client):
        """GET /search-history returns empty list when current_user is None."""
        _auth_session(client)
        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.news.flask_api.current_user",
                return_value=None,
                create=True,
            ),
            patch(
                "local_deep_research.web.auth.decorators.current_user",
                return_value=None,
                create=True,
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.get("/news/api/search-history")
            assert resp.status_code == 200
            assert resp.get_json()["search_history"] == []

    def test_clear_search_history_unauthenticated_returns_success(self, client):
        """DELETE /search-history returns success when current_user is None."""
        _auth_session(client)
        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.news.flask_api.current_user",
                return_value=None,
                create=True,
            ),
            patch(
                "local_deep_research.web.auth.decorators.current_user",
                return_value=None,
                create=True,
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.delete("/news/api/search-history")
            assert resp.status_code == 200
            assert resp.get_json()["status"] == "success"

    def test_add_search_history_missing_query_returns_400(self, client):
        """POST /search-history returns 400 when query field is missing."""
        _auth_session(client)
        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.web.auth.decorators.current_user",
                return_value="testuser",
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.post(
                "/news/api/search-history",
                json={"type": "filter"},
                content_type="application/json",
            )
            assert resp.status_code == 400
            assert "query" in resp.get_json()["error"].lower()

    def test_add_search_history_unauthenticated_returns_401(self, client):
        """POST /search-history returns 401 when current_user is None."""
        _auth_session(client)
        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.news.flask_api.current_user",
                return_value=None,
                create=True,
            ),
            patch(
                "local_deep_research.web.auth.decorators.current_user",
                return_value=None,
                create=True,
            ),
        ):
            mock_db.is_user_connected.return_value = True

            resp = client.post(
                "/news/api/search-history",
                json={"query": "test"},
                content_type="application/json",
            )
            assert resp.status_code == 401
