"""
Tests for web/services/research_sources_service.py

Tests cover:
- ResearchSourcesService.save_research_sources()
- ResearchSourcesService.get_research_sources()
- ResearchSourcesService.update_research_with_sources()
"""

from unittest.mock import Mock, patch, MagicMock


class TestSaveResearchSources:
    """Tests for save_research_sources method."""

    def test_save_research_sources_empty_list(self):
        """Test saving empty list returns 0."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        result = ResearchSourcesService.save_research_sources(
            "test-id", [], username="testuser"
        )

        assert result == 0

    def test_save_research_sources_success(self):
        """Save 2 non-academic web sources → 2 ResearchResource adds.

        These URLs (``example.com``) don't match any academic engine
        pattern, so ``detect_engine`` returns None and the Paper /
        PaperAppearance write path short-circuits. The 1:1 add-count
        below is correct only for non-academic sources; academic
        sources go through three add() calls each (ResearchResource +
        Paper + PaperAppearance). Real academic dedup behavior is
        covered by tests/database/test_paper_dedup_integration.py.
        """
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        sources = [
            {
                "url": "https://example.com/1",
                "title": "Source 1",
                "snippet": "Test",
            },
            {"url": "https://example.com/2", "title": "Source 2"},
        ]

        with patch(
            "local_deep_research.web.services.research_sources_service.get_user_db_session"
        ) as mock_get_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_by.return_value.count.return_value = 0
            mock_get_session.return_value = mock_session

            result = ResearchSourcesService.save_research_sources(
                "test-id", sources, username="testuser"
            )

            assert result == 2
            assert mock_session.add.call_count == 2
            mock_session.commit.assert_called_once()

    def test_save_research_sources_skips_existing(self):
        """Test skipping save when sources already exist."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        sources = [{"url": "https://example.com", "title": "Test"}]

        with patch(
            "local_deep_research.web.services.research_sources_service.get_user_db_session"
        ) as mock_get_session:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            # Return that 5 resources already exist
            mock_session.query.return_value.filter_by.return_value.count.return_value = 5
            mock_get_session.return_value = mock_session

            result = ResearchSourcesService.save_research_sources(
                "test-id", sources, username="testuser"
            )

            assert result == 5
            mock_session.add.assert_not_called()

    def test_save_research_sources_skips_no_url(self):
        """Test skipping sources without URL."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        sources = [
            {"title": "No URL source"},  # No url field
            {"url": "", "title": "Empty URL"},  # Empty url
            {"url": "https://valid.com", "title": "Valid"},
        ]

        with patch(
            "local_deep_research.web.services.research_sources_service.get_user_db_session"
        ) as mock_get_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_by.return_value.count.return_value = 0
            mock_get_session.return_value = mock_session

            result = ResearchSourcesService.save_research_sources(
                "test-id", sources, username="testuser"
            )

            # Only valid URL should be saved
            assert result == 1

    def test_save_research_sources_uses_link_fallback(self):
        """Test using 'link' as fallback for 'url'."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        sources = [{"link": "https://example.com", "title": "Test"}]

        with patch(
            "local_deep_research.web.services.research_sources_service.get_user_db_session"
        ) as mock_get_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_by.return_value.count.return_value = 0
            mock_get_session.return_value = mock_session

            result = ResearchSourcesService.save_research_sources(
                "test-id", sources, username="testuser"
            )

            assert result == 1

    def test_save_research_sources_truncates_snippet(self):
        """Test that long snippets are truncated to 1000 chars."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        long_snippet = "x" * 2000
        sources = [
            {
                "url": "https://example.com",
                "title": "Test",
                "snippet": long_snippet,
            }
        ]

        with patch(
            "local_deep_research.web.services.research_sources_service.get_user_db_session"
        ) as mock_get_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_by.return_value.count.return_value = 0
            mock_get_session.return_value = mock_session

            ResearchSourcesService.save_research_sources(
                "test-id", sources, username="testuser"
            )

            # Verify the resource was added with truncated preview
            add_call = mock_session.add.call_args
            resource = add_call[0][0]
            assert len(resource.content_preview) == 1000

    def test_save_research_sources_handles_individual_errors(self):
        """Test that individual source errors don't stop the batch."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        sources = [
            {"url": "https://good.com", "title": "Good"},
            {"url": "https://bad.com", "title": "Bad"},  # Will cause error
            {"url": "https://good2.com", "title": "Good 2"},
        ]

        with patch(
            "local_deep_research.web.services.research_sources_service.get_user_db_session"
        ) as mock_get_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_by.return_value.count.return_value = 0
            mock_get_session.return_value = mock_session

            # The add method works for good URLs
            result = ResearchSourcesService.save_research_sources(
                "test-id", sources, username="testuser"
            )

            assert result == 3


class TestGetResearchSources:
    """Tests for get_research_sources method."""

    def test_get_research_sources_returns_list(self):
        """Test that method returns a list."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        with patch(
            "local_deep_research.web.services.research_sources_service.get_user_db_session"
        ) as mock_get_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_by.return_value.order_by.return_value.all.return_value = []
            mock_get_session.return_value = mock_session

            result = ResearchSourcesService.get_research_sources(
                "test-id", username="testuser"
            )

            assert isinstance(result, list)
            assert len(result) == 0

    def test_get_research_sources_formats_correctly(self):
        """Test that resources are formatted correctly."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        mock_resource = MagicMock()
        mock_resource.id = 1
        mock_resource.url = "https://example.com"
        mock_resource.title = "Test Title"
        mock_resource.content_preview = "Preview text"
        mock_resource.source_type = "web"
        mock_resource.resource_metadata = {"key": "value"}
        mock_resource.created_at = "2024-01-01T00:00:00"

        with patch(
            "local_deep_research.web.services.research_sources_service.get_user_db_session"
        ) as mock_get_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_by.return_value.order_by.return_value.all.return_value = [
                mock_resource
            ]
            mock_get_session.return_value = mock_session

            result = ResearchSourcesService.get_research_sources(
                "test-id", username="testuser"
            )

            assert len(result) == 1
            assert result[0]["id"] == 1
            assert result[0]["url"] == "https://example.com"
            assert result[0]["title"] == "Test Title"
            assert result[0]["snippet"] == "Preview text"
            assert result[0]["source_type"] == "web"
            assert result[0]["metadata"] == {"key": "value"}

    def test_get_research_sources_handles_none_metadata(self):
        """Test handling resources with None metadata."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        mock_resource = MagicMock()
        mock_resource.id = 1
        mock_resource.url = "https://example.com"
        mock_resource.title = "Test"
        mock_resource.content_preview = None
        mock_resource.source_type = "web"
        mock_resource.resource_metadata = None
        mock_resource.created_at = None

        with patch(
            "local_deep_research.web.services.research_sources_service.get_user_db_session"
        ) as mock_get_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_by.return_value.order_by.return_value.all.return_value = [
                mock_resource
            ]
            mock_get_session.return_value = mock_session

            result = ResearchSourcesService.get_research_sources(
                "test-id", username="testuser"
            )

            assert result[0]["metadata"] == {}


class TestUpdateResearchWithSources:
    """Tests for update_research_with_sources method."""

    def test_update_research_with_sources_success(self):
        """Test successful research update with sources."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        sources = [{"url": "https://example.com", "title": "Test"}]

        mock_research = MagicMock()
        mock_research.research_meta = {}

        with patch.object(
            ResearchSourcesService, "save_research_sources", return_value=1
        ):
            with patch(
                "local_deep_research.web.services.research_sources_service.get_user_db_session"
            ) as mock_get_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_by.return_value.first.return_value = mock_research
                mock_get_session.return_value = mock_session

                result = ResearchSourcesService.update_research_with_sources(
                    "test-id", sources, username="testuser"
                )

                assert result is True
                assert mock_research.research_meta["sources_count"] == 1
                assert mock_research.research_meta["has_sources"] is True

    def test_update_research_not_found(self):
        """Test update when research not found."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        with patch.object(
            ResearchSourcesService, "save_research_sources", return_value=0
        ):
            with patch(
                "local_deep_research.web.services.research_sources_service.get_user_db_session"
            ) as mock_get_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_by.return_value.first.return_value = None
                mock_get_session.return_value = mock_session

                result = ResearchSourcesService.update_research_with_sources(
                    "nonexistent-id", [], username="testuser"
                )

                assert result is False

    def test_update_research_initializes_none_metadata(self):
        """Test that None metadata is initialized."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        mock_research = MagicMock()
        mock_research.research_meta = None

        with patch.object(
            ResearchSourcesService, "save_research_sources", return_value=5
        ):
            with patch(
                "local_deep_research.web.services.research_sources_service.get_user_db_session"
            ) as mock_get_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_by.return_value.first.return_value = mock_research
                mock_get_session.return_value = mock_session

                result = ResearchSourcesService.update_research_with_sources(
                    "test-id", [], username="testuser"
                )

                assert result is True
                assert mock_research.research_meta == {
                    "sources_count": 5,
                    "has_sources": True,
                }

    def test_update_research_handles_exception(self):
        """Test that exceptions return False."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        with patch.object(
            ResearchSourcesService,
            "save_research_sources",
            side_effect=Exception("DB error"),
        ):
            result = ResearchSourcesService.update_research_with_sources(
                "test-id", [], username="testuser"
            )

            assert result is False


class TestMergeIdentifiersJournalColumns:
    """_merge_identifiers fills container_title only when the existing
    row has NULL — first-write-wins keeps the row stable as the same
    Paper gets re-encountered across research sessions.

    Note: there is no ``journal_quality`` column on Paper; quality is
    resolved live at render time via ``journals.quality`` (Tier 4) or
    the bundled reference DB (Tier 1-3).
    """

    def _make_paper_stub(self, **initial):
        """Minimal stand-in for a Paper ORM row.

        `_merge_identifiers` only assigns attributes — no SQLAlchemy
        machinery is needed. Using a plain object with preset fields
        makes the test assertions straightforward.
        """
        defaults = {
            "doi": None,
            "arxiv_id": None,
            "pmid": None,
            "journal_id": None,
            "container_title": None,
            "paper_metadata": None,
        }
        defaults.update(initial)

        class _Stub:
            pass

        stub = _Stub()
        for k, v in defaults.items():
            setattr(stub, k, v)
        return stub

    def test_fills_container_title_when_missing(self):
        from local_deep_research.web.services.research_sources_service import (
            _merge_identifiers,
        )

        paper = self._make_paper_stub()
        _merge_identifiers(
            paper,
            {"container_title": "Nature"},
            {},
        )
        assert paper.container_title == "Nature"

    def test_does_not_overwrite_existing_container_title(self):
        from local_deep_research.web.services.research_sources_service import (
            _merge_identifiers,
        )

        paper = self._make_paper_stub(container_title="Science")
        _merge_identifiers(
            paper,
            {"container_title": "Nature"},
            {},
        )
        assert paper.container_title == "Science"


class TestContainerTitleNotInMetadataBlob:
    """After the B.2 fix, container_title lives only in the indexed
    Paper column — not duplicated inside the paper_metadata JSON blob.
    The CSL-JSON export (used for citation rendering) already captures
    the raw value inside citation_fields["csl_json"] during
    normalize_citation, so popping the top-level key is safe.
    """

    def test_normalize_citation_still_populates_csl_container_title(self):
        """The CSL export captures container_title before any write-path
        pop — citation rendering stays intact even after B.2."""
        from local_deep_research.utilities.citation_normalizer import (
            normalize_citation,
        )

        source = {
            "title": "A Study",
            "link": "https://doi.org/10.1234/test",
            "journal": "Nature",
            "source": "openalex",
            "doi": "10.1234/test",
            "year": 2024,
        }
        fields = normalize_citation(source)
        assert fields is not None
        # normalize_citation still emits container_title at the top
        # level so the write path can pop-and-promote it to the Paper
        # column. The CSL-JSON snapshot has already captured it too.
        assert fields.get("container_title") == "Nature"
        assert fields["csl_json"].get("container-title") == "Nature"

    def test_write_path_pop_preserves_csl_json(self):
        """Simulate the write-path pop pattern and confirm csl_json still
        carries the container-title after container_title is removed
        from the top-level dict. This is the invariant B.2 relies on.
        """
        from local_deep_research.utilities.citation_normalizer import (
            normalize_citation,
        )

        source = {
            "title": "A Study",
            "link": "https://doi.org/10.1234/test",
            "journal": "Nature",
            "source": "openalex",
            "doi": "10.1234/test",
        }
        fields = normalize_citation(source)
        assert fields is not None

        # Mirror the write-path pop behaviour. container_title drops
        # out of `fields`, but csl_json (which the bibliography
        # exporter reads) still has it.
        fields.pop("container_title", None)
        assert "container_title" not in fields
        assert fields["csl_json"].get("container-title") == "Nature"


class TestResearchSourcesServiceClass:
    """Tests for ResearchSourcesService class."""

    def test_class_has_static_methods(self):
        """Test that class has required static methods."""
        from local_deep_research.web.services.research_sources_service import (
            ResearchSourcesService,
        )

        assert hasattr(ResearchSourcesService, "save_research_sources")
        assert hasattr(ResearchSourcesService, "get_research_sources")
        assert hasattr(ResearchSourcesService, "update_research_with_sources")

        assert callable(ResearchSourcesService.save_research_sources)
        assert callable(ResearchSourcesService.get_research_sources)
        assert callable(ResearchSourcesService.update_research_with_sources)
