"""
Tests for LibraryService collections and stats methods.

Covers:
- get_all_collections: empty, None doc_count, dict structure
- get_unique_domains: None filtering, empty results
- get_research_list_with_stats: empty, with stats, ratings, missing ratings
- open_file_location: no URL, no tracker, successful open
"""

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


# ============== 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(
        "local_deep_research.research_library.services.library_service.get_user_db_session",
        side_effect=_cm,
    )


# ============== get_all_collections ==============


class TestGetAllCollectionsEdgeCases:
    """Edge cases for get_all_collections."""

    def test_empty_collections_returns_empty_list(self, mocker):
        """No collections → empty list."""
        service = _make_service()
        mock_session = MagicMock()

        mock_query = MagicMock()
        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_cm(mocker, mock_session)

        result = service.get_all_collections()

        assert result == []

    def test_none_doc_count_uses_zero_fallback(self, mocker):
        """Collection with None doc_count → uses 0 fallback."""
        service = _make_service()
        mock_session = MagicMock()

        mock_coll = Mock()
        mock_coll.id = "coll-1"
        mock_coll.name = "Empty Collection"
        mock_coll.description = "No docs"
        mock_coll.is_default = False

        mock_query = MagicMock()
        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_coll, None, None)]
        mock_session.query.return_value = mock_query
        _mock_session_cm(mocker, mock_session)

        result = service.get_all_collections()

        assert len(result) == 1
        assert result[0]["document_count"] == 0

    def test_collection_dict_has_all_expected_keys(self, mocker):
        """Each collection dict has: id, name, description, is_default, document_count, indexed_document_count."""
        service = _make_service()
        mock_session = MagicMock()

        mock_coll = Mock()
        mock_coll.id = "coll-1"
        mock_coll.name = "Test"
        mock_coll.description = "Desc"
        mock_coll.is_default = True

        mock_query = MagicMock()
        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_coll, 7, 3)]
        mock_session.query.return_value = mock_query
        _mock_session_cm(mocker, mock_session)

        result = service.get_all_collections()
        entry = result[0]

        assert set(entry.keys()) == {
            "id",
            "name",
            "description",
            "is_default",
            "document_count",
            "indexed_document_count",
        }
        assert entry["id"] == "coll-1"
        assert entry["name"] == "Test"
        assert entry["is_default"] is True
        assert entry["document_count"] == 7
        assert entry["indexed_document_count"] == 3

    def test_multiple_collections_all_present(self, mocker):
        """Multiple collections all appear in result."""
        service = _make_service()
        mock_session = MagicMock()

        colls = []
        for i in range(3):
            c = Mock()
            c.id = f"coll-{i}"
            c.name = f"Collection {i}"
            c.description = f"Desc {i}"
            c.is_default = i == 0
            colls.append((c, i * 5, i * 2))

        mock_query = MagicMock()
        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 = colls
        mock_session.query.return_value = mock_query
        _mock_session_cm(mocker, mock_session)

        result = service.get_all_collections()

        assert len(result) == 3
        ids = [r["id"] for r in result]
        assert "coll-0" in ids
        assert "coll-1" in ids
        assert "coll-2" in ids


# ============== get_unique_domains ==============


class TestGetUniqueDomainsEdgeCases:
    """Edge cases for get_unique_domains."""

    def test_none_values_filtered_out(self, mocker):
        """None in domain results is filtered by 'if d[0]' guard."""
        service = _make_service()
        mock_session = MagicMock()

        mock_session.query.return_value.filter.return_value.all.return_value = [
            (None,),
            ("arxiv.org",),
        ]
        _mock_session_cm(mocker, mock_session)

        result = service.get_unique_domains()

        assert result == ["arxiv.org"]

    def test_empty_query_results_returns_empty_list(self, mocker):
        """No documents → empty domain list."""
        service = _make_service()
        mock_session = MagicMock()

        mock_session.query.return_value.filter.return_value.all.return_value = []
        _mock_session_cm(mocker, mock_session)

        result = service.get_unique_domains()

        assert result == []

    def test_all_domains_returned(self, mocker):
        """Multiple domains all present."""
        service = _make_service()
        mock_session = MagicMock()

        mock_session.query.return_value.filter.return_value.all.return_value = [
            ("arxiv.org",),
            ("pubmed",),
            ("other",),
        ]
        _mock_session_cm(mocker, mock_session)

        result = service.get_unique_domains()

        assert set(result) == {"arxiv.org", "pubmed", "other"}


# ============== get_research_list_with_stats ==============


class TestGetResearchListWithStats:
    """Tests for get_research_list_with_stats."""

    def test_empty_research_list(self, mocker):
        """No research sessions → empty list, no ratings query."""
        service = _make_service()
        mock_session = MagicMock()

        mock_query = MagicMock()
        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_cm(mocker, mock_session)

        result = service.get_research_list_with_stats()

        assert result == []

    def test_research_with_correct_stat_values(self, mocker):
        """Research entry includes correct total/downloaded/downloadable counts."""
        service = _make_service()
        mock_session = MagicMock()

        mock_research = Mock()
        mock_research.id = 1
        mock_research.title = "Test Research"
        mock_research.query = "quantum"
        mock_research.mode = "deep"
        mock_research.status = "completed"
        mock_research.created_at = "2024-01-01"
        mock_research.duration_seconds = 120

        # Main query
        mock_main_query = MagicMock()
        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_research, 10, 8, 5)]

        # Ratings query
        mock_ratings_query = MagicMock()
        mock_ratings_query.filter.return_value = mock_ratings_query
        mock_ratings_query.all.return_value = []

        # Domain query
        mock_domain_query = MagicMock()
        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 = [(1, "arxiv.org", 3)]

        call_count = {"n": 0}

        def query_side_effect(*args):
            call_count["n"] += 1
            if call_count["n"] == 1:
                return mock_main_query
            if call_count["n"] == 2:
                return mock_ratings_query
            return mock_domain_query

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

        result = service.get_research_list_with_stats()

        assert len(result) == 1
        entry = result[0]
        assert entry["total_resources"] == 10
        assert entry["downloaded_count"] == 8
        assert entry["downloadable_count"] == 5

    def test_rating_from_preloaded_dict(self, mocker):
        """Rating value correctly looked up from preloaded ratings dict."""
        service = _make_service()
        mock_session = MagicMock()

        mock_research = Mock()
        mock_research.id = 42
        mock_research.title = "Rated Research"
        mock_research.query = "test"
        mock_research.mode = "quick"
        mock_research.status = "completed"
        mock_research.created_at = "2024-01-01"
        mock_research.duration_seconds = 60

        mock_rating = Mock()
        mock_rating.research_id = 42
        mock_rating.rating = 5

        mock_main_query = MagicMock()
        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_research, 3, 2, 1)]

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

        mock_domain_query = MagicMock()
        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
            if call_count["n"] == 2:
                return mock_ratings_query
            return mock_domain_query

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

        result = service.get_research_list_with_stats()

        assert result[0]["rating"] == 5

    def test_missing_rating_returns_none(self, mocker):
        """Research with no rating entry → rating is None."""
        service = _make_service()
        mock_session = MagicMock()

        mock_research = Mock()
        mock_research.id = 99
        mock_research.title = "Unrated"
        mock_research.query = "test"
        mock_research.mode = "quick"
        mock_research.status = "completed"
        mock_research.created_at = "2024-01-01"
        mock_research.duration_seconds = 30

        mock_main_query = MagicMock()
        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_research, 1, 0, 0)]

        mock_ratings_query = MagicMock()
        mock_ratings_query.filter.return_value = mock_ratings_query
        mock_ratings_query.all.return_value = []

        mock_domain_query = MagicMock()
        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
            if call_count["n"] == 2:
                return mock_ratings_query
            return mock_domain_query

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

        result = service.get_research_list_with_stats()

        assert result[0]["rating"] is None

    def test_top_domains_present(self, mocker):
        """Domain breakdown appears as top_domains list."""
        service = _make_service()
        mock_session = MagicMock()

        mock_research = Mock()
        mock_research.id = 1
        mock_research.title = "Test"
        mock_research.query = "test"
        mock_research.mode = "quick"
        mock_research.status = "completed"
        mock_research.created_at = "2024-01-01"
        mock_research.duration_seconds = 30

        mock_main_query = MagicMock()
        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_research, 5, 3, 2)]

        mock_ratings_query = MagicMock()
        mock_ratings_query.filter.return_value = mock_ratings_query
        mock_ratings_query.all.return_value = []

        mock_domain_query = MagicMock()
        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 = [
            (1, "arxiv.org", 3),
            (1, "pubmed", 2),
        ]

        call_count = {"n": 0}

        def query_side_effect(*args):
            call_count["n"] += 1
            if call_count["n"] == 1:
                return mock_main_query
            if call_count["n"] == 2:
                return mock_ratings_query
            return mock_domain_query

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

        result = service.get_research_list_with_stats()

        assert "top_domains" in result[0]
        assert len(result[0]["top_domains"]) == 2


# ============== open_file_location ==============


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

    def test_doc_has_no_original_url_returns_false(self, mocker):
        """Doc with original_url=None → returns False (no tracker lookup)."""
        service = _make_service()
        mock_session = MagicMock()

        mock_doc = Mock()
        mock_doc.original_url = None

        mock_session.query.return_value.get.return_value = mock_doc
        _mock_session_cm(mocker, mock_session)

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

        assert result is False

    def test_tracker_not_found_returns_false(self, mocker):
        """Doc exists but no tracker → returns False."""
        service = _make_service()
        mock_session = MagicMock()

        mock_doc = Mock()
        mock_doc.original_url = "https://example.com/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 = None

        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)

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

        assert result is False

    def test_successful_open_calls_utility(self, mocker):
        """Valid path that exists → calls open_file_location utility, returns True."""
        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)

        mock_validated_path = MagicMock()
        mock_validated_path.is_file.return_value = True

        mocker.patch(
            "local_deep_research.research_library.services.library_service.get_absolute_path_from_settings",
            return_value=MagicMock(),
        )
        mocker.patch(
            "local_deep_research.research_library.services.library_service.PathValidator.validate_safe_path",
            return_value=mock_validated_path,
        )
        mock_open_fn = mocker.patch(
            "local_deep_research.research_library.services.library_service.open_file_location",
            return_value=True,
        )

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

        assert result is True
        mock_open_fn.assert_called_once()
