# allow: no-sut-import — black-box HTTP test; drives real routes through the Flask test client
"""Tests for rate limiting and notification endpoints in settings_routes.py."""

from contextlib import contextmanager
from types import SimpleNamespace
from unittest.mock import patch, MagicMock
import time

import pytest

SETTINGS_PREFIX = "/settings"


def _fake_estimate(engine_type, success_rate, total_attempts=10):
    """Build a RateLimitEstimate-shaped row for the DB-backed route."""
    return SimpleNamespace(
        engine_type=engine_type,
        base_wait_seconds=2.0,
        min_wait_seconds=1.0,
        max_wait_seconds=5.0,
        last_updated=time.time(),
        total_attempts=total_attempts,
        success_rate=success_rate,
    )


@contextmanager
def _patch_estimates(estimates=None, query_error=None):
    """Patch settings_routes.get_user_db_session so the rate-limiting
    status route reads *estimates* (or raises *query_error*)."""
    session = MagicMock()
    if query_error is not None:
        session.query.side_effect = query_error
    else:
        session.query.return_value.order_by.return_value.all.return_value = (
            estimates or []
        )

    @contextmanager
    def _ctx(username, password=None):
        yield session

    with patch(
        "local_deep_research.web.routes.settings_routes.get_user_db_session",
        side_effect=_ctx,
    ):
        yield session


class TestApiGetRateLimitingStatus:
    """Tests for /api/rate-limiting/status endpoint."""

    def test_requires_authentication(self, client):
        response = client.get(f"{SETTINGS_PREFIX}/api/rate-limiting/status")
        assert response.status_code == 401, response.status_code

    def test_returns_status_and_engines(self, authenticated_client):
        # The route reads engine rows from persisted RateLimitEstimate
        # records (DB-backed), and the status block from the user's
        # settings — no get_tracker call anymore.
        estimates = [
            _fake_estimate("bing", 0.8, total_attempts=30),
            _fake_estimate("google", 0.95, total_attempts=50),
        ]
        with _patch_estimates(estimates):
            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/rate-limiting/status"
            )
        assert response.status_code == 200
        data = response.get_json()
        # status carries the rate-limiting settings block (exact key set)
        assert set(data["status"].keys()) == {
            "enabled",
            "profile",
            "exploration_rate",
            "learning_rate",
            "memory_window",
        }
        assert len(data["engines"]) == 2
        # ordered by engine_type -> bing first
        assert data["engines"][0]["engine_type"] == "bing"
        assert data["engines"][1]["engine_type"] == "google"
        assert data["engines"][1]["success_rate"] == 95.0

    def test_status_block_surfaces_settings_and_defaults(
        self, authenticated_client
    ):
        """Guard the rate_limiting.* status block this PR added/fixed: the
        new ``profile`` key, the ``enabled: True`` fallback default (the
        sole reason for this PR's final commit), and that configured values
        flow through rather than being hardcoded.

        ``enabled`` and ``profile`` are omitted from the patched settings so
        the route's own fallback defaults (True / "balanced") must apply;
        the rest are configured to non-defaults to prove flow-through.
        """

        configured = {
            "rate_limiting.exploration_rate": 0.2,
            "rate_limiting.learning_rate": 0.5,
            "rate_limiting.memory_window": 50,
        }

        def fake_get(key, default=None):
            return configured.get(key, default)

        with (
            _patch_estimates([]),
            patch(
                "local_deep_research.web.routes.settings_routes._get_setting_from_session",
                side_effect=fake_get,
            ),
        ):
            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/rate-limiting/status"
            )

        assert response.status_code == 200
        status = response.get_json()["status"]
        assert status == {
            "enabled": True,  # PR-fixed fallback default (was False)
            "profile": "balanced",  # fallback default
            "exploration_rate": 0.2,  # configured -> flows through
            "learning_rate": 0.5,
            "memory_window": 50,
        }

    def test_handles_zero_attempt_engine(self, authenticated_client):
        # A freshly-tracked engine has success_rate 0.0 (the column is
        # NOT NULL with default 0.0), which renders as 0.0 - there is no
        # None case to handle now that the route is DB-backed.
        estimates = [_fake_estimate("new_engine", 0.0, total_attempts=0)]
        with _patch_estimates(estimates):
            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/rate-limiting/status"
            )
        data = response.get_json()
        assert data["engines"][0]["success_rate"] == 0.0

    def test_error_returns_500(self, authenticated_client):
        with _patch_estimates(query_error=RuntimeError("db error")):
            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/rate-limiting/status"
            )
        assert response.status_code == 500
        data = response.get_json()
        assert "error" in data


class TestApiResetEngineRateLimiting:
    """Tests for /api/rate-limiting/engines/<engine_type>/reset endpoint."""

    def test_requires_authentication(self, client):
        response = client.post(
            f"{SETTINGS_PREFIX}/api/rate-limiting/engines/google/reset"
        )
        assert response.status_code == 401, response.status_code

    def test_resets_engine(self, authenticated_client):
        # Deletes the persisted RateLimitEstimate row for the engine (DB-backed,
        # no get_tracker) and commits.
        with _patch_estimates() as session:
            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/rate-limiting/engines/google/reset"
            )
        assert response.status_code == 200
        assert "google" in response.get_json()["message"]
        session.query.return_value.filter_by.assert_called_once_with(
            engine_type="google"
        )
        session.query.return_value.filter_by.return_value.delete.assert_called_once()
        session.commit.assert_called_once()

    def test_error_returns_500(self, authenticated_client):
        with _patch_estimates(query_error=RuntimeError("db fail")):
            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/rate-limiting/engines/google/reset"
            )
        assert response.status_code == 500


class TestApiCleanupRateLimiting:
    """Tests for /api/rate-limiting/cleanup endpoint."""

    def test_requires_authentication(self, client):
        response = client.post(f"{SETTINGS_PREFIX}/api/rate-limiting/cleanup")
        assert response.status_code == 401, response.status_code

    def test_cleanup_default_days(self, authenticated_client):
        # DB-backed: deletes old RateLimitEstimate rows and commits (no tracker).
        with _patch_estimates() as session:
            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/rate-limiting/cleanup"
            )
        assert response.status_code == 200
        assert "30 days" in response.get_json()["message"]
        session.query.return_value.filter.return_value.delete.assert_called_once()
        session.commit.assert_called_once()

    def test_cleanup_custom_days(self, authenticated_client):
        with _patch_estimates() as session:
            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/rate-limiting/cleanup",
                json={"days": 7},
            )
        assert response.status_code == 200
        assert "7 days" in response.get_json()["message"]
        session.query.return_value.filter.return_value.delete.assert_called_once()
        session.commit.assert_called_once()

    def test_error_returns_500(self, authenticated_client):
        with _patch_estimates(query_error=RuntimeError("fail")):
            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/rate-limiting/cleanup"
            )
        assert response.status_code == 500

    @pytest.mark.parametrize("days_value", [0, -1, 366, 1000])
    def test_rejects_out_of_range_days(self, days_value, authenticated_client):
        response = authenticated_client.post(
            f"{SETTINGS_PREFIX}/api/rate-limiting/cleanup",
            json={"days": days_value},
        )
        assert response.status_code == 400

    @pytest.mark.parametrize("days_value", ["not-a-number", None, [1, 2]])
    def test_rejects_non_integer_days(self, days_value, authenticated_client):
        response = authenticated_client.post(
            f"{SETTINGS_PREFIX}/api/rate-limiting/cleanup",
            json={"days": days_value},
        )
        assert response.status_code == 400


class TestCheckOllamaStatusSettings:
    """Tests for /api/ollama-status endpoint in settings_routes."""

    def test_requires_authentication(self, client):
        response = client.get(f"{SETTINGS_PREFIX}/api/ollama-status")
        assert response.status_code == 401, response.status_code

    @patch("local_deep_research.web.routes.settings_routes.safe_get")
    @patch(
        "local_deep_research.web.routes.settings_routes._get_setting_from_session"
    )
    def test_ollama_running(
        self, mock_get_setting, mock_safe_get, authenticated_client
    ):
        mock_get_setting.return_value = "http://localhost:11434"
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"version": "0.3.0"}
        mock_safe_get.return_value = mock_resp

        response = authenticated_client.get(
            f"{SETTINGS_PREFIX}/api/ollama-status"
        )
        assert response.status_code == 200
        data = response.get_json()
        assert data["running"] is True
        assert data["version"] == "0.3.0"

    @patch("local_deep_research.web.routes.settings_routes.safe_get")
    @patch(
        "local_deep_research.web.routes.settings_routes._get_setting_from_session"
    )
    def test_ollama_non_200(
        self, mock_get_setting, mock_safe_get, authenticated_client
    ):
        mock_get_setting.return_value = "http://localhost:11434"
        mock_resp = MagicMock()
        mock_resp.status_code = 500
        mock_safe_get.return_value = mock_resp

        response = authenticated_client.get(
            f"{SETTINGS_PREFIX}/api/ollama-status"
        )
        data = response.get_json()
        assert data["running"] is False

    @patch("local_deep_research.web.routes.settings_routes.safe_get")
    @patch(
        "local_deep_research.web.routes.settings_routes._get_setting_from_session"
    )
    def test_ollama_connection_error(
        self, mock_get_setting, mock_safe_get, authenticated_client
    ):
        import requests

        mock_get_setting.return_value = "http://localhost:11434"
        mock_safe_get.side_effect = requests.exceptions.ConnectionError(
            "refused"
        )

        response = authenticated_client.get(
            f"{SETTINGS_PREFIX}/api/ollama-status"
        )
        data = response.get_json()
        assert data["running"] is False


class TestApiTestNotificationUrl:
    """Tests for /api/notifications/test-url endpoint."""

    def test_requires_authentication(self, client):
        response = client.post(
            f"{SETTINGS_PREFIX}/api/notifications/test-url",
            json={"service_url": "http://example.com"},
        )
        assert response.status_code == 401, response.status_code

    @patch("local_deep_research.notifications.service.NotificationService")
    def test_successful_test(self, mock_ns_cls, authenticated_client):
        mock_ns = MagicMock()
        mock_ns.test_service.return_value = {
            "success": True,
            "message": "Notification sent",
            "error": "",
        }
        mock_ns_cls.return_value = mock_ns

        response = authenticated_client.post(
            f"{SETTINGS_PREFIX}/api/notifications/test-url",
            json={"service_url": "tgram://token/chat_id"},
        )
        assert response.status_code == 200
        data = response.get_json()
        assert data["success"] is True

    @patch("local_deep_research.notifications.service.NotificationService")
    def test_failed_test(self, mock_ns_cls, authenticated_client):
        mock_ns = MagicMock()
        mock_ns.test_service.return_value = {
            "success": False,
            "message": "",
            "error": "Invalid URL",
        }
        mock_ns_cls.return_value = mock_ns

        response = authenticated_client.post(
            f"{SETTINGS_PREFIX}/api/notifications/test-url",
            json={"service_url": "invalid://url"},
        )
        data = response.get_json()
        assert data["success"] is False

    def test_missing_service_url(self, authenticated_client):
        response = authenticated_client.post(
            f"{SETTINGS_PREFIX}/api/notifications/test-url",
            json={},
        )
        assert response.status_code == 400
        data = response.get_json()
        assert data["success"] is False

    def test_no_json_body(self, authenticated_client):
        response = authenticated_client.post(
            f"{SETTINGS_PREFIX}/api/notifications/test-url",
        )
        # No JSON body causes get_json() to return None, hitting the error handler
        assert response.status_code == 500, response.status_code

    @patch("local_deep_research.notifications.service.NotificationService")
    def test_does_not_leak_internal_details(
        self, mock_ns_cls, authenticated_client
    ):
        """Response should only contain expected safe fields."""
        mock_ns = MagicMock()
        mock_ns.test_service.return_value = {
            "success": True,
            "message": "OK",
            "error": "",
            "internal_debug": "SECRET_TOKEN_123",
        }
        mock_ns_cls.return_value = mock_ns

        response = authenticated_client.post(
            f"{SETTINGS_PREFIX}/api/notifications/test-url",
            json={"service_url": "tgram://token/chat_id"},
        )
        data = response.get_json()
        assert "internal_debug" not in data
        assert "SECRET_TOKEN_123" not in str(data)
