"""
Tests for LibraryService.
"""

from unittest.mock import Mock, patch, MagicMock


class TestLibraryServiceUrlDetection:
    """Tests for URL detection methods."""

    def test_is_arxiv_url_with_arxiv_domain(self):
        """Detects arxiv.org URLs correctly."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            # Valid arXiv URLs
            assert (
                service._is_arxiv_url("https://arxiv.org/abs/2301.00001")
                is True
            )
            assert (
                service._is_arxiv_url("https://arxiv.org/pdf/2301.00001.pdf")
                is True
            )
            assert (
                service._is_arxiv_url("http://arxiv.org/abs/1234.5678") is True
            )
            assert (
                service._is_arxiv_url("https://export.arxiv.org/abs/2301.00001")
                is True
            )

    def test_is_arxiv_url_with_non_arxiv_domain(self):
        """Rejects non-arXiv URLs."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            assert service._is_arxiv_url("https://google.com") is False
            assert (
                service._is_arxiv_url("https://pubmed.ncbi.nlm.nih.gov/12345")
                is False
            )
            assert (
                service._is_arxiv_url("https://example.com/arxiv.org") is False
            )

    def test_is_arxiv_url_with_invalid_url(self):
        """Handles invalid URLs gracefully."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            assert service._is_arxiv_url("not a url") is False
            assert service._is_arxiv_url("") is False

    def test_is_pubmed_url_with_pubmed_domain(self):
        """Detects PubMed URLs correctly."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            # Valid PubMed URLs
            assert (
                service._is_pubmed_url(
                    "https://pubmed.ncbi.nlm.nih.gov/12345678"
                )
                is True
            )
            assert (
                service._is_pubmed_url(
                    "https://ncbi.nlm.nih.gov/pmc/articles/PMC1234567"
                )
                is True
            )

    def test_is_pubmed_url_with_non_pubmed_domain(self):
        """Rejects non-PubMed URLs."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            assert (
                service._is_pubmed_url("https://arxiv.org/abs/2301.00001")
                is False
            )
            assert service._is_pubmed_url("https://google.com") is False


class TestLibraryServiceDomainExtraction:
    """Tests for domain extraction."""

    def test_extract_domain_from_url(self):
        """Extracts domain from URL correctly."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            assert (
                service._extract_domain("https://arxiv.org/abs/2301.00001")
                == "arxiv.org"
            )
            assert (
                service._extract_domain("https://pubmed.ncbi.nlm.nih.gov/12345")
                == "pubmed.ncbi.nlm.nih.gov"
            )
            assert (
                service._extract_domain("https://example.com/path")
                == "example.com"
            )

    def test_extract_domain_with_invalid_url(self):
        """Handles invalid URLs gracefully."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            assert service._extract_domain("not a url") == ""
            assert service._extract_domain("") == ""


class TestLibraryServiceUrlHash:
    """Tests for URL hashing."""

    def test_get_url_hash_normalizes_url(self):
        """URL hashing normalizes URLs before hashing."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            # Same URL with different protocols should produce same hash
            hash1 = service._get_url_hash("https://arxiv.org/abs/2301.00001")
            hash2 = service._get_url_hash("http://arxiv.org/abs/2301.00001")
            assert hash1 == hash2

    def test_get_url_hash_removes_www(self):
        """URL hashing removes www prefix."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            hash1 = service._get_url_hash("https://www.example.com/page")
            hash2 = service._get_url_hash("https://example.com/page")
            assert hash1 == hash2

    def test_get_url_hash_removes_trailing_slash(self):
        """URL hashing removes trailing slashes."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            hash1 = service._get_url_hash("https://example.com/page/")
            hash2 = service._get_url_hash("https://example.com/page")
            assert hash1 == hash2


class TestLibraryServiceToggleFavorite:
    """Tests for toggling document favorites."""

    def test_toggle_favorite_document_found(self, library_session, mocker):
        """Toggles favorite status when document exists."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        # Create a mock document
        mock_doc = Mock()
        mock_doc.favorite = False

        # Mock the session context
        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.get.return_value = mock_doc
        mock_session_context.return_value = mock_session

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.toggle_favorite("doc-123")

            # Should toggle to True
            assert mock_doc.favorite is True
            assert result is True

    def test_toggle_favorite_document_not_found(self, mocker):
        """Returns False when document doesn't exist."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        # Mock the session context
        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.get.return_value = None
        mock_session_context.return_value = mock_session

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.toggle_favorite("nonexistent-doc")
            assert result is False


class TestLibraryServiceDeleteDocument:
    """Tests for document deletion."""

    def test_delete_document_not_found(self, mocker):
        """Returns False when document doesn't exist."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        # Mock the session context
        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.get.return_value = None
        mock_session_context.return_value = mock_session

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.delete_document("nonexistent-doc")
            assert result is False


class TestLibraryServiceGetUniqueDomains:
    """Tests for getting unique domains."""

    def test_get_unique_domains_returns_list(self, mocker):
        """Returns a sorted list of unique netlocs from all document URLs."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        # Mock the session context with sample data
        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        mock_session.query.return_value.filter.return_value.yield_per.return_value = [
            ("https://arxiv.org/abs/1234",),
            ("https://nature.com/paper1",),
        ]
        mock_session_context.return_value = mock_session

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.get_unique_domains()

            assert isinstance(result, list)
            assert "arxiv.org" in result
            assert "nature.com" in result


class TestLibraryServiceGetAllCollections:
    """Tests for getting all collections."""

    def test_get_all_collections_returns_list(self, mocker):
        """Returns a list of collections with document counts."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        # Create mock collection
        mock_collection = Mock()
        mock_collection.id = "coll-123"
        mock_collection.name = "Test Collection"
        mock_collection.description = "A test collection"
        mock_collection.is_default = False

        # Mock the session context
        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        # Mock query chain
        mock_query = Mock()
        mock_query.outerjoin.return_value = mock_query
        mock_query.group_by.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.all.return_value = [(mock_collection, 5, 3)]
        mock_session.query.return_value = mock_query
        mock_session_context.return_value = mock_session

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.get_all_collections()

            assert isinstance(result, list)
            assert len(result) == 1
            assert result[0]["id"] == "coll-123"
            assert result[0]["name"] == "Test Collection"
            assert result[0]["document_count"] == 5


class TestLibraryServiceGetDocumentById:
    """Tests for getting document by ID."""

    def test_get_document_by_id_not_found(self, mocker):
        """Returns None when document not found."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        # Mock the session context
        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        # Mock query to return None
        mock_query = Mock()
        mock_query.outerjoin.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.first.return_value = None
        mock_session.query.return_value = mock_query
        mock_session_context.return_value = mock_session

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.get_document_by_id("nonexistent-doc")
            assert result is None


class TestLibraryServiceGetLibraryStats:
    """Tests for get_library_stats method."""

    def test_get_library_stats_returns_dict(self, mocker):
        """Returns dictionary with library statistics."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        # Mock query counts
        mock_session.query.return_value.count.return_value = 10
        mock_session.query.return_value.filter.return_value.count.return_value = 5
        mock_session_context.return_value = mock_session

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.get_library_stats()

            assert isinstance(result, dict)


class TestLibraryServiceGetDocuments:
    """Tests for get_documents method."""

    def test_get_documents_returns_list(self, mocker):
        """Returns list of documents."""
        from contextlib import contextmanager

        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        # Create a proper mock session
        mock_session = MagicMock()

        # Mock query chain - need to support chained calls
        mock_query = MagicMock()
        mock_query.outerjoin.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.offset.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []
        mock_query.count.return_value = 0
        mock_session.query.return_value = mock_query

        # Create a context manager that yields our mock session
        @contextmanager
        def mock_get_session(username, password=None):
            yield mock_session

        # Patch at the module level where it's imported
        mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session",
            side_effect=mock_get_session,
        )

        # Mock get_default_library_id since get_documents() calls it first
        # It's imported inside the function, so patch at the source module
        mocker.patch(
            "local_deep_research.database.library_init.get_default_library_id",
            return_value="default-library-id",
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.get_documents()

            # get_documents() returns List[Dict] directly, not {"documents": [...]}
            assert isinstance(result, list)


class TestLibraryServiceApplyDomainFilter:
    """Tests for _apply_domain_filter method."""

    def test_apply_domain_filter_arxiv(self, mocker):
        """Applies arxiv domain filter correctly."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        mock_query = Mock()
        mock_query.filter.return_value = mock_query

        # Create a proper mock model class with the required attribute
        mock_model = Mock()
        mock_model.original_url = Mock()
        mock_model.original_url.ilike = Mock(return_value="filter_condition")

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            service._apply_domain_filter(mock_query, mock_model, "arxiv.org")

            # Should have called filter
            assert mock_query.filter.called


class TestLibraryServiceApplySearchFilter:
    """Tests for _apply_search_filter method."""

    def test_apply_search_filter_query(self, mocker):
        """Applies search query filter correctly."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        mock_query = Mock()
        mock_query.filter.return_value = mock_query

        # Create a proper mock model class with required attributes
        # Use Mock() for return values since SQLAlchemy's or_() will receive them
        mock_model = Mock()
        mock_model.title = Mock()
        mock_model.title.ilike = Mock(
            return_value=Mock()
        )  # Return Mock, not string
        mock_model.authors = Mock()
        mock_model.authors.ilike = Mock(return_value=Mock())
        mock_model.doi = Mock()
        mock_model.doi.ilike = Mock(return_value=Mock())

        # Mock the or_ function to avoid SQLAlchemy validation
        mocker.patch(
            "local_deep_research.research_library.services.library_service.or_",
            return_value=Mock(),
        )

        # Also mock ResearchResource.title.ilike since _apply_search_filter uses it
        mock_resource = Mock()
        mock_resource.title = Mock()
        mock_resource.title.ilike = Mock(return_value=Mock())
        mocker.patch(
            "local_deep_research.research_library.services.library_service.ResearchResource",
            mock_resource,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            service._apply_search_filter(mock_query, mock_model, "test search")

            assert mock_query.filter.called


class TestLibraryServiceGetResearchListWithStats:
    """Tests for get_research_list_with_stats method."""

    def test_get_research_list_with_stats_returns_list(self, mocker):
        """Returns list of research with stats."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        # Mock query
        mock_query = Mock()
        mock_query.outerjoin.return_value = mock_query
        mock_query.group_by.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.all.return_value = []
        mock_session.query.return_value = mock_query
        mock_session_context.return_value = mock_session

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.get_research_list_with_stats()

            assert isinstance(result, list)


class TestGetResearchListRatingsPreload:
    """Tests for batch-preloaded ratings in get_research_list_with_stats (N+1 fix)."""

    def test_ratings_fetched_in_single_batch_query(self, mocker):
        """Ratings should be preloaded via .in_() query, not per-research."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)

        # Create mock research results (tuples of research, total, downloaded, downloadable)
        mock_research1 = Mock()
        mock_research1.id = 1
        mock_research2 = Mock()
        mock_research2.id = 2

        # Set up main query to return two research results
        mock_main_query = Mock()
        mock_main_query.outerjoin.return_value = mock_main_query
        mock_main_query.group_by.return_value = mock_main_query
        mock_main_query.order_by.return_value = mock_main_query
        mock_main_query.all.return_value = [
            (mock_research1, 5, 3, 5),
            (mock_research2, 10, 8, 10),
        ]

        # Set up ratings query (preloaded batch)
        mock_rating = Mock()
        mock_rating.research_id = 1
        mock_rating.rating = 4
        mock_rating.notes = "Good"

        mock_ratings_query = Mock()
        mock_ratings_query.filter.return_value = mock_ratings_query
        mock_ratings_query.all.return_value = [mock_rating]

        # Set up domain query chain (used inside loop for domain breakdown)
        mock_domain_query = Mock()
        mock_domain_query.filter.return_value = mock_domain_query
        mock_domain_query.group_by.return_value = mock_domain_query
        mock_domain_query.limit.return_value = mock_domain_query
        mock_domain_query.all.return_value = []

        call_count = {"n": 0}

        def query_side_effect(*args):
            call_count["n"] += 1
            if call_count["n"] == 1:
                return mock_main_query  # Main research query
            if call_count["n"] == 2:
                return mock_ratings_query  # Batch ratings query
            return mock_domain_query  # Domain breakdown queries

        mock_session.query.side_effect = query_side_effect
        mock_session_context.return_value = mock_session

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.get_research_list_with_stats()

        assert isinstance(result, list)
        assert len(result) == 2
        # The ratings query (call #2) should use .filter() with .in_(),
        # not be called per-research inside the loop
        mock_ratings_query.filter.assert_called_once()


class TestLibraryServiceOpenFileLocation:
    """Tests for open_file_location method."""

    def test_open_file_location_document_not_found(self, mocker):
        """Returns False when document not found."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.get.return_value = None
        mock_session_context.return_value = mock_session

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.open_file_location("nonexistent-doc")

            assert result is False


class TestLibraryServiceSyncLibrary:
    """Tests for sync_library_with_filesystem method."""

    def test_sync_library_returns_dict(self, mocker):
        """Returns dictionary with sync results."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        mock_session_context = mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session"
        )
        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_session.query.return_value.all.return_value = []
        mock_session_context.return_value = mock_session

        # Mock path operations
        mocker.patch("pathlib.Path.exists", return_value=True)
        mocker.patch("pathlib.Path.glob", return_value=[])

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.sync_library_with_filesystem()

            assert isinstance(result, dict)


class TestLibraryServiceMarkForRedownload:
    """Tests for mark_for_redownload method."""

    def test_mark_for_redownload_returns_count(self, mocker):
        """Returns count of marked documents."""
        from contextlib import contextmanager

        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        # Create mock document with real string values
        mock_doc = Mock()
        mock_doc.original_url = (
            "https://example.com/doc.pdf"  # Real string for _get_url_hash
        )
        mock_doc.status = "completed"
        mock_doc.id = "doc-123"

        # Create mock tracker
        mock_tracker = Mock()
        mock_tracker.is_downloaded = True
        mock_tracker.file_path = "/path/to/file.pdf"

        # Create mock session
        mock_session = MagicMock()

        # Mock the query().get() chain for Document lookup
        mock_doc_query = MagicMock()
        mock_doc_query.get.return_value = mock_doc

        # Mock the filter_by().first() chain for DownloadTracker lookup
        mock_tracker_query = MagicMock()
        mock_tracker_filter = MagicMock()
        mock_tracker_filter.first.return_value = mock_tracker
        mock_tracker_query.filter_by.return_value = mock_tracker_filter

        # Configure query() to return different mocks based on model
        def query_side_effect(model):
            # Check model name since we can't import the actual models easily
            model_name = getattr(model, "__name__", str(model))
            if "Document" in str(model_name) or "Document" in str(model):
                return mock_doc_query
            if "DownloadTracker" in str(model_name) or "Tracker" in str(model):
                return mock_tracker_query
            return MagicMock()

        mock_session.query.side_effect = query_side_effect

        # Create a context manager that yields our mock session
        @contextmanager
        def mock_get_session(username, password=None):
            yield mock_session

        mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session",
            side_effect=mock_get_session,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service.mark_for_redownload(["doc-123"])

            assert isinstance(result, int)
            assert result == 1  # One document was marked


class TestLibraryServiceHasBlobInDb:
    """Tests for _has_blob_in_db method."""

    def test_has_blob_in_db_true(self, mocker):
        """Returns True when blob exists."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = Mock()

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service._has_blob_in_db(mock_session, "doc-123")

            assert result is True

    def test_has_blob_in_db_false(self, mocker):
        """Returns False when blob does not exist."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service._has_blob_in_db(mock_session, "doc-123")

            assert result is False


class TestLibraryServiceGetStoragePath:
    """Tests for _get_storage_path method."""

    def test_get_storage_path_returns_string(self, mocker):
        """Returns string path."""
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        # Mock the settings manager at the correct import location
        mock_settings_manager = Mock()
        mock_settings_manager.get_setting.return_value = "/test/storage/path"

        mocker.patch(
            "local_deep_research.utilities.db_utils.get_settings_manager",
            return_value=mock_settings_manager,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            result = service._get_storage_path()
            assert isinstance(result, str)


# ============== Integration tests against a real session ==============
# These use library_session (in-memory SQLite) to exercise behavior that
# mocks can't reproduce — specifically, a real Document with
# original_url=None, which mocks would silently coerce into a string.


class TestSyncLibraryUploadsIntegration:
    """Regression for #3869: uploads must not crash sync nor be deleted by it."""

    def test_sync_skips_uploads_and_does_not_delete_them(
        self,
        library_session,
        mock_source_type,
        mock_upload_source_type,
        mocker,
    ):
        """Upload (original_url=None) survives sync; download with no tracker is processed."""
        import hashlib
        import uuid
        from contextlib import contextmanager

        from local_deep_research.database.models.library import (
            Document,
            DocumentStatus,
        )
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        upload_id = str(uuid.uuid4())
        upload = Document(
            id=upload_id,
            source_type_id=mock_upload_source_type.id,
            document_hash=hashlib.sha256(b"upload").hexdigest(),
            original_url=None,
            file_size=1024,
            file_type="pdf",
            title="User Upload",
            status=DocumentStatus.COMPLETED,
        )
        download_id = str(uuid.uuid4())
        download = Document(
            id=download_id,
            source_type_id=mock_source_type.id,
            document_hash=hashlib.sha256(b"download").hexdigest(),
            original_url="https://example.com/paper.pdf",
            file_size=2048,
            file_type="pdf",
            title="Research Download",
            status=DocumentStatus.COMPLETED,
        )
        library_session.add_all([upload, download])
        library_session.commit()

        @contextmanager
        def _session_cm(*_args, **_kwargs):
            yield library_session

        mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session",
            side_effect=_session_cm,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            stats = service.sync_library_with_filesystem()

        # Upload was excluded from the sync (regression guard for both the
        # TypeError crash and the silent cascade-delete in the else branch).
        assert library_session.query(Document).get(upload_id) is not None
        assert stats["total_documents"] == 1  # only the download is counted

        # The download has no DownloadTracker, so it must still travel through
        # the destructive `else` branch and be cascade-deleted. Asserting this
        # explicitly proves the deletion path stays functional for legitimate
        # downloads — without it, a regression that broke the else branch
        # would still pass the upload-survival check above.
        assert library_session.query(Document).get(download_id) is None
        assert stats["files_missing"] == 1


class TestMarkForRedownloadUploadsIntegration:
    """Regression for #3869: mark_for_redownload must not crash on uploads."""

    def test_mark_for_redownload_skips_upload_documents(
        self,
        library_session,
        mock_upload_source_type,
        mocker,
        caplog,
    ):
        """Calling mark_for_redownload with an upload's id is a no-op, not a crash."""
        import hashlib
        import uuid
        from contextlib import contextmanager

        from local_deep_research.database.models.library import (
            Document,
            DocumentStatus,
        )
        from local_deep_research.research_library.services.library_service import (
            LibraryService,
        )

        upload_id = str(uuid.uuid4())
        upload = Document(
            id=upload_id,
            source_type_id=mock_upload_source_type.id,
            document_hash=hashlib.sha256(b"upload-mfr").hexdigest(),
            original_url=None,
            file_size=512,
            file_type="pdf",
            title="User Upload",
            status=DocumentStatus.COMPLETED,
        )
        library_session.add(upload)
        library_session.commit()

        @contextmanager
        def _session_cm(*_args, **_kwargs):
            yield library_session

        mocker.patch(
            "local_deep_research.research_library.services.library_service.get_user_db_session",
            side_effect=_session_cm,
        )

        with patch.object(
            LibraryService, "__init__", lambda self, username: None
        ):
            service = LibraryService.__new__(LibraryService)
            service.username = "test_user"

            count = service.mark_for_redownload([upload_id])

        assert count == 0
        # Upload's status must be unchanged.
        refreshed = library_session.query(Document).get(upload_id)
        assert refreshed.status == DocumentStatus.COMPLETED
