"""
View-focused coverage tests for library_routes.py.

Covers:
- library_page with domain/research/collection filters
- document_details_page not-found path
- view_pdf_page no-document and no-file_path paths
- view_text_page no-document and no-content paths
- open_folder disabled (403)
- download_text_single success path
- get_authenticated_user_password: session store, g fallback, no password
"""

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

import pytest
from flask import Flask, jsonify

from local_deep_research.web.auth.routes import auth_bp
from local_deep_research.research_library.routes.library_routes import (
    library_bp,
)

_ROUTES = "local_deep_research.research_library.routes.library_routes"


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


def _create_app():
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"
    app.config["WTF_CSRF_ENABLED"] = False
    app.config["TESTING"] = True
    app.register_blueprint(auth_bp)
    app.register_blueprint(library_bp)

    @app.errorhandler(500)
    def _handle_500(error):
        return jsonify({"error": "Internal server error"}), 500

    return app


def _mock_db_manager():
    mock_db = Mock()
    mock_db.is_user_connected.return_value = True
    mock_db.connections = {"testuser": True}
    mock_db.has_encryption = False
    return mock_db


def _build_mock_query(
    all_result=None, first_result=None, count_result=0, get_result=None
):
    q = Mock()
    q.all.return_value = all_result if all_result is not None else []
    q.first.return_value = first_result
    q.count.return_value = count_result
    q.get.return_value = get_result
    q.filter_by.return_value = q
    q.filter.return_value = q
    q.order_by.return_value = q
    q.outerjoin.return_value = q
    q.join.return_value = q
    q.limit.return_value = q
    q.offset.return_value = q
    q.delete.return_value = 0
    return q


@contextmanager
def _auth_client(
    app,
    library_service=None,
    download_service=None,
    mock_db_session=None,
    settings_overrides=None,
    get_auth_password="mock_password",
    render_return="<html>ok</html>",
    extra_patches=None,
):
    mock_db = _mock_db_manager()
    lib_svc = library_service or Mock()
    lib_cls = Mock(return_value=lib_svc)
    dl_svc = download_service or Mock()
    dl_svc.__enter__ = Mock(return_value=dl_svc)
    dl_svc.__exit__ = Mock(return_value=False)
    dl_cls = Mock(return_value=dl_svc)
    db_session = mock_db_session or Mock()
    if not hasattr(db_session, "query") or not callable(
        getattr(db_session, "query", None)
    ):
        db_session = Mock()
        db_session.query = Mock(return_value=_build_mock_query())
    db_session.commit = Mock()
    db_session.add = Mock()

    @contextmanager
    def fake_get_user_db_session(*a, **kw):
        yield db_session

    mock_sm = Mock()
    defaults = {
        "research_library.pdf_storage_mode": "database",
        "research_library.shared_library": False,
        "research_library.storage_path": "/tmp/test_lib",
    }
    if settings_overrides:
        defaults.update(settings_overrides)
    mock_sm.get_setting.side_effect = lambda k, d=None: defaults.get(k, d)
    mock_render = Mock(return_value=render_return)
    patches = [
        patch("local_deep_research.web.auth.decorators.db_manager", mock_db),
        patch(f"{_ROUTES}.LibraryService", lib_cls),
        patch(f"{_ROUTES}.DownloadService", dl_cls),
        patch(
            f"{_ROUTES}.get_user_db_session",
            side_effect=fake_get_user_db_session,
        ),
        patch(f"{_ROUTES}.get_settings_manager", return_value=mock_sm),
        patch(
            "local_deep_research.utilities.db_utils.get_settings_manager",
            return_value=mock_sm,
        ),
        patch(f"{_ROUTES}.render_template_with_defaults", mock_render),
        patch(
            f"{_ROUTES}.get_authenticated_user_password",
            return_value=get_auth_password,
        ),
        patch(
            "local_deep_research.database.library_init.get_default_library_id",
            return_value=None,
        ),
    ]
    if extra_patches:
        patches.extend(extra_patches)
    started = []
    try:
        for p in patches:
            started.append(p.start())
        with app.test_client() as client:
            with client.session_transaction() as sess:
                sess["username"] = "testuser"
                sess["session_id"] = "test-session-id"
            yield (
                client,
                {
                    "library_service": lib_svc,
                    "download_service": dl_svc,
                    "download_cls": dl_cls,
                    "db_session": db_session,
                    "settings": mock_sm,
                    "render": mock_render,
                },
            )
    finally:
        for p in patches:
            p.stop()


@pytest.fixture
def app():
    return _create_app()


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestLibraryPageWithFilters:
    """library_page passes domain/research/collection query params to the service."""

    def test_library_page_with_domain_filter(self, app):
        lib_svc = Mock()
        lib_svc.get_library_stats.return_value = {"storage_path": "/tmp"}
        lib_svc.get_documents.return_value = []
        lib_svc.get_unique_domains.return_value = ["arxiv.org"]
        lib_svc.get_research_list_for_dropdown.return_value = []
        lib_svc.get_all_collections.return_value = []
        lib_svc.count_documents.return_value = 0

        with _auth_client(app, library_service=lib_svc) as (client, ctx):
            resp = client.get("/library/?domain=arxiv.org")

        assert resp.status_code == 200
        lib_svc.get_documents.assert_called_once_with(
            research_id=None,
            domain="arxiv.org",
            collection_id=None,
            date_filter=None,
            limit=100,
            offset=0,
        )

    def test_library_page_with_research_filter(self, app):
        lib_svc = Mock()
        lib_svc.get_library_stats.return_value = {"storage_path": "/tmp"}
        lib_svc.get_documents.return_value = []
        lib_svc.get_unique_domains.return_value = []
        lib_svc.get_research_list_for_dropdown.return_value = []
        lib_svc.get_all_collections.return_value = []
        lib_svc.count_documents.return_value = 0

        with _auth_client(app, library_service=lib_svc) as (client, ctx):
            resp = client.get("/library/?research=42")

        assert resp.status_code == 200
        lib_svc.get_documents.assert_called_once_with(
            research_id="42",
            domain=None,
            collection_id=None,
            date_filter=None,
            limit=100,
            offset=0,
        )

    def test_library_page_with_collection_filter(self, app):
        lib_svc = Mock()
        lib_svc.get_library_stats.return_value = {"storage_path": "/tmp"}
        lib_svc.get_documents.return_value = []
        lib_svc.get_unique_domains.return_value = []
        lib_svc.get_research_list_for_dropdown.return_value = []
        lib_svc.get_all_collections.return_value = []
        lib_svc.count_documents.return_value = 0

        with _auth_client(app, library_service=lib_svc) as (client, ctx):
            resp = client.get("/library/?collection=99")

        assert resp.status_code == 200
        lib_svc.get_documents.assert_called_once_with(
            research_id=None,
            domain=None,
            collection_id="99",
            date_filter=None,
            limit=100,
            offset=0,
        )
        # Render call includes selected_collection
        render_call_kwargs = ctx["render"].call_args[1]
        assert render_call_kwargs["selected_collection"] == "99"

    def test_library_page_no_filters(self, app):
        lib_svc = Mock()
        lib_svc.get_library_stats.return_value = {"storage_path": ""}
        lib_svc.get_documents.return_value = []
        lib_svc.get_unique_domains.return_value = []
        lib_svc.get_research_list_for_dropdown.return_value = []
        lib_svc.get_all_collections.return_value = []
        lib_svc.count_documents.return_value = 0

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.get("/library/")

        assert resp.status_code == 200
        lib_svc.get_documents.assert_called_once_with(
            research_id=None,
            domain=None,
            collection_id=None,
            date_filter=None,
            limit=100,
            offset=0,
        )


class TestDocumentDetailsNotFound:
    """document_details_page returns 404 when service returns None."""

    def test_document_details_not_found(self, app):
        lib_svc = Mock()
        lib_svc.get_document_by_id.return_value = None

        with _auth_client(app, library_service=lib_svc) as (client, _):
            resp = client.get("/library/document/nonexistent-doc-id")

        assert resp.status_code == 404
        assert b"not found" in resp.data.lower()


class TestViewPdfNoDocument:
    """view_pdf_page returns 404 when the Document row does not exist."""

    def test_view_pdf_no_document(self, app):
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=None)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/document/ghost-id/pdf")

        assert resp.status_code == 404
        assert b"not found" in resp.data.lower()


class TestViewPdfNoFilePath:
    """view_pdf_page returns 404 when PDFStorageManager returns None (no bytes)."""

    def test_view_pdf_no_file_path(self, app):
        doc = Mock(id="doc-1", file_path=None, filename="paper.pdf")
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=doc)

        mgr = Mock()
        mgr.load_pdf.return_value = None  # No PDF bytes available

        with _auth_client(
            app,
            mock_db_session=db_session,
            extra_patches=[
                patch(f"{_ROUTES}.PDFStorageManager", return_value=mgr),
                patch(
                    f"{_ROUTES}.get_library_directory",
                    return_value="/tmp/lib",
                ),
            ],
        ) as (client, _):
            resp = client.get("/library/document/doc-1/pdf")

        assert resp.status_code == 404
        assert b"not available" in resp.data.lower()


class TestViewTextNoDocument:
    """view_text_page returns 404 when Document row does not exist."""

    def test_view_text_no_document(self, app):
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=None)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/document/missing-doc/txt")

        assert resp.status_code == 404
        assert b"not found" in resp.data.lower()


class TestViewTextNoContent:
    """view_text_page returns 404 when document has no text_content."""

    def test_view_text_no_content_none(self, app):
        doc = Mock(text_content=None, title="Empty Doc")
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=doc)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/document/empty-doc/txt")

        assert resp.status_code == 404
        assert b"not available" in resp.data.lower()

    def test_view_text_no_content_empty_string(self, app):
        doc = Mock(text_content="", title="Empty Doc")
        db_session = Mock()
        db_session.query.return_value = _build_mock_query(first_result=doc)

        with _auth_client(app, mock_db_session=db_session) as (client, _):
            resp = client.get("/library/document/empty-doc/txt")

        assert resp.status_code == 404


class TestOpenFolderDisabled:
    """open_folder always returns 403 (disabled for server deployments)."""

    def test_open_folder_disabled(self, app):
        with _auth_client(app) as (client, _):
            resp = client.post("/library/api/open-folder")

        assert resp.status_code == 403
        data = resp.get_json()
        assert data["status"] == "error"
        assert "disabled" in data["message"].lower()


class TestDownloadTextSingleSuccess:
    """download_text_single returns success JSON on the happy path."""

    def test_download_text_single_success(self, app):
        dl_svc = Mock()
        dl_svc.download_as_text.return_value = (True, None)
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)

        with _auth_client(app, download_service=dl_svc) as (client, _):
            resp = client.post("/library/api/download-text/7")

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

    def test_download_text_single_failure(self, app):
        dl_svc = Mock()
        dl_svc.download_as_text.return_value = (False, "some internal error")
        dl_svc.__enter__ = Mock(return_value=dl_svc)
        dl_svc.__exit__ = Mock(return_value=False)

        with _auth_client(app, download_service=dl_svc) as (client, _):
            resp = client.post("/library/api/download-text/8")

        assert resp.status_code == 200
        data = resp.get_json()
        assert data["success"] is False
        # Internal error message must NOT be exposed to caller
        assert "some internal error" not in data.get("error", "")


class TestGetAuthenticatedUserPasswordPaths:
    """Unit tests for get_authenticated_user_password helper.

    The function imports session_password_store inside its body:
        from ...database.session_passwords import session_password_store
    so the correct patch target is the store object on that module, not on
    library_routes.
    """

    _STORE = (
        "local_deep_research.database.session_passwords.session_password_store"
    )

    def test_returns_password_from_session_store(self, app):
        """Happy path: session_password_store returns a password."""
        from local_deep_research.research_library.routes.library_routes import (
            get_authenticated_user_password,
        )

        mock_store = Mock()
        mock_store.get_session_password.return_value = "secret123"

        with app.test_request_context("/"):
            with patch(self._STORE, mock_store):
                result = get_authenticated_user_password(
                    "testuser", flask_session_id="sid-abc"
                )

        assert result == "secret123"
        mock_store.get_session_password.assert_called_once_with(
            "testuser", "sid-abc"
        )

    def test_falls_back_to_g_user_password(self, app):
        """Falls back to g.user_password when session store returns None."""
        from local_deep_research.research_library.routes.library_routes import (
            get_authenticated_user_password,
        )

        mock_store = Mock()
        mock_store.get_session_password.return_value = None

        with app.test_request_context("/"):
            from flask import g

            g.user_password = "g_password_fallback"

            with patch(self._STORE, mock_store):
                result = get_authenticated_user_password(
                    "testuser", flask_session_id="sid-xyz"
                )

        assert result == "g_password_fallback"

    def test_raises_authentication_required_error_when_no_password(self, app):
        """Raises AuthenticationRequiredError when no password source is available."""
        from local_deep_research.research_library.routes.library_routes import (
            get_authenticated_user_password,
        )
        from local_deep_research.web.exceptions import (
            AuthenticationRequiredError,
        )

        mock_store = Mock()
        mock_store.get_session_password.return_value = None

        with app.test_request_context("/"):
            # g.user_password must NOT be set — fresh request context has none
            with patch(self._STORE, mock_store):
                with pytest.raises(AuthenticationRequiredError):
                    get_authenticated_user_password(
                        "testuser", flask_session_id="sid-none"
                    )

    def test_falls_back_when_session_store_raises_exception(self, app):
        """When session store raises an exception, g.user_password is used."""
        from local_deep_research.research_library.routes.library_routes import (
            get_authenticated_user_password,
        )

        mock_store = Mock()
        mock_store.get_session_password.side_effect = RuntimeError(
            "store unavailable"
        )

        with app.test_request_context("/"):
            from flask import g

            g.user_password = "fallback_after_exception"

            with patch(self._STORE, mock_store):
                result = get_authenticated_user_password(
                    "testuser", flask_session_id="sid-err"
                )

        assert result == "fallback_after_exception"
