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


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 list."""
        self._setup_auth(client)
        with (
            patch(
                "local_deep_research.web.auth.decorators.db_manager",
            ) as mock_db,
            patch(
                "local_deep_research.news.flask_api.api.get_subscriptions",
                return_value={"subscriptions": []},
            ),
        ):
            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)

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

        mock_ctx_mgr = MagicMock()
        mock_ctx_mgr.__enter__ = MagicMock(return_value=MagicMock())
        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.news.flask_api.api.get_subscriptions",
                return_value={
                    "subscriptions": [
                        {
                            "id": "sub_1",
                            "query": "AI news YYYY-MM-DD",
                            "model_provider": "OPENAI",
                            "model": "gpt-4",
                            "search_strategy": "news_aggregation",
                            "name": "AI Digest",
                            "custom_endpoint": None,
                        }
                    ]
                },
            ),
            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/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_failed_response_returns_error(self, client):
        """Non-ok response from research API returns the status code."""
        self._setup_auth(client)

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

        mock_ctx_mgr = MagicMock()
        mock_ctx_mgr.__enter__ = MagicMock(return_value=MagicMock())
        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.news.flask_api.api.get_subscriptions",
                return_value={
                    "subscriptions": [
                        {
                            "id": "sub_1",
                            "query": "test",
                            "name": None,
                        }
                    ]
                },
            ),
            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/subscriptions/sub_1/run")
            assert resp.status_code == 503
            assert "error" in resp.get_json()


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 news_scheduler
            if hasattr(app, "news_scheduler"):
                delattr(app, "news_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.news_scheduler = MagicMock()
        app.news_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, "news_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_news_scheduler",
                return_value=mock_scheduler,
                create=True,
            ),
            patch(
                "local_deep_research.news.subscription_manager.scheduler.get_news_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.news.subscription_manager.scheduler.get_news_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
