"""
Coverage tests for api_routes.py targeting uncovered statements and branches.

Covers:
- get_current_config: exception path (lines 64-66)
- api_research_status: exception path (lines 117-119)
- api_terminate_research: exception path (lines 151-153)
- api_get_resources: exception path (lines 168-170)
- api_add_resource: SSRF rejection (lines 207-208)
- api_add_resource: success path with add_resource call (lines 227-245)
- api_add_resource: exception path (lines 243-247)
- api_delete_resource: exception path (lines 274-276)
- check_ollama_status: old API format (lines 342-343)
- check_ollama_status: invalid JSON response (lines 354-358)
- check_ollama_status: non-200 status code (lines 364-368)
- check_ollama_status: timeout error (lines 386-395)
- check_ollama_status: general exception (lines 397-406)
- check_ollama_model: non-200 status (lines 460-463)
- check_ollama_model: old API format (lines 492-493)
- check_ollama_model: no models found (lines 525-526)
- check_ollama_model: JSON parse error (lines 542-554)
- check_ollama_model: connection error (lines 556-567)
- check_ollama_model: timeout error (lines 568-578)
- check_ollama_model: general exception (lines 580-596)
"""

import requests
from unittest.mock import MagicMock, patch

import pytest
from flask import Flask

from local_deep_research.web.auth import auth_bp
from local_deep_research.web.routes.api_routes import api_bp

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

MODULE = "local_deep_research.web.routes.api_routes"
AUTH_DB_MANAGER = "local_deep_research.web.auth.decorators.db_manager"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _mock_auth():
    """Return a MagicMock that satisfies login_required db_manager check."""
    return MagicMock(is_user_connected=MagicMock(return_value=True))


def _make_db_ctx(mock_session):
    """Build a mock context-manager for get_user_db_session."""
    ctx = MagicMock()
    ctx.__enter__ = MagicMock(return_value=mock_session)
    ctx.__exit__ = MagicMock(return_value=None)
    return ctx


def _make_db_ctx_raising(exc):
    """Build a context-manager that raises on __enter__."""
    ctx = MagicMock()
    ctx.__enter__ = MagicMock(side_effect=exc)
    ctx.__exit__ = MagicMock(return_value=None)
    return ctx


def _build_filter_chain(result):
    """Build chained SQLAlchemy mock query for filter_by().first()."""
    q = MagicMock()
    q.filter_by.return_value.first.return_value = result
    return q


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def app():
    """Minimal Flask app with auth and api blueprints."""
    application = Flask(__name__)
    application.config["SECRET_KEY"] = "test-secret"
    application.config["TESTING"] = True
    application.register_blueprint(auth_bp)
    application.register_blueprint(api_bp)
    return application


def _authed_get(app, path, **kwargs):
    """Issue an authenticated GET request and return the response."""
    with app.test_client() as c:
        with c.session_transaction() as sess:
            sess["username"] = "testuser"
        return c.get(path, **kwargs)


def _authed_post(app, path, **kwargs):
    """Issue an authenticated POST request and return the response."""
    with app.test_client() as c:
        with c.session_transaction() as sess:
            sess["username"] = "testuser"
        return c.post(path, **kwargs)


def _authed_delete(app, path, **kwargs):
    """Issue an authenticated DELETE request and return the response."""
    with app.test_client() as c:
        with c.session_transaction() as sess:
            sess["username"] = "testuser"
        return c.delete(path, **kwargs)


# ---------------------------------------------------------------------------
# get_current_config: exception path (lines 64-66)
# ---------------------------------------------------------------------------


class TestGetCurrentConfigException:
    """Exception path in get_current_config."""

    def test_get_current_config_exception_returns_500(self, app):
        """When get_user_db_session raises, return 500 with error."""
        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                f"{MODULE}.get_user_db_session",
                return_value=_make_db_ctx_raising(RuntimeError("db down")),
            ):
                resp = _authed_get(app, "/settings/current-config")
        assert resp.status_code == 500
        data = resp.get_json()
        assert data["success"] is False


# ---------------------------------------------------------------------------
# api_research_status: exception path (lines 117-119)
# ---------------------------------------------------------------------------


class TestApiResearchStatusException:
    """Exception path in api_research_status."""

    def test_research_status_exception_returns_500(self, app):
        """When db session raises, return 500 error."""
        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                f"{MODULE}.get_user_db_session",
                return_value=_make_db_ctx_raising(RuntimeError("db error")),
            ):
                resp = _authed_get(app, "/status/some-id")
        assert resp.status_code == 500
        data = resp.get_json()
        assert data["status"] == "error"


# ---------------------------------------------------------------------------
# api_terminate_research: exception path (lines 151-153)
# ---------------------------------------------------------------------------


class TestApiTerminateResearchException:
    """Exception path in api_terminate_research."""

    def test_terminate_exception_returns_500(self, app):
        """When cancel_research raises, return 500."""
        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                f"{MODULE}.cancel_research",
                side_effect=RuntimeError("cancel boom"),
            ):
                resp = _authed_post(app, "/terminate/some-id")
        assert resp.status_code == 500
        data = resp.get_json()
        assert data["status"] == "error"


# ---------------------------------------------------------------------------
# api_get_resources: exception path (lines 168-170)
# ---------------------------------------------------------------------------


class TestApiGetResourcesException:
    """Exception path in api_get_resources."""

    def test_get_resources_exception_returns_500(self, app):
        """When get_resources_for_research raises, return 500."""
        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                f"{MODULE}.get_resources_for_research",
                side_effect=RuntimeError("res err"),
            ):
                resp = _authed_get(app, "/resources/some-id")
        assert resp.status_code == 500
        data = resp.get_json()
        assert data["status"] == "error"


# ---------------------------------------------------------------------------
# api_add_resource: SSRF rejection (lines 207-208)
# ---------------------------------------------------------------------------


class TestApiAddResourceSsrf:
    """SSRF URL rejection in api_add_resource."""

    def test_ssrf_invalid_url_rejected(self, app):
        """When validate_url returns False, return 400."""
        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                "local_deep_research.security.ssrf_validator.validate_url",
                return_value=False,
            ):
                resp = _authed_post(
                    app,
                    "/resources/some-id",
                    json={
                        "title": "Bad",
                        "url": "http://169.254.169.254/latest",
                    },
                    content_type="application/json",
                )
        assert resp.status_code == 400
        data = resp.get_json()
        assert data["message"] == "Invalid URL"


# ---------------------------------------------------------------------------
# api_add_resource: success path (lines 227-245)
# ---------------------------------------------------------------------------


class TestApiAddResourceSuccess:
    """Success and exception paths in api_add_resource."""

    def test_add_resource_success(self, app):
        """When research exists and URL valid, add resource and return success."""
        mock_session = MagicMock()
        mock_session.query.return_value = _build_filter_chain(MagicMock())

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                "local_deep_research.security.ssrf_validator.validate_url",
                return_value=True,
            ):
                with patch(
                    f"{MODULE}.get_user_db_session",
                    return_value=_make_db_ctx(mock_session),
                ):
                    with patch(
                        f"{MODULE}.add_resource", return_value=42
                    ) as mock_add:
                        resp = _authed_post(
                            app,
                            "/resources/res-123",
                            json={
                                "title": "My Resource",
                                "url": "https://example.com/page",
                                "content_preview": "preview text",
                                "source_type": "pdf",
                                "metadata": {"key": "val"},
                            },
                            content_type="application/json",
                        )

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["status"] == "success"
        assert data["resource_id"] == 42
        mock_add.assert_called_once_with(
            research_id="res-123",
            title="My Resource",
            url="https://example.com/page",
            content_preview="preview text",
            source_type="pdf",
            metadata={"key": "val"},
        )

    def test_add_resource_exception_returns_500(self, app):
        """When add_resource raises, return 500."""
        mock_session = MagicMock()
        mock_session.query.return_value = _build_filter_chain(MagicMock())

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                "local_deep_research.security.ssrf_validator.validate_url",
                return_value=True,
            ):
                with patch(
                    f"{MODULE}.get_user_db_session",
                    return_value=_make_db_ctx(mock_session),
                ):
                    with patch(
                        f"{MODULE}.add_resource",
                        side_effect=RuntimeError("insert fail"),
                    ):
                        resp = _authed_post(
                            app,
                            "/resources/res-123",
                            json={
                                "title": "My Resource",
                                "url": "https://example.com/page",
                            },
                            content_type="application/json",
                        )

        assert resp.status_code == 500
        data = resp.get_json()
        assert data["status"] == "error"


# ---------------------------------------------------------------------------
# api_delete_resource: exception path (lines 274-276)
# ---------------------------------------------------------------------------


class TestApiDeleteResourceException:
    """Exception path in api_delete_resource."""

    def test_delete_resource_exception_returns_500(self, app):
        """When delete_resource raises, return 500."""
        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                f"{MODULE}.delete_resource",
                side_effect=RuntimeError("delete boom"),
            ):
                resp = _authed_delete(app, "/resources/res-id/delete/1")
        assert resp.status_code == 500
        data = resp.get_json()
        assert data["status"] == "error"


# ---------------------------------------------------------------------------
# check_ollama_status: edge cases (lines 342-406)
# ---------------------------------------------------------------------------


class TestCheckOllamaStatusEdgeCases:
    """Edge cases for check_ollama_status endpoint."""

    def test_old_api_format(self, app):
        """When response has no 'models' key, use old format (array)."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "ollama_base_url": "http://localhost:11434",
        }
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = [{"name": "llama3"}, {"name": "mistral"}]

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(f"{MODULE}.safe_get", return_value=mock_resp):
                resp = _authed_get(app, "/check/ollama_status")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["running"] is True
        assert data["model_count"] == 2

    def test_invalid_json_response(self, app):
        """When response.json() raises ValueError, report running with warning."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "ollama_base_url": "http://localhost:11434",
        }
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.side_effect = ValueError("bad json")

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(f"{MODULE}.safe_get", return_value=mock_resp):
                resp = _authed_get(app, "/check/ollama_status")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["running"] is True
        assert "invalid" in data["message"].lower()

    def test_non_200_status(self, app):
        """When Ollama returns non-200, report not running."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "ollama_base_url": "http://localhost:11434",
        }
        mock_resp = MagicMock()
        mock_resp.status_code = 503

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(f"{MODULE}.safe_get", return_value=mock_resp):
                resp = _authed_get(app, "/check/ollama_status")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["running"] is False
        assert data["status_code"] == 503

    def test_timeout_error(self, app):
        """When safe_get raises Timeout, report not running with timeout type."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "ollama_base_url": "http://localhost:11434",
        }

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                f"{MODULE}.safe_get",
                side_effect=requests.exceptions.Timeout("timed out"),
            ):
                resp = _authed_get(app, "/check/ollama_status")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["running"] is False
        assert data["error_type"] == "timeout"

    def test_general_exception(self, app):
        """When an unexpected exception occurs, report not running."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "ollama_base_url": "http://localhost:11434",
        }

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                f"{MODULE}.normalize_url",
                side_effect=RuntimeError("unexpected"),
            ):
                resp = _authed_get(app, "/check/ollama_status")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["running"] is False
        assert data["error_type"] == "exception"


# ---------------------------------------------------------------------------
# check_ollama_model: edge cases (lines 460-596)
# ---------------------------------------------------------------------------


class TestCheckOllamaModelEdgeCases:
    """Edge cases for check_ollama_model endpoint."""

    def test_non_200_status(self, app):
        """When Ollama API returns non-200, report not available."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "model": "llama3",
            "ollama_base_url": "http://localhost:11434",
        }
        mock_resp = MagicMock()
        mock_resp.status_code = 503

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(f"{MODULE}.safe_get", return_value=mock_resp):
                resp = _authed_get(app, "/check/ollama_model")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["available"] is False
        assert data["status_code"] == 503

    def test_old_api_format(self, app):
        """When response has no 'models' key, use old format (array)."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "model": "llama3",
            "ollama_base_url": "http://localhost:11434",
        }
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = [{"name": "llama3"}]

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(f"{MODULE}.safe_get", return_value=mock_resp):
                resp = _authed_get(app, "/check/ollama_model")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["available"] is True

    def test_no_models_found(self, app):
        """When models list is empty, report not available with pull message."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "model": "llama3",
            "ollama_base_url": "http://localhost:11434",
        }
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {"models": []}

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(f"{MODULE}.safe_get", return_value=mock_resp):
                resp = _authed_get(app, "/check/ollama_model")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["available"] is False
        assert "pull" in data["message"].lower()

    def test_json_parse_error(self, app):
        """When response.json() raises ValueError, report parse error."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "model": "llama3",
            "ollama_base_url": "http://localhost:11434",
        }
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.side_effect = ValueError("bad json")

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(f"{MODULE}.safe_get", return_value=mock_resp):
                resp = _authed_get(app, "/check/ollama_model")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["available"] is False
        assert data["error_type"] == "json_parse_error"

    def test_connection_error(self, app):
        """When safe_get raises ConnectionError, report connection error."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "model": "llama3",
            "ollama_base_url": "http://localhost:11434",
        }

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                f"{MODULE}.safe_get",
                side_effect=requests.exceptions.ConnectionError("refused"),
            ):
                resp = _authed_get(app, "/check/ollama_model")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["available"] is False
        assert data["error_type"] == "connection_error"

    def test_timeout_error(self, app):
        """When safe_get raises Timeout, report timeout."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "model": "llama3",
            "ollama_base_url": "http://localhost:11434",
        }

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                f"{MODULE}.safe_get",
                side_effect=requests.exceptions.Timeout("timed out"),
            ):
                resp = _authed_get(app, "/check/ollama_model")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["available"] is False
        assert data["error_type"] == "timeout"

    def test_general_exception(self, app):
        """When an unexpected exception occurs, report exception."""
        app.config["LLM_CONFIG"] = {
            "provider": "ollama",
            "model": "llama3",
            "ollama_base_url": "http://localhost:11434",
        }

        with patch(AUTH_DB_MANAGER, _mock_auth()):
            with patch(
                f"{MODULE}.normalize_url",
                side_effect=RuntimeError("unexpected"),
            ):
                resp = _authed_get(app, "/check/ollama_model")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["available"] is False
        assert data["error_type"] == "exception"


class TestProbeOllamaTags:
    """Direct tests for the shared _probe_ollama_tags helper — the single
    source the status and model-availability checks both consume, so an
    'is Ollama up?' answer can no longer drift between them."""

    def _probe(self, base_url="http://localhost:11434"):
        from local_deep_research.web.routes.api_routes import (
            _probe_ollama_tags,
        )

        return _probe_ollama_tags(base_url)

    def test_ok_new_format(self):
        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {"models": [{"name": "llama3"}]}
        with patch(f"{MODULE}.safe_get", return_value=resp):
            outcome, payload = self._probe()
        assert outcome == "ok"
        assert payload == [{"name": "llama3"}]

    def test_ok_old_format(self):
        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = [{"name": "a"}, {"name": "b"}]
        with patch(f"{MODULE}.safe_get", return_value=resp):
            outcome, payload = self._probe()
        assert outcome == "ok"
        assert len(payload) == 2

    def test_bad_status_returns_status_code(self):
        resp = MagicMock()
        resp.status_code = 503
        with patch(f"{MODULE}.safe_get", return_value=resp):
            outcome, payload = self._probe()
        assert outcome == "bad_status"
        assert payload == 503

    def test_invalid_json(self):
        resp = MagicMock()
        resp.status_code = 200
        resp.json.side_effect = ValueError("bad")
        with patch(f"{MODULE}.safe_get", return_value=resp):
            outcome, payload = self._probe()
        assert outcome == "invalid_json"
        assert payload is None

    def test_connection_error(self):
        import requests

        with patch(
            f"{MODULE}.safe_get",
            side_effect=requests.exceptions.ConnectionError(),
        ):
            outcome, payload = self._probe()
        assert outcome == "connection_error"

    def test_timeout(self):
        import requests

        with patch(
            f"{MODULE}.safe_get", side_effect=requests.exceptions.Timeout()
        ):
            outcome, payload = self._probe()
        assert outcome == "timeout"
