"""
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 success with empty context when parent missing."""

    def test_parent_not_found_returns_empty_context(self, app):
        """Returns success with available_sources=0 when parent is not found."""
        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 == 200
        data = resp.get_json()
        assert data["success"] is True
        assert data["available_sources"] == 0
        assert data["suggested_strategy"] == "source-based"
        assert data["parent_research"]["id"] == "missing-id"


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


# ---------------------------------------------------------------------------
# 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")),
            patch(f"{MODULE}.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()
