"""
Coverage tests for followup_research/routes.py targeting uncovered branches.

Covers:
- prepare_followup: missing parent_research_id returns 400
- prepare_followup: missing question returns 400
- prepare_followup: parent not found returns success with empty context
- prepare_followup: parent found with resources returns full context
- prepare_followup: unexpected exception returns 500
- start_followup: successful start with password from session_password_store
- start_followup: password fallback to g.user_password
- start_followup: password fallback to temp_auth_store
- start_followup: no password available logs warning
- start_followup: unexpected exception returns 500
"""

from contextlib import contextmanager
from unittest.mock import MagicMock, Mock, patch

import pytest
from flask import Flask

from local_deep_research.web.auth import auth_bp
from local_deep_research.followup_research.routes import followup_bp

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

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

# Lazy-imported symbols (imported inside route function bodies, not at module level)
SETTINGS_MANAGER = "local_deep_research.settings.manager.SettingsManager"
DB_SESSION_CTX = (
    "local_deep_research.database.session_context.get_user_db_session"
)
RESEARCH_HISTORY = "local_deep_research.database.models.ResearchHistory"
SESSION_PWD_STORE = (
    "local_deep_research.database.session_passwords.session_password_store"
)
TEMP_AUTH_STORE = "local_deep_research.database.temp_auth.temp_auth_store"
START_RESEARCH = (
    "local_deep_research.web.services.research_service.start_research_process"
)
RUN_RESEARCH = (
    "local_deep_research.web.services.research_service.run_research_process"
)


# ---------------------------------------------------------------------------
# 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 _fake_settings_snapshot():
    """Return a fake settings snapshot dict."""
    return {
        "search.search_strategy": {"value": "source-based"},
        "search.iterations": {"value": 2},
        "search.questions_per_iteration": {"value": 4},
        "llm.provider": {"value": "OLLAMA"},
        "llm.model": {"value": "gemma3:12b"},
        "search.tool": {"value": "searxng"},
        "llm.openai_endpoint.url": {"value": None},
    }


def _make_settings_mock():
    """Return a SettingsManager mock returning a fake snapshot."""
    mock_sm = Mock()
    mock_sm.get_all_settings.return_value = _fake_settings_snapshot()
    return mock_sm


@contextmanager
def _mock_db_session_ctx():
    """Context manager that patches get_user_db_session (lazy import) to yield a MagicMock."""
    mock_db = MagicMock()

    @contextmanager
    def fake_get_user_db_session(username):
        yield mock_db

    with patch(DB_SESSION_CTX, side_effect=fake_get_user_db_session):
        yield mock_db


def _authed_post(app, path, json_body, extra_session=None):
    """Issue an authenticated POST request and return the response."""
    with app.test_client() as c:
        with c.session_transaction() as sess:
            sess["username"] = "testuser"
            if extra_session:
                sess.update(extra_session)
        return c.post(path, json=json_body)


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


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


# ---------------------------------------------------------------------------
# prepare_followup: missing fields (lines 48-54)
# ---------------------------------------------------------------------------


class TestPrepareFollowupMissingFields:
    """prepare_followup returns 400 when required fields are absent."""

    def test_missing_parent_id_returns_400(self, app):
        """Returns 400 when parent_research_id is not provided."""
        with patch(AUTH_DB_MANAGER, _mock_auth()):
            resp = _authed_post(
                app, "/api/followup/prepare", {"question": "why?"}
            )
        assert resp.status_code == 400
        data = resp.get_json()
        assert data["success"] is False
        assert "Missing" in data["error"]

    def test_missing_question_returns_400(self, app):
        """Returns 400 when question is not provided."""
        with patch(AUTH_DB_MANAGER, _mock_auth()):
            resp = _authed_post(
                app,
                "/api/followup/prepare",
                {"parent_research_id": "abc-123"},
            )
        assert resp.status_code == 400
        data = resp.get_json()
        assert data["success"] is False
        assert "Missing" in data["error"]


# ---------------------------------------------------------------------------
# prepare_followup: parent not found (lines 78-95)
# ---------------------------------------------------------------------------


class TestPrepareFollowupParentNotFound:
    """prepare_followup returns 404 when parent research does not exist."""

    def test_parent_not_found_returns_404(self, app):
        """Returns 404 with success=False when parent_research_id has no row.

        Earlier code returned 200 + fabricated placeholder data here,
        which let the frontend silently render dummy context and
        trigger a follow-up LLM call against a ghost parent. The
        contract now matches the rest of the API: 404 means "not
        found" and the caller is expected to surface that to the user.
        """
        mock_service = Mock()
        mock_service.load_parent_research.return_value = None

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=_make_settings_mock()),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
        ):
            resp = _authed_post(
                app,
                "/api/followup/prepare",
                {"parent_research_id": "missing-id", "question": "follow?"},
            )

        assert resp.status_code == 404
        data = resp.get_json()
        assert data["success"] is False
        assert "Parent research not found" in data["error"]


# ---------------------------------------------------------------------------
# prepare_followup: parent found with resources (lines 97-110)
# ---------------------------------------------------------------------------


class TestPrepareFollowupParentFound:
    """prepare_followup returns full context when parent exists."""

    def test_parent_found_with_resources(self, app):
        """Returns parent summary and correct source count when parent exists."""
        mock_service = Mock()
        mock_service.load_parent_research.return_value = {
            "query": "original question",
            "resources": [{"url": "http://a.com"}, {"url": "http://b.com"}],
        }

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=_make_settings_mock()),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
        ):
            resp = _authed_post(
                app,
                "/api/followup/prepare",
                {"parent_research_id": "found-id", "question": "more?"},
            )

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["success"] is True
        assert data["parent_summary"] == "original question"
        assert data["available_sources"] == 2
        assert data["parent_research"]["sources_count"] == 2
        assert data["parent_research"]["id"] == "found-id"


# ---------------------------------------------------------------------------
# prepare_followup: exception path (lines 112-116)
# ---------------------------------------------------------------------------


class TestPrepareFollowupException:
    """prepare_followup returns 500 on unexpected exception."""

    def test_exception_in_service_returns_500(self, app):
        """Returns 500 when an unexpected exception occurs."""
        mock_service = Mock()
        mock_service.load_parent_research.side_effect = RuntimeError("boom")

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=_make_settings_mock()),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
        ):
            resp = _authed_post(
                app,
                "/api/followup/prepare",
                {"parent_research_id": "x", "question": "y"},
            )

        assert resp.status_code == 500
        data = resp.get_json()
        assert data["success"] is False
        assert "internal error" in data["error"].lower()


# ---------------------------------------------------------------------------
# start_followup: successful start with session_password_store (lines 243-249)
# ---------------------------------------------------------------------------


class TestStartFollowupSpawnFailure:
    """If start_research_process raises, flip ResearchHistory.status to FAILED
    and return 500 — don't leave the row orphaned as IN_PROGRESS."""

    def test_spawn_failure_marks_research_failed(self, app):
        from local_deep_research.constants import ResearchStatus

        mock_service = Mock()
        mock_service.perform_followup.return_value = {
            "query": "follow-up question",
            "max_iterations": 2,
            "questions_per_iteration": 4,
            "delegate_strategy": "source-based",
            "research_context": {"summary": "context"},
            "parent_research_id": "parent-1",
        }

        research_row = Mock()
        mock_db = MagicMock()
        mock_db.query.return_value.filter_by.return_value.first.return_value = (
            research_row
        )

        @contextmanager
        def fake_get_user_db_session(username):
            yield mock_db

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=_make_settings_mock()),
            patch(DB_SESSION_CTX, side_effect=fake_get_user_db_session),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
            patch(START_RESEARCH, side_effect=RuntimeError("spawn failed")),
            patch(RUN_RESEARCH),
            patch(RESEARCH_HISTORY),
            patch(
                "uuid.uuid4",
                return_value=Mock(__str__=lambda s: "failing-research-id"),
            ),
            patch(SESSION_PWD_STORE) as mock_pwd_store,
        ):
            mock_pwd_store.retrieve.return_value = "secret-password"
            resp = _authed_post(
                app,
                "/api/followup/start",
                {"parent_research_id": "parent-1", "question": "details?"},
                extra_session={"session_id": "sess-123"},
            )

        assert resp.status_code == 500
        data = resp.get_json()
        assert data["success"] is False
        # ResearchHistory row was flipped to FAILED by the cleanup handler.
        assert research_row.status == ResearchStatus.FAILED
        # Cleanup commit was issued.
        mock_db.commit.assert_called()


class TestStartFollowupSuccess:
    """start_followup succeeds and retrieves password from session_password_store."""

    def test_start_success_with_session_password(self, app):
        """Successful start; password retrieved from session_password_store."""
        mock_service = Mock()
        mock_service.perform_followup.return_value = {
            "query": "follow-up question",
            "max_iterations": 2,
            "questions_per_iteration": 4,
            "delegate_strategy": "source-based",
            "research_context": {"summary": "context"},
            "parent_research_id": "parent-1",
        }

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=_make_settings_mock()),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
            patch(START_RESEARCH),
            patch(RUN_RESEARCH),
            patch(RESEARCH_HISTORY),
            patch(
                "uuid.uuid4",
                return_value=Mock(__str__=lambda s: "new-research-id"),
            ),
            patch(SESSION_PWD_STORE) as mock_pwd_store,
        ):
            mock_pwd_store.retrieve.return_value = "secret-password"
            resp = _authed_post(
                app,
                "/api/followup/start",
                {"parent_research_id": "parent-1", "question": "details?"},
                extra_session={"session_id": "sess-123"},
            )

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["success"] is True
        assert "research_id" in data
        assert data["message"] == "Follow-up research started"


class TestStartFollowupModelRequired:
    """start_followup rejects an empty llm.model before spawning anything."""

    def test_empty_model_returns_400_before_spawn(self, app):
        """Empty ``llm.model`` returns HTTP 400 with an actionable message
        *before* any ResearchHistory row is written or worker thread spawned,
        mirroring research_routes.start_research. Without this guard the
        worker thread dies and leaves an orphan IN_PROGRESS row.
        """
        mock_service = Mock()
        mock_service.perform_followup.return_value = {
            "query": "follow-up question",
            "max_iterations": 2,
            "questions_per_iteration": 4,
            "delegate_strategy": "source-based",
            "research_context": {"summary": "context"},
            "parent_research_id": "parent-1",
        }

        # Settings snapshot with an empty model — the regression under test.
        snapshot = _fake_settings_snapshot()
        snapshot["llm.model"] = {"value": ""}
        mock_sm = Mock()
        mock_sm.get_all_settings.return_value = snapshot

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=mock_sm),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
            patch(START_RESEARCH) as mock_start,
            patch(RUN_RESEARCH) as mock_run,
            patch(RESEARCH_HISTORY) as mock_history,
            patch(SESSION_PWD_STORE) as mock_pwd_store,
        ):
            mock_pwd_store.retrieve.return_value = "secret-password"
            resp = _authed_post(
                app,
                "/api/followup/start",
                {"parent_research_id": "parent-1", "question": "details?"},
                extra_session={"session_id": "sess-123"},
            )

        assert resp.status_code == 400
        data = resp.get_json()
        assert data["success"] is False
        assert "Model is required" in data["error"]
        # No orphan ResearchHistory row and no worker thread were created.
        mock_history.assert_not_called()
        mock_start.assert_not_called()
        mock_run.assert_not_called()


class TestStartFollowupCustomEndpointSSRF:
    """SSRF pre-flight on llm.openai_endpoint.url.

    The endpoint URL is later handed to the OpenAI client (httpx) with no
    SafeSession wrapping, so the route layer is the only place to reject
    cloud-metadata / link-local targets. Like the empty-model guard above,
    the check fires BEFORE any ResearchHistory row is written so a rejected
    request leaves no orphan IN_PROGRESS row.
    """

    _AWS_METADATA = (
        "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
    )

    def _snapshot_with_endpoint(self, url):
        snapshot = _fake_settings_snapshot()
        snapshot["llm.openai_endpoint.url"] = {"value": url}
        return snapshot

    def test_metadata_endpoint_rejected_before_db_write(self, app):
        """Cloud metadata URLs are blocked before ResearchHistory is created."""
        mock_service = Mock()
        mock_service.perform_followup.return_value = {
            "query": "follow-up question",
            "max_iterations": 2,
            "questions_per_iteration": 4,
            "delegate_strategy": "source-based",
            "research_context": {"summary": "context"},
            "parent_research_id": "parent-1",
        }
        mock_sm = Mock()
        mock_sm.get_all_settings.return_value = self._snapshot_with_endpoint(
            self._AWS_METADATA
        )

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=mock_sm),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
            patch(START_RESEARCH) as mock_start,
            patch(RUN_RESEARCH) as mock_run,
            patch(RESEARCH_HISTORY) as mock_history,
            patch(SESSION_PWD_STORE) as mock_pwd_store,
        ):
            mock_pwd_store.retrieve.return_value = "secret-password"
            resp = _authed_post(
                app,
                "/api/followup/start",
                {"parent_research_id": "parent-1", "question": "details?"},
                extra_session={"session_id": "sess-123"},
            )

        assert resp.status_code == 400
        data = resp.get_json()
        assert data["success"] is False
        assert "endpoint" in data["error"].lower()
        mock_history.assert_not_called()
        mock_start.assert_not_called()
        mock_run.assert_not_called()

    def test_garbage_url_rejected_before_db_write(self, app):
        """Malformed URLs are rejected before ResearchHistory is created."""
        mock_service = Mock()
        mock_service.perform_followup.return_value = {
            "query": "q",
            "max_iterations": 1,
            "questions_per_iteration": 1,
            "delegate_strategy": "source-based",
            "research_context": {},
            "parent_research_id": "parent-1",
        }
        mock_sm = Mock()
        mock_sm.get_all_settings.return_value = self._snapshot_with_endpoint(
            "not-a-url"
        )

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=mock_sm),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
            patch(START_RESEARCH) as mock_start,
            patch(RUN_RESEARCH),
            patch(RESEARCH_HISTORY) as mock_history,
            patch(SESSION_PWD_STORE) as mock_pwd_store,
        ):
            mock_pwd_store.retrieve.return_value = "secret-password"
            resp = _authed_post(
                app,
                "/api/followup/start",
                {"parent_research_id": "parent-1", "question": "details?"},
                extra_session={"session_id": "sess-123"},
            )

        assert resp.status_code == 400
        mock_history.assert_not_called()
        mock_start.assert_not_called()

    def test_localhost_endpoint_accepted(self, app):
        """Local LLM endpoints on localhost pass validation and reach the spawn."""
        mock_service = Mock()
        mock_service.perform_followup.return_value = {
            "query": "q",
            "max_iterations": 1,
            "questions_per_iteration": 1,
            "delegate_strategy": "source-based",
            "research_context": {},
            "parent_research_id": "parent-1",
        }
        mock_sm = Mock()
        mock_sm.get_all_settings.return_value = self._snapshot_with_endpoint(
            "http://localhost:11434/v1"
        )

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=mock_sm),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
            patch(START_RESEARCH) as mock_start,
            patch(RUN_RESEARCH),
            patch(RESEARCH_HISTORY),
            patch(
                "uuid.uuid4",
                return_value=Mock(__str__=lambda s: "new-research-id"),
            ),
            patch(SESSION_PWD_STORE) as mock_pwd_store,
        ):
            mock_pwd_store.retrieve.return_value = "secret-password"
            resp = _authed_post(
                app,
                "/api/followup/start",
                {"parent_research_id": "parent-1", "question": "details?"},
                extra_session={"session_id": "sess-123"},
            )

        assert resp.status_code == 200
        mock_start.assert_called_once()


# ---------------------------------------------------------------------------
# start_followup: password fallback to g.user_password (lines 251-253)
# ---------------------------------------------------------------------------


class TestStartFollowupPasswordFallbackG:
    """start_followup falls back to g.user_password when session store returns None."""

    def test_password_fallback_to_g_user_password(self, app):
        """Password falls back to g.user_password when session_password_store yields None."""
        mock_service = Mock()
        mock_service.perform_followup.return_value = {
            "query": "q",
            "max_iterations": 1,
            "questions_per_iteration": 3,
            "delegate_strategy": "source-based",
            "research_context": {},
            "parent_research_id": "p1",
        }

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=_make_settings_mock()),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
            patch(START_RESEARCH),
            patch(RUN_RESEARCH),
            patch(RESEARCH_HISTORY),
            patch("uuid.uuid4", return_value=Mock(__str__=lambda s: "id2")),
            patch(SESSION_PWD_STORE) as mock_pwd_store,
        ):
            mock_pwd_store.retrieve.return_value = None
            resp = _authed_post(
                app,
                "/api/followup/start",
                {"parent_research_id": "p1", "question": "q?"},
                extra_session={"session_id": "sess-456"},
            )

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


# ---------------------------------------------------------------------------
# start_followup: password fallback to temp_auth_store (lines 255-264)
# ---------------------------------------------------------------------------


class TestStartFollowupPasswordFallbackTempAuth:
    """start_followup falls back to temp_auth_store when other sources return None."""

    def test_password_fallback_to_temp_auth(self, app):
        """Password is taken from temp_auth_store when session and g sources are absent."""
        mock_service = Mock()
        mock_service.perform_followup.return_value = {
            "query": "q",
            "max_iterations": 1,
            "questions_per_iteration": 3,
            "delegate_strategy": "source-based",
            "research_context": {},
            "parent_research_id": "p1",
        }

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=_make_settings_mock()),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
            patch(START_RESEARCH),
            patch(RUN_RESEARCH),
            patch(RESEARCH_HISTORY),
            patch("uuid.uuid4", return_value=Mock(__str__=lambda s: "id3")),
            patch(SESSION_PWD_STORE) as mock_pwd_store,
            patch(TEMP_AUTH_STORE) as mock_temp_auth,
        ):
            mock_pwd_store.retrieve.return_value = None
            mock_temp_auth.peek_auth.return_value = ("testuser", "temp-pw")
            resp = _authed_post(
                app,
                "/api/followup/start",
                {"parent_research_id": "p1", "question": "q?"},
                extra_session={
                    "session_id": "sess-789",
                    "temp_auth_token": "tok-abc",
                },
            )

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


# ---------------------------------------------------------------------------
# start_followup: no password available logs warning (lines 266-269)
# ---------------------------------------------------------------------------


class TestStartFollowupNoPassword:
    """start_followup logs a warning when no password is available from any source."""

    def test_no_password_logs_warning(self, app):
        """Logs warning when password cannot be retrieved from any source.

        With non-encrypted DB, no password just logs a warning and continues.
        We must mock has_encryption=False, otherwise the early password
        check returns 401 (correct behavior for encrypted DBs).
        """
        mock_service = Mock()
        mock_service.perform_followup.return_value = {
            "query": "q",
            "max_iterations": 1,
            "questions_per_iteration": 3,
            "delegate_strategy": "source-based",
            "research_context": {},
            "parent_research_id": "p1",
        }

        mock_db_mgr = MagicMock(has_encryption=False)

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=_make_settings_mock()),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
            patch(START_RESEARCH),
            patch(RUN_RESEARCH),
            patch(RESEARCH_HISTORY),
            patch("uuid.uuid4", return_value=Mock(__str__=lambda s: "id4")),
            # The unencrypted "no password" warning now fires inside the
            # shared resolve_user_password helper, so assert on its logger.
            patch(
                "local_deep_research.web.auth.password_utils.logger"
            ) as mock_logger,
            patch(
                "local_deep_research.database.encrypted_db.db_manager",
                mock_db_mgr,
            ),
        ):
            # No session_id and no temp_auth_token in session
            resp = _authed_post(
                app,
                "/api/followup/start",
                {"parent_research_id": "p1", "question": "q?"},
            )

        assert resp.status_code == 200
        warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
        assert any("No password available" in call for call in warning_calls)


# ---------------------------------------------------------------------------
# start_followup: exception path (lines 317-321)
# ---------------------------------------------------------------------------


class TestStartFollowupException:
    """start_followup returns 500 on unexpected exception."""

    def test_exception_returns_500(self, app):
        """Returns 500 when an unexpected exception occurs in start_followup."""
        mock_service = Mock()
        mock_service.perform_followup.side_effect = RuntimeError("kaboom")

        with (
            patch(AUTH_DB_MANAGER, _mock_auth()),
            patch(SETTINGS_MANAGER, return_value=_make_settings_mock()),
            _mock_db_session_ctx(),
            patch(
                f"{MODULE}.FollowUpResearchService", return_value=mock_service
            ),
        ):
            resp = _authed_post(
                app,
                "/api/followup/start",
                {"parent_research_id": "x", "question": "y"},
            )

        assert resp.status_code == 500
        data = resp.get_json()
        assert data["success"] is False
        assert "internal error" in data["error"].lower()
