"""
Comprehensive coverage tests for news/flask_api.py
"""

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

MODULE = "local_deep_research.news.flask_api"
AUTH_MOD = "local_deep_research.web.auth.decorators"


@pytest.fixture()
def app():
    from local_deep_research.news.flask_api import news_api_bp

    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"
    app.config["TESTING"] = True
    app.register_blueprint(news_api_bp, url_prefix="/news/api")
    return app


@pytest.fixture()
def client(app):
    return app.test_client()


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


@pytest.fixture(autouse=True)
def _bypass_auth():
    mock_db = MagicMock()
    mock_db.is_user_connected.return_value = True
    with patch(f"{AUTH_MOD}.db_manager", mock_db):
        yield


class TestSubscribe:
    def test_success(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.create_subscription", return_value={"id": "s1"}
            ) as m,
        ):
            resp = client.post("/news/api/subscribe", json={"query": "AI news"})
            assert resp.status_code == 200
            assert resp.get_json()["id"] == "s1"
            assert m.call_args.kwargs["query"] == "AI news"

    def test_folder_name_and_active_forwarded(self, client):
        """folder_id, name and is_active from the request body must reach
        api.create_subscription (regression test for #4489, where the
        frontend dropped these fields)."""
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.create_subscription", return_value={"id": "s1"}
            ) as m,
        ):
            resp = client.post(
                "/news/api/subscribe",
                json={
                    "query": "AI news",
                    "name": "My subscription",
                    "folder_id": "7f3d2a10-0000-0000-0000-000000000000",
                    "is_active": False,
                },
            )
            assert resp.status_code == 200
            kwargs = m.call_args.kwargs
            assert kwargs["folder_id"] == "7f3d2a10-0000-0000-0000-000000000000"
            assert kwargs["name"] == "My subscription"
            assert kwargs["is_active"] is False

    def test_missing_query_returns_400(self, client):
        _auth(client)
        with patch(f"{MODULE}.get_user_id", return_value="testuser"):
            resp = client.post(
                "/news/api/subscribe", json={"subscription_type": "search"}
            )
            assert resp.status_code == 400
            assert "required" in resp.get_json()["error"]

    def test_invalid_json_returns_400(self, client):
        _auth(client)
        resp = client.post(
            "/news/api/subscribe",
            data="not json",
            content_type="application/json",
        )
        assert resp.status_code == 400

    def test_value_error_returns_400(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.create_subscription",
                side_effect=ValueError("bad"),
            ),
        ):
            resp = client.post("/news/api/subscribe", json={"query": "test"})
            assert resp.status_code == 400

    def test_generic_exception_returns_500(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.create_subscription",
                side_effect=RuntimeError("boom"),
            ),
        ):
            resp = client.post("/news/api/subscribe", json={"query": "test"})
            assert resp.status_code == 500


class TestVoteOnNews:
    def test_success(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.submit_feedback", return_value={"status": "ok"}
            ),
        ):
            resp = client.post(
                "/news/api/vote", json={"card_id": "c1", "vote": "up"}
            )
            assert resp.status_code == 200

    def test_missing_card_id_returns_400(self, client):
        _auth(client)
        with patch(f"{MODULE}.get_user_id", return_value="testuser"):
            resp = client.post("/news/api/vote", json={"vote": "up"})
            assert resp.status_code == 400

    def test_missing_vote_returns_400(self, client):
        _auth(client)
        with patch(f"{MODULE}.get_user_id", return_value="testuser"):
            resp = client.post("/news/api/vote", json={"card_id": "c1"})
            assert resp.status_code == 400

    def test_not_found_returns_404(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.submit_feedback",
                side_effect=ValueError("Card not found"),
            ),
        ):
            resp = client.post(
                "/news/api/vote", json={"card_id": "c1", "vote": "up"}
            )
            assert resp.status_code == 404

    def test_other_value_error_returns_400(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.submit_feedback",
                side_effect=ValueError("invalid direction"),
            ),
        ):
            resp = client.post(
                "/news/api/vote", json={"card_id": "c1", "vote": "bad"}
            )
            assert resp.status_code == 400

    def test_generic_exception_returns_500(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.submit_feedback",
                side_effect=RuntimeError("boom"),
            ),
        ):
            resp = client.post(
                "/news/api/vote", json={"card_id": "c1", "vote": "up"}
            )
            assert resp.status_code == 500


class TestGetBatchFeedback:
    def test_returns_votes(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.get_votes_for_cards",
                return_value={"votes": {"c1": "up"}},
            ),
        ):
            resp = client.post(
                "/news/api/feedback/batch", json={"card_ids": ["c1"]}
            )
            assert resp.status_code == 200
            assert resp.get_json()["votes"]["c1"] == "up"

    def test_empty_card_ids_returns_empty_votes(self, client):
        _auth(client)
        resp = client.post("/news/api/feedback/batch", json={"card_ids": []})
        assert resp.status_code == 200
        assert resp.get_json()["votes"] == {}

    def test_not_found_returns_404(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.get_votes_for_cards",
                side_effect=ValueError("not found"),
            ),
        ):
            resp = client.post(
                "/news/api/feedback/batch", json={"card_ids": ["c1"]}
            )
            assert resp.status_code == 404

    def test_generic_exception_returns_500(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.get_votes_for_cards",
                side_effect=RuntimeError("db"),
            ),
        ):
            resp = client.post(
                "/news/api/feedback/batch", json={"card_ids": ["c1"]}
            )
            assert resp.status_code == 500


class TestSubmitFeedback:
    def test_success(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.submit_feedback",
                return_value={"status": "recorded"},
            ),
        ):
            resp = client.post("/news/api/feedback/card1", json={"vote": "up"})
            assert resp.status_code == 200
            assert resp.get_json()["status"] == "recorded"

    def test_missing_vote_returns_400(self, client):
        _auth(client)
        with patch(f"{MODULE}.get_user_id", return_value="testuser"):
            resp = client.post("/news/api/feedback/card1", json={})
            assert resp.status_code == 400
            assert "vote" in resp.get_json()["error"].lower()

    def test_not_found_returns_404(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.submit_feedback",
                side_effect=ValueError("Card not found"),
            ),
        ):
            resp = client.post("/news/api/feedback/card1", json={"vote": "up"})
            assert resp.status_code == 404

    def test_must_be_returns_400(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.submit_feedback",
                side_effect=ValueError("vote must be up or down"),
            ),
        ):
            resp = client.post("/news/api/feedback/card1", json={"vote": "x"})
            assert resp.status_code == 400
            assert resp.get_json()["error"] == "Invalid input value"

    def test_other_value_error_returns_400(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.submit_feedback",
                side_effect=ValueError("something else"),
            ),
        ):
            resp = client.post("/news/api/feedback/card1", json={"vote": "up"})
            assert resp.status_code == 400

    def test_generic_exception_returns_500(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.submit_feedback",
                side_effect=RuntimeError("crash"),
            ),
        ):
            resp = client.post("/news/api/feedback/card1", json={"vote": "up"})
            assert resp.status_code == 500


class TestResearchNewsItem:
    def test_with_depth(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.research_news_item", return_value={"summary": "ok"}
        ) as m:
            resp = client.post(
                "/news/api/research/card42", json={"depth": "detailed"}
            )
            assert resp.status_code == 200
            m.assert_called_once_with("card42", "detailed")

    def test_default_depth(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.research_news_item", return_value={"summary": "ok"}
        ) as m:
            resp = client.post("/news/api/research/card42", json={})
            assert resp.status_code == 200
            m.assert_called_once_with("card42", "quick")

    def test_exception_returns_500(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.research_news_item", side_effect=RuntimeError("fail")
        ):
            resp = client.post(
                "/news/api/research/card42", json={"depth": "quick"}
            )
            assert resp.status_code == 500


class TestGetNewsFeed:
    def _ms(self):
        m = MagicMock()
        m.get_setting.return_value = 20
        return m

    def test_success(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(f"{MODULE}.get_settings_manager", return_value=self._ms()),
            patch(
                f"{MODULE}.api.get_news_feed",
                return_value={"news_items": [{"id": "1"}]},
            ),
        ):
            resp = client.get("/news/api/feed")
            assert resp.status_code == 200
            assert len(resp.get_json()["news_items"]) == 1

    def test_error_in_result_returns_500(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(f"{MODULE}.get_settings_manager", return_value=self._ms()),
            patch(
                f"{MODULE}.api.get_news_feed",
                return_value={"error": "db error", "news_items": []},
            ),
        ):
            resp = client.get("/news/api/feed")
            assert resp.status_code == 500

    def test_must_be_between_returns_400(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(f"{MODULE}.get_settings_manager", return_value=self._ms()),
            patch(
                f"{MODULE}.api.get_news_feed",
                return_value={
                    "error": "limit must be between 1 and 100",
                    "news_items": [],
                },
            ),
        ):
            resp = client.get("/news/api/feed")
            assert resp.status_code == 400

    def test_exception_returns_500(self, client):
        _auth(client)
        with patch(f"{MODULE}.get_user_id", side_effect=RuntimeError("boom")):
            resp = client.get("/news/api/feed")
            assert resp.status_code == 500


def _mock_db_session():
    ms = MagicMock()
    ctx = MagicMock()
    ctx.__enter__ = MagicMock(return_value=ms)
    ctx.__exit__ = MagicMock(return_value=False)
    return ctx, ms


class TestCreateFolder:
    def test_success(self, client):
        _auth(client)
        ctx, ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = None
        mf = MagicMock()
        mf.to_dict.return_value = {"id": "f1", "name": "Tech"}
        mm = MagicMock()
        mm.create_folder.return_value = mf
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager", return_value=mm),
        ):
            resp = client.post(
                "/news/api/subscription/folders", json={"name": "Tech"}
            )
            assert resp.status_code == 201

    def test_missing_name_returns_400(self, client):
        _auth(client)
        resp = client.post("/news/api/subscription/folders", json={})
        assert resp.status_code == 400

    def test_duplicate_returns_409(self, client):
        _auth(client)
        ctx, ms = _mock_db_session()
        ms.query.return_value.filter_by.return_value.first.return_value = (
            MagicMock()
        )
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager"),
        ):
            resp = client.post(
                "/news/api/subscription/folders", json={"name": "Dup"}
            )
            assert resp.status_code == 409


class TestUpdateFolder:
    def test_success(self, client):
        _auth(client)
        ctx, ms = _mock_db_session()
        mf = MagicMock()
        mf.to_dict.return_value = {"id": "f1", "name": "Updated"}
        mm = MagicMock()
        mm.update_folder.return_value = mf
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager", return_value=mm),
        ):
            resp = client.put(
                "/news/api/subscription/folders/f1", json={"name": "Updated"}
            )
            assert resp.status_code == 200

    def test_not_found_returns_404(self, client):
        _auth(client)
        ctx, ms = _mock_db_session()
        mm = MagicMock()
        mm.update_folder.return_value = None
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager", return_value=mm),
        ):
            resp = client.put(
                "/news/api/subscription/folders/f1", json={"name": "X"}
            )
            assert resp.status_code == 404


class TestDeleteFolder:
    def test_success(self, client):
        _auth(client)
        ctx, _ = _mock_db_session()
        mm = MagicMock()
        mm.delete_folder.return_value = True
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager", return_value=mm),
        ):
            resp = client.delete("/news/api/subscription/folders/f1")
            assert resp.status_code == 200

    def test_not_found_returns_404(self, client):
        _auth(client)
        ctx, _ = _mock_db_session()
        mm = MagicMock()
        mm.delete_folder.return_value = False
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager", return_value=mm),
        ):
            resp = client.delete("/news/api/subscription/folders/f1")
            assert resp.status_code == 404

    def test_with_move_to(self, client):
        _auth(client)
        ctx, _ = _mock_db_session()
        mm = MagicMock()
        mm.delete_folder.return_value = True
        with (
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager", return_value=mm),
        ):
            resp = client.delete("/news/api/subscription/folders/f1?move_to=f2")
            assert resp.status_code == 200
            mm.delete_folder.assert_called_once_with("f1", "f2")


class TestGetFolders:
    def test_success(self, client):
        _auth(client)
        ctx, _ = _mock_db_session()
        fo = MagicMock()
        fo.to_dict.return_value = {"id": "f1", "name": "Tech"}
        mm = MagicMock()
        mm.get_user_folders.return_value = [fo]
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager", return_value=mm),
        ):
            resp = client.get("/news/api/subscription/folders")
            assert resp.status_code == 200
            assert resp.get_json()[0]["name"] == "Tech"


class TestDeleteSubscription:
    def test_success(self, client):
        _auth(client)
        with patch(f"{MODULE}.api.delete_subscription", return_value=True):
            resp = client.delete("/news/api/subscriptions/s1")
            assert resp.status_code == 200
            assert resp.get_json()["status"] == "success"

    def test_not_found(self, client):
        _auth(client)
        with patch(f"{MODULE}.api.delete_subscription", return_value=False):
            resp = client.delete("/news/api/subscriptions/s1")
            assert resp.status_code == 404

    def test_exception_returns_500(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.delete_subscription",
            side_effect=RuntimeError("boom"),
        ):
            resp = client.delete("/news/api/subscriptions/s1")
            assert resp.status_code == 500


class TestGetSubscription:
    def test_success(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.get_subscription",
            return_value={"id": "s1", "query": "AI"},
        ):
            resp = client.get("/news/api/subscriptions/s1")
            assert resp.status_code == 200
            assert resp.get_json()["id"] == "s1"

    def test_null_id_returns_400(self, client):
        _auth(client)
        resp = client.get("/news/api/subscriptions/null")
        assert resp.status_code == 400

    def test_undefined_id_returns_400(self, client):
        _auth(client)
        resp = client.get("/news/api/subscriptions/undefined")
        assert resp.status_code == 400

    def test_not_found(self, client):
        _auth(client)
        with patch(f"{MODULE}.api.get_subscription", return_value=None):
            resp = client.get("/news/api/subscriptions/s1")
            assert resp.status_code == 404

    def test_exception_returns_500(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.get_subscription", side_effect=RuntimeError("db")
        ):
            resp = client.get("/news/api/subscriptions/s1")
            assert resp.status_code == 500


class TestUpdateSubscription:
    def test_success(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.update_subscription",
            return_value={"id": "s1", "name": "New"},
        ):
            resp = client.put(
                "/news/api/subscriptions/s1", json={"name": "New"}
            )
            assert resp.status_code == 200

    def test_not_found_error(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.update_subscription",
            return_value={"error": "Subscription not found"},
        ):
            resp = client.put("/news/api/subscriptions/s1", json={"name": "X"})
            assert resp.status_code == 404

    def test_other_error(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.update_subscription",
            return_value={"error": "invalid field"},
        ):
            resp = client.put("/news/api/subscriptions/s1", json={"name": "X"})
            assert resp.status_code == 400

    def test_exception_returns_500(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.update_subscription",
            side_effect=RuntimeError("crash"),
        ):
            resp = client.put("/news/api/subscriptions/s1", json={"name": "X"})
            assert resp.status_code == 500


class TestGetCurrentUserSubscriptions:
    def test_success(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.get_subscriptions",
                return_value={"subscriptions": []},
            ),
        ):
            resp = client.get("/news/api/subscriptions/current")
            assert resp.status_code == 200

    def test_error_in_result(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.get_subscriptions",
                return_value={"error": "db issue"},
            ),
        ):
            resp = client.get("/news/api/subscriptions/current")
            assert resp.status_code == 500


class TestSubscriptionHistory:
    def _ms(self):
        m = MagicMock()
        m.get_setting.return_value = 20
        return m

    def test_success(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_settings_manager", return_value=self._ms()),
            patch(
                f"{MODULE}.api.get_subscription_history",
                return_value={"history": []},
            ),
        ):
            resp = client.get("/news/api/subscriptions/s1/history")
            assert resp.status_code == 200

    def test_error_in_result(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_settings_manager", return_value=self._ms()),
            patch(
                f"{MODULE}.api.get_subscription_history",
                return_value={"error": "fail"},
            ),
        ):
            resp = client.get("/news/api/subscriptions/s1/history")
            assert resp.status_code == 500


class TestSavePreferences:
    def test_success(self, client):
        _auth(client)
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(
                f"{MODULE}.api.save_news_preferences",
                return_value={"status": "ok"},
            ),
        ):
            resp = client.post(
                "/news/api/preferences", json={"preferences": {"theme": "dark"}}
            )
            assert resp.status_code == 200


class TestGetCategories:
    def test_success(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.get_news_categories",
            return_value={"categories": ["tech"]},
        ):
            resp = client.get("/news/api/categories")
            assert resp.status_code == 200

    def test_exception(self, client):
        _auth(client)
        with patch(
            f"{MODULE}.api.get_news_categories",
            side_effect=RuntimeError("fail"),
        ):
            resp = client.get("/news/api/categories")
            assert resp.status_code == 500


class TestSubscriptionStats:
    def test_success(self, client):
        _auth(client)
        ctx, _ = _mock_db_session()
        mm = MagicMock()
        mm.get_subscription_stats.return_value = {"total": 5}
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager", return_value=mm),
        ):
            resp = client.get("/news/api/subscription/stats")
            assert resp.status_code == 200
            assert resp.get_json()["total"] == 5


class TestGetSubscriptionsOrganized:
    def test_success(self, client):
        """Returns the flattened {folder_name: [subscription, ...]} map.

        get_subscriptions_by_folder returns plain dicts in a
        {"folders": [...], "uncategorized": [...]} shape; the route must
        flatten them. (The previous version called .to_dict() on those plain
        dicts and 500'd; this also pins that regression.)
        """
        _auth(client)
        ctx, _ = _mock_db_session()
        mm = MagicMock()
        mm.get_subscriptions_by_folder.return_value = {
            "folders": [
                {
                    "folder": {"id": "f1", "name": "General"},
                    "subscriptions": [{"id": "s1", "query_or_topic": "q1"}],
                },
            ],
            "uncategorized": [{"id": "s2", "query_or_topic": "q2"}],
        }
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager", return_value=mm),
        ):
            resp = client.get("/news/api/subscription/subscriptions/organized")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["General"] == [{"id": "s1", "query_or_topic": "q1"}]
            assert data["uncategorized"] == [
                {"id": "s2", "query_or_topic": "q2"}
            ]

    def test_folder_named_uncategorized_does_not_drop_subscriptions(
        self, client
    ):
        """A user folder literally named "uncategorized" must not have its
        subscriptions clobbered by the ungrouped bucket — both are merged."""
        _auth(client)
        ctx, _ = _mock_db_session()
        mm = MagicMock()
        mm.get_subscriptions_by_folder.return_value = {
            "folders": [
                {
                    "folder": {"id": "f1", "name": "uncategorized"},
                    "subscriptions": [{"id": "folder_sub"}],
                },
            ],
            "uncategorized": [{"id": "ungrouped_sub"}],
        }
        with (
            patch(f"{MODULE}.get_user_id", return_value="testuser"),
            patch(f"{MODULE}.get_user_db_session", return_value=ctx),
            patch(f"{MODULE}.FolderManager", return_value=mm),
        ):
            resp = client.get("/news/api/subscription/subscriptions/organized")
            assert resp.status_code == 200
            ids = {s["id"] for s in resp.get_json()["uncategorized"]}
            # Neither the folder's sub nor the ungrouped sub is dropped.
            assert ids == {"folder_sub", "ungrouped_sub"}


SCHED_MOD = "local_deep_research.scheduler.background"


class TestSchedulerStatus:
    def test_success(self, client):
        _auth(client)
        ms = MagicMock()
        ms.is_running = True
        ms.config = {"check_interval": 300}
        ms.user_sessions = {"testuser": {"scheduled_jobs": {"j1"}}}
        job = MagicMock()
        job.id = "j1"
        job.name = "Test Job"
        job.args = ("testuser", 1)
        job.next_run_time = None
        ms.scheduler.get_jobs.return_value = [job]
        with (
            patch(f"{MODULE}.get_env_setting", return_value=False),
            patch(f"{SCHED_MOD}.get_background_job_scheduler", return_value=ms),
        ):
            resp = client.get("/news/api/scheduler/status")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["is_running"] is True
            assert data["active_users"] == 1


class TestSchedulerStats:
    def test_success(self, client):
        _auth(client)
        ms = MagicMock()
        ms.is_running = True
        ms.user_sessions = {}
        ms.scheduler.get_jobs.return_value = []
        with (
            patch(f"{MODULE}.get_env_setting", return_value=True),
            patch(f"{SCHED_MOD}.get_background_job_scheduler", return_value=ms),
        ):
            resp = client.get("/news/api/scheduler/stats")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["scheduler_running"] is True


class TestGetActiveUsers:
    def test_success(self, client):
        _auth(client)
        ms = MagicMock()
        ms.get_user_sessions_summary.return_value = [
            {"user_id": "testuser", "last_activity": "2026-01-01"},
        ]
        with (
            patch(f"{MODULE}.get_env_setting", return_value=False),
            patch(f"{SCHED_MOD}.get_background_job_scheduler", return_value=ms),
        ):
            resp = client.get("/news/api/scheduler/users")
            assert resp.status_code == 200
            data = resp.get_json()
            assert data["active_users"] == 1
            assert data["users"][0]["user_id"] == "testuser"


class TestErrorHandlers:
    def test_bad_request_handler(self, app):
        from local_deep_research.news.flask_api import bad_request

        with app.app_context():
            resp, status = bad_request(None)
            assert status == 400

    def test_not_found_handler(self, app):
        from local_deep_research.news.flask_api import not_found

        with app.app_context():
            resp, status = not_found(None)
            assert status == 404

    def test_internal_error_handler(self, app):
        from local_deep_research.news.flask_api import internal_error

        with app.app_context():
            resp, status = internal_error(None)
            assert status == 500
