"""
Coverage tests for LibraryService — targeting the 29 missing statements.

Focuses narrowly on uncovered branches:
- _get_safe_absolute_path: empty/sentinel paths, None abs_path return
- get_document_by_id: downloaded_at branches (processed_at with/without isoformat,
  uploaded_at fallback, None), research_created_at (str vs datetime vs None),
  original_url from resource fallback, no-resource domain fallback
- get_documents: downloaded_at with processed_at, uploaded_at fallback,
  _sort_date with uploaded_at, collection_id provided (skips get_default_library_id)
- open_file_location: library_root is None, ValueError from validate_safe_path
"""

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

MODULE = "local_deep_research.research_library.services.library_service"


# ============== Helper ==============


def _make_service():
    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"
    return service


def _mock_session_cm(mocker, mock_session):
    """Patch get_user_db_session as a proper context manager."""

    @contextmanager
    def _cm(username, password=None):
        yield mock_session

    mocker.patch(
        f"{MODULE}.get_user_db_session",
        side_effect=_cm,
    )


def _setup_get_document_by_id(
    mocker,
    mock_session,
    doc_overrides=None,
    research_overrides=None,
    resource=None,
):
    """
    Configure mock_session for get_document_by_id.
    Returns (service, mock_doc, mock_resource, mock_research).
    """
    service = _make_service()

    mock_doc = Mock()
    mock_doc.id = "doc-123"
    mock_doc.resource_id = "res-1"
    mock_doc.research_id = "research-1"
    mock_doc.title = "Test Paper"
    mock_doc.original_url = "https://arxiv.org/abs/2301.00001"
    mock_doc.file_path = "pdfs/test.pdf"
    mock_doc.filename = "test.pdf"
    mock_doc.file_size = 1024
    mock_doc.file_type = "pdf"
    mock_doc.mime_type = "application/pdf"
    mock_doc.text_content = None
    mock_doc.status = "completed"
    mock_doc.processed_at = None
    mock_doc.favorite = False
    mock_doc.tags = []
    mock_doc.storage_mode = "filesystem"

    if doc_overrides:
        for k, v in doc_overrides.items():
            setattr(mock_doc, k, v)

    if resource is None:
        mock_resource = Mock()
        mock_resource.title = "Resource Title"
        mock_resource.url = "https://arxiv.org/abs/2301.00001"
    else:
        mock_resource = resource

    mock_research = Mock()
    mock_research.query = "quantum computing"
    mock_research.created_at = "2024-01-01T00:00:00"
    if research_overrides:
        for k, v in research_overrides.items():
            setattr(mock_research, k, v)

    mock_main_query = MagicMock()
    mock_main_query.outerjoin.return_value = mock_main_query
    mock_main_query.filter.return_value = mock_main_query
    mock_main_query.first.return_value = (
        mock_doc,
        mock_resource,
        mock_research,
    )

    mock_dc = Mock()
    mock_dc.indexed = False
    mock_dc.chunk_count = 0
    mock_coll = Mock()
    mock_coll.id = "coll-1"
    mock_coll.name = "Library"

    mock_coll_query = MagicMock()
    mock_coll_query.join.return_value = mock_coll_query
    mock_coll_query.filter.return_value = mock_coll_query
    mock_coll_query.all.return_value = [(mock_dc, mock_coll)]

    call_count = {"n": 0}

    def query_side_effect(*args):
        call_count["n"] += 1
        return mock_main_query if call_count["n"] == 1 else mock_coll_query

    mock_session.query.side_effect = query_side_effect
    _mock_session_cm(mocker, mock_session)

    mocker.patch(
        f"{MODULE}.get_absolute_path_from_settings",
        return_value=None,
    )

    return service, mock_doc, mock_resource, mock_research


# ============== _get_safe_absolute_path ==============


class TestGetSafeAbsolutePath:
    """Tests for the _get_safe_absolute_path helper."""

    def test_empty_file_path_returns_none(self):
        """Empty string returns None without calling get_absolute_path_from_settings."""
        service = _make_service()
        result = service._get_safe_absolute_path("")
        assert result is None

    def test_sentinel_path_returns_none(self):
        """A FILE_PATH_SENTINELS value returns None."""
        from local_deep_research.constants import FILE_PATH_METADATA_ONLY

        service = _make_service()
        result = service._get_safe_absolute_path(FILE_PATH_METADATA_ONLY)
        assert result is None

    def test_valid_path_with_none_abs_path_returns_none(self, mocker):
        """When get_absolute_path_from_settings returns None, result is None."""
        service = _make_service()
        mocker.patch(
            f"{MODULE}.get_absolute_path_from_settings",
            return_value=None,
        )
        result = service._get_safe_absolute_path("some/relative/path.pdf")
        assert result is None

    def test_valid_path_with_abs_path_returns_string(self, mocker):
        """When get_absolute_path_from_settings returns a path, result is str."""
        service = _make_service()
        from pathlib import Path

        fake_path = Path("/library/some/relative/path.pdf")
        mocker.patch(
            f"{MODULE}.get_absolute_path_from_settings",
            return_value=fake_path,
        )
        result = service._get_safe_absolute_path("some/relative/path.pdf")
        assert result == str(fake_path)
        assert isinstance(result, str)


# ============== get_document_by_id downloaded_at branches ==============


class TestGetDocumentByIdDownloadedAt:
    """Uncovered branches in the downloaded_at ternary expression."""

    def test_processed_at_without_isoformat_uses_str(self, mocker):
        """processed_at exists but has no isoformat → str(processed_at) used."""
        mock_session = MagicMock()
        # Use a plain string as processed_at — strings have no .isoformat(), so
        # the ternary falls to str(processed_at).
        fake_processed_at = "2024-01-15"
        service, mock_doc, _, _ = _setup_get_document_by_id(
            mocker,
            mock_session,
            doc_overrides={"processed_at": fake_processed_at},
        )

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

        assert result is not None
        # str("2024-01-15") == "2024-01-15"
        assert result["downloaded_at"] == str(fake_processed_at)

    def test_no_processed_at_with_uploaded_at_uses_isoformat(self, mocker):
        """processed_at is None, uploaded_at has isoformat → uploaded_at.isoformat() used."""
        mock_session = MagicMock()
        mock_uploaded_at = Mock()
        mock_uploaded_at.isoformat.return_value = "2024-02-20T10:00:00"

        service, mock_doc, _, _ = _setup_get_document_by_id(
            mocker,
            mock_session,
            doc_overrides={
                "processed_at": None,
                "uploaded_at": mock_uploaded_at,
            },
        )

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

        assert result is not None
        assert result["downloaded_at"] == "2024-02-20T10:00:00"

    def test_no_processed_at_no_uploaded_at_returns_none(self, mocker):
        """processed_at is None and no uploaded_at → downloaded_at is None."""
        mock_session = MagicMock()

        # Use a real Mock but delete uploaded_at attribute
        class DocWithoutUploadedAt:
            pass

        service, mock_doc, _, _ = _setup_get_document_by_id(
            mocker,
            mock_session,
            doc_overrides={"processed_at": None},
        )
        # Ensure hasattr(doc, 'uploaded_at') returns False
        del mock_doc.uploaded_at

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

        assert result is not None
        assert result["downloaded_at"] is None


# ============== get_document_by_id research_created_at branches ==============


class TestGetDocumentByIdResearchCreatedAt:
    """Uncovered branches for research_created_at ternary."""

    def test_research_created_at_as_datetime_uses_isoformat(self, mocker):
        """research.created_at is a datetime object → .isoformat() called."""
        mock_session = MagicMock()
        mock_created_at = Mock()
        mock_created_at.isoformat.return_value = "2024-03-01T12:00:00"
        # isinstance(research.created_at, str) == False → goes to elif branch

        service, _, _, mock_research = _setup_get_document_by_id(
            mocker,
            mock_session,
            research_overrides={"created_at": mock_created_at},
        )
        # Make isinstance check return False (created_at is not str)
        mock_research.created_at = mock_created_at

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

        assert result is not None
        assert result["research_created_at"] == "2024-03-01T12:00:00"

    def test_no_research_created_at_returns_none(self, mocker):
        """research is None → research_created_at is None."""
        service = _make_service()
        mock_session = MagicMock()

        mock_doc = Mock()
        mock_doc.id = "doc-123"
        mock_doc.resource_id = None
        mock_doc.research_id = None
        mock_doc.title = "Upload"
        mock_doc.original_url = None
        mock_doc.file_path = None
        mock_doc.filename = "upload.pdf"
        mock_doc.file_size = 512
        mock_doc.file_type = "pdf"
        mock_doc.mime_type = "application/pdf"
        mock_doc.text_content = None
        mock_doc.status = "completed"
        mock_doc.processed_at = None
        mock_doc.favorite = False
        mock_doc.tags = []
        mock_doc.storage_mode = "filesystem"

        mock_main_query = MagicMock()
        mock_main_query.outerjoin.return_value = mock_main_query
        mock_main_query.filter.return_value = mock_main_query
        # research is None (user upload)
        mock_main_query.first.return_value = (mock_doc, None, None)

        mock_coll_query = MagicMock()
        mock_coll_query.join.return_value = mock_coll_query
        mock_coll_query.filter.return_value = mock_coll_query
        mock_coll_query.all.return_value = []

        call_count = {"n": 0}

        def query_side_effect(*args):
            call_count["n"] += 1
            return mock_main_query if call_count["n"] == 1 else mock_coll_query

        mock_session.query.side_effect = query_side_effect
        _mock_session_cm(mocker, mock_session)

        mocker.patch(
            f"{MODULE}.get_absolute_path_from_settings", return_value=None
        )

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

        assert result is not None
        assert result["research_created_at"] is None
        assert result["research_title"] == "User Upload"
        assert result["domain"] == "User Upload"


# ============== get_document_by_id original_url fallback ==============


class TestGetDocumentByIdOriginalUrlFallback:
    """original_url falls back to resource.url when doc.original_url is None."""

    def test_doc_original_url_none_uses_resource_url(self, mocker):
        """doc.original_url=None → original_url comes from resource.url."""
        mock_session = MagicMock()
        service, mock_doc, mock_resource, _ = _setup_get_document_by_id(
            mocker,
            mock_session,
            doc_overrides={"original_url": None},
        )
        mock_resource.url = "https://example.com/paper.pdf"

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

        assert result is not None
        assert result["original_url"] == "https://example.com/paper.pdf"


# ============== open_file_location uncovered branches ==============


class TestOpenFileLocationMissingBranches:
    """Branches in open_file_location not covered by existing tests."""

    def test_library_root_none_returns_false(self, mocker):
        """get_absolute_path_from_settings returns None for library root → False."""
        service = _make_service()
        mock_session = MagicMock()

        mock_doc = Mock()
        mock_doc.original_url = "https://example.com/doc.pdf"

        mock_tracker = Mock()
        mock_tracker.file_path = "pdfs/doc.pdf"

        mock_doc_query = MagicMock()
        mock_doc_query.get.return_value = mock_doc
        mock_tracker_query = MagicMock()
        mock_tracker_query.filter_by.return_value.first.return_value = (
            mock_tracker
        )

        def query_router(model):
            name = getattr(model, "__name__", str(model))
            if "DownloadTracker" in str(name) or "Tracker" in str(model):
                return mock_tracker_query
            return mock_doc_query

        mock_session.query.side_effect = query_router
        _mock_session_cm(mocker, mock_session)

        # library_root is None when get_absolute_path_from_settings("") returns None
        mocker.patch(
            f"{MODULE}.get_absolute_path_from_settings",
            return_value=None,
        )

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

        assert result is False

    def test_path_validation_raises_value_error_returns_false(self, mocker):
        """PathValidator.validate_safe_path raises ValueError → returns False."""
        service = _make_service()
        mock_session = MagicMock()

        mock_doc = Mock()
        mock_doc.original_url = "https://example.com/doc.pdf"

        mock_tracker = Mock()
        mock_tracker.file_path = "../../etc/passwd"  # traversal attempt

        mock_doc_query = MagicMock()
        mock_doc_query.get.return_value = mock_doc
        mock_tracker_query = MagicMock()
        mock_tracker_query.filter_by.return_value.first.return_value = (
            mock_tracker
        )

        def query_router(model):
            name = getattr(model, "__name__", str(model))
            if "DownloadTracker" in str(name) or "Tracker" in str(model):
                return mock_tracker_query
            return mock_doc_query

        mock_session.query.side_effect = query_router
        _mock_session_cm(mocker, mock_session)

        mocker.patch(
            f"{MODULE}.get_absolute_path_from_settings",
            return_value=MagicMock(),  # non-None library root
        )
        mocker.patch(
            f"{MODULE}.PathValidator.validate_safe_path",
            side_effect=ValueError("Path traversal detected"),
        )

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

        assert result is False


# ============== get_documents with explicit collection_id ==============


class TestGetDocumentsWithCollectionId:
    """get_documents when collection_id is explicitly provided (skips get_default_library_id)."""

    def test_explicit_collection_id_skips_default_lookup(self, mocker):
        """When collection_id is provided, get_default_library_id is NOT called."""
        service = _make_service()
        mock_session = MagicMock()

        mock_query = MagicMock()
        mock_query.join.return_value = mock_query
        mock_query.outerjoin.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []
        mock_session.query.return_value = mock_query

        _mock_session_cm(mocker, mock_session)

        mock_get_default = mocker.patch(
            "local_deep_research.database.library_init.get_default_library_id",
            return_value="default-id",
        )

        result = service.get_documents(collection_id="explicit-coll-id")

        assert isinstance(result, list)
        mock_get_default.assert_not_called()


# ============== get_documents downloaded_at / _sort_date with uploaded_at ==============


class TestGetDocumentsDateBranches:
    """Test downloaded_at and _sort_date branches in get_documents result processing."""

    def _make_mock_doc_result(self, processed_at=None, uploaded_at=None):
        """Create a (doc, resource, research, doc_collection) tuple."""
        mock_doc = Mock()
        mock_doc.id = "doc-1"
        mock_doc.resource_id = "res-1"
        mock_doc.research_id = "research-1"
        mock_doc.title = "Test"
        mock_doc.authors = "Author"
        mock_doc.published_date = None
        mock_doc.doi = None
        mock_doc.arxiv_id = None
        mock_doc.pmid = None
        mock_doc.file_path = "pdfs/test.pdf"
        mock_doc.filename = "test.pdf"
        mock_doc.file_size = 1024
        mock_doc.file_type = "pdf"
        mock_doc.original_url = "https://arxiv.org/abs/1234.5678"
        mock_doc.status = "completed"
        mock_doc.processed_at = processed_at
        mock_doc.favorite = False
        mock_doc.tags = []
        mock_doc.text_content = None
        mock_doc.storage_mode = "filesystem"

        if uploaded_at is not None:
            mock_doc.uploaded_at = uploaded_at

        mock_resource = Mock()
        mock_resource.title = "Resource"

        mock_research = Mock()
        mock_research.title = "Research"
        mock_research.query = "test query"
        mock_research.mode = "deep"
        mock_research.created_at = "2024-01-01"

        mock_doc_collection = Mock()
        mock_doc_collection.indexed = False
        mock_doc_collection.chunk_count = 0

        return (mock_doc, mock_resource, mock_research, mock_doc_collection)

    def test_downloaded_at_uses_processed_at_isoformat(self, mocker):
        """processed_at with isoformat → downloaded_at = processed_at.isoformat()."""
        service = _make_service()
        mock_session = MagicMock()

        mock_processed_at = Mock()
        mock_processed_at.isoformat.return_value = "2024-05-01T09:00:00"

        result_row = self._make_mock_doc_result(processed_at=mock_processed_at)

        mock_query = MagicMock()
        mock_query.join.return_value = mock_query
        mock_query.outerjoin.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = [result_row]
        mock_session.query.return_value = mock_query

        _mock_session_cm(mocker, mock_session)

        mocker.patch(
            "local_deep_research.database.library_init.get_default_library_id",
            return_value="default-id",
        )
        mocker.patch(
            f"{MODULE}.get_absolute_path_from_settings",
            return_value=None,
        )

        results = service.get_documents()

        assert len(results) == 1
        assert results[0]["downloaded_at"] == "2024-05-01T09:00:00"

    def test_downloaded_at_falls_back_to_uploaded_at(self, mocker):
        """No processed_at, has uploaded_at → downloaded_at = uploaded_at.isoformat()."""
        service = _make_service()
        mock_session = MagicMock()

        mock_uploaded_at = Mock()
        mock_uploaded_at.isoformat.return_value = "2024-06-15T14:30:00"

        result_row = self._make_mock_doc_result(
            processed_at=None, uploaded_at=mock_uploaded_at
        )

        mock_query = MagicMock()
        mock_query.join.return_value = mock_query
        mock_query.outerjoin.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = [result_row]
        mock_session.query.return_value = mock_query

        _mock_session_cm(mocker, mock_session)

        mocker.patch(
            "local_deep_research.database.library_init.get_default_library_id",
            return_value="default-id",
        )
        mocker.patch(
            f"{MODULE}.get_absolute_path_from_settings",
            return_value=None,
        )

        results = service.get_documents()

        assert len(results) == 1
        assert results[0]["downloaded_at"] == "2024-06-15T14:30:00"
