"""Tests for Europe PMC search and DOI cross-source dedup"""

from unittest.mock import ANY, AsyncMock, MagicMock, patch

import pytest


class TestParseEuropePmcArticle:
    """_parse_europe_pmc_article converts Europe PMC JSON to standard dict format"""

    def test_basic_article(self):
        from app.services.pubmed_api import _parse_europe_pmc_article
        art = {
            "id": "12345678",
            "pmid": "12345678",
            "title": "Test Article Title",
            "abstractText": "This is a test abstract.",
            "authorString": "Smith J, Doe J",
            "journalTitle": "Test Journal",
            "journalIssn": "1234-5678",
            "pubYear": "2024",
            "firstPublicationDate": "2024-06-15",
            "doi": "10.1000/test",
            "pmcid": "PMC1234567",
            "publicationType": "Journal Article",
            "language": "eng",
            "isOpenAccess": True,
            "citedByCount": 5,
        }
        result = _parse_europe_pmc_article(art)
        assert result is not None
        assert result["pmid"] == 12345678
        assert result["title"] == "Test Article Title"
        assert result["abstract"] == "This is a test abstract."
        assert len(result["authors"]) == 2
        assert result["authors"][0]["family"] == "Smith"
        assert result["doi"] == "10.1000/test"
        assert result["pmc_id"] == "PMC1234567"
        assert result["is_oa"] is True
        assert result["journal"] == "Test Journal"
        assert result["journal_issn"] == "1234-5678"
        assert result["pub_year"] == 2024
        assert result["pub_date"] is not None
        assert result["source"] == "europe_pmc"

    def test_no_pmid(self):
        from app.services.pubmed_api import _parse_europe_pmc_article
        assert _parse_europe_pmc_article({"id": None}) is None

    def test_empty_abstract(self):
        from app.services.pubmed_api import _parse_europe_pmc_article
        art = {"id": "99999", "title": "No Abstract"}
        result = _parse_europe_pmc_article(art)
        assert result["pmid"] == 99999
        assert result["abstract"] is None

    def test_mesh_headings(self):
        from app.services.pubmed_api import _parse_europe_pmc_article
        art = {
            "id": "11111",
            "title": "With MeSH",
            "meshHeadingList": {
                "meshHeading": [
                    {"descriptorName": "Lung Neoplasms", "descriptorUI": "D008175", "majorTopic_YN": "Y",
                     "qualifierList": {"qualifier": [{"qualifierName": "diagnosis"}]}},
                    {"descriptorName": "Immunotherapy", "descriptorUI": "D007167", "majorTopic_YN": "N"},
                ]
            },
        }
        result = _parse_europe_pmc_article(art)
        assert len(result["mesh_headings"]) == 2
        assert result["mesh_headings"][0]["descriptor"] == "Lung Neoplasms"
        assert result["mesh_headings"][0]["ui"] == "D008175"
        assert result["mesh_headings"][0]["major"] is True
        assert result["mesh_headings"][0]["qualifiers"] == ["diagnosis"]
        assert result["mesh_headings"][1]["major"] is False

    def test_keywords(self):
        from app.services.pubmed_api import _parse_europe_pmc_article
        art = {
            "id": "22222",
            "title": "With Keywords",
            "keywordList": {"keyword": ["cancer", "immunotherapy", "pd-1"]},
        }
        result = _parse_europe_pmc_article(art)
        assert result["keywords"] == ["cancer", "immunotherapy", "pd-1"]

    def test_retracted_flag(self):
        from app.services.pubmed_api import _parse_europe_pmc_article
        art = {"id": "33333", "title": "Retracted", "isRetracted": True}
        result = _parse_europe_pmc_article(art)
        assert result["retracted"] is True

    def test_author_list(self):
        from app.services.pubmed_api import _parse_europe_pmc_article
        art = {
            "id": "44444",
            "title": "Author List",
            "authorList": {
                "author": [
                    {"fullName": "Smith, John", "firstName": "John", "lastName": "Smith",
                     "affiliation": ["University of Testing"]},
                    {"fullName": "Doe, Jane", "firstName": "Jane", "lastName": "Doe",
                     "affiliation": ["Research Institute"]},
                ]
            },
        }
        result = _parse_europe_pmc_article(art)
        assert len(result["authors"]) == 2
        assert result["authors"][0]["family"] == "Smith"
        assert result["authors"][0]["given"] == "John"
        assert result["authors"][0]["affiliation"] == "University of Testing"

    def test_pub_date_variants(self):
        from app.services.pubmed_api import _parse_europe_pmc_article
        # Year only
        r1 = _parse_europe_pmc_article({"id": "1", "title": "T", "pubYear": "2023"})
        assert r1["pub_year"] == 2023
        # Full date
        r2 = _parse_europe_pmc_article({"id": "2", "title": "T", "firstPublicationDate": "2024-03-15"})
        assert r2["pub_year"] == 2024
        assert str(r2["pub_date"]) == "2024-03-15"


class TestSearchEuropePmc:
    """search_europe_pmc_articles with mocked HTTP"""

    @pytest.mark.asyncio
    async def test_empty_query(self):
        from app.services.pubmed_api import search_europe_pmc_articles
        result = await search_europe_pmc_articles("")
        assert result == []

    @pytest.mark.asyncio
    async def test_single_page(self):
        from app.services.pubmed_api import search_europe_pmc_articles
        mock_response = MagicMock()
        mock_response.json.return_value = {
            "hitCount": 2,
            "nextCursorMark": None,
            "resultList": {
                "result": [
                    {"id": "100", "title": "Article One"},
                    {"id": "200", "title": "Article Two"},
                ]
            },
        }

        with patch("app.services.pubmed_api._fetch_with_retry", new_callable=AsyncMock) as mock_fetch:
            mock_fetch.return_value = mock_response
            results = await search_europe_pmc_articles("lung cancer", max_results=100)
            assert len(results) == 2
            assert results[0]["pmid"] == 100
            assert results[1]["pmid"] == 200

    @pytest.mark.asyncio
    async def test_cursor_pagination(self):
        """Verify cursor pagination loops through pages"""
        from app.services.pubmed_api import search_europe_pmc_articles

        page1 = MagicMock()
        page1.json.return_value = {
            "hitCount": 3,
            "nextCursorMark": "cursor_page_2",
            "resultList": {"result": [{"id": "100", "title": "Page 1"}]},
        }
        page2 = MagicMock()
        page2.json.return_value = {
            "hitCount": 3,
            "nextCursorMark": "cursor_page_3",
            "resultList": {"result": [{"id": "200", "title": "Page 2"}]},
        }
        page3 = MagicMock()
        page3.json.return_value = {
            "hitCount": 3,
            "nextCursorMark": None,
            "resultList": {"result": [{"id": "300", "title": "Page 3"}]},
        }

        with patch("app.services.pubmed_api._fetch_with_retry", new_callable=AsyncMock) as mock_fetch:
            mock_fetch.side_effect = [page1, page2, page3]
            results = await search_europe_pmc_articles("test", max_results=100)
            assert len(results) == 3
            assert results[0]["pmid"] == 100
            assert results[1]["pmid"] == 200
            assert results[2]["pmid"] == 300

    @pytest.mark.asyncio
    async def test_max_results_limit(self):
        """Should stop when max_results is reached mid-page"""
        from app.services.pubmed_api import search_europe_pmc_articles

        mock_response = MagicMock()
        mock_response.json.return_value = {
            "hitCount": 100,
            "nextCursorMark": "more",
            "resultList": {
                "result": [
                    {"id": str(i), "title": f"Article {i}"} for i in range(1, 101)
                ]
            },
        }

        with patch("app.services.pubmed_api._fetch_with_retry", new_callable=AsyncMock) as mock_fetch:
            mock_fetch.return_value = mock_response
            results = await search_europe_pmc_articles("test", max_results=10)
            assert len(results) == 10

    @pytest.mark.asyncio
    async def test_http_error_handling(self):
        """HTTP errors should not crash — return partial results"""
        from app.services.pubmed_api import search_europe_pmc_articles

        with patch("app.services.pubmed_api._fetch_with_retry", side_effect=Exception("Connection failed")):
            results = await search_europe_pmc_articles("lung cancer")
            assert results == []


class TestDoiDedup:
    """DOI cross-source dedup in _process_article"""

    @pytest.mark.asyncio
    async def test_doi_dedup_creates_new_when_no_match(self):
        """When DOI has no match in DB, create new record normally"""
        from app.services.pubmed_api import _run_pipeline

        mock_result = MagicMock()
        mock_result.scalar.return_value = None
        mock_result.scalars.return_value.all.return_value = []
        mock_db = AsyncMock()
        mock_db.execute = AsyncMock(return_value=mock_result)

        article = {"pmid": 99999, "doi": "10.1000/new", "title": "New Article",
                   "pub_types": [], "source": "europe_pmc"}

        with patch("app.services.pubmed_api.search_europe_pmc_articles", new_callable=AsyncMock) as mock_search, \
             patch("app.services.pubmed_api.generate_feeds_for_literature", new_callable=AsyncMock) as mock_feed, \
             patch("app.services.pubmed_api._tag_article", new_callable=AsyncMock) as mock_tag:
            mock_search.return_value = [article]
            mock_feed.return_value = 0
            mock_tag.return_value = 0

            stats = await _run_pipeline(
                mock_db, max_per_query=100,
                use_majr=True, use_broad=False,
                use_europe_pmc=True,
            )
            assert stats["new"] == 1
            assert stats["updated"] == 0

    @pytest.mark.asyncio
    async def test_doi_dedup_updates_existing_on_match(self):
        """When DOI matches an existing record with different PMID, update in place"""
        from app.services.pubmed_api import _run_pipeline

        mock_db = AsyncMock()

        # First query (checking existing PMIDs) returns empty
        # Second query (DOI dedup check) returns an existing lit with PMID=11111
        existing_lit = MagicMock()
        existing_lit.pmid = 11111

        async def mock_execute_side_effect(stmt, **kw):
            result = MagicMock()
            cls = type(stmt)

            # Check if it's a DOI dedup query (has doi != pmid condition)
            stmt_str = str(stmt.compile(compile_kwargs={"literal_binds": True}))

            if "global_literature.doi" in stmt_str and "global_literature.pmid !=" in stmt_str:
                result.scalar.return_value = existing_lit
            else:
                result.scalars.return_value.all.return_value = []
                result.scalar.return_value = 0
            return result

        mock_db.execute.side_effect = mock_execute_side_effect

        article = {"pmid": 99999, "doi": "10.1000/existing", "title": "Updated Article",
                   "pub_types": ["Journal Article"], "source": "europe_pmc"}

        with patch("app.services.pubmed_api.search_europe_pmc_articles", new_callable=AsyncMock) as mock_search, \
             patch("app.services.pubmed_api.generate_feeds_for_literature", new_callable=AsyncMock), \
             patch("app.services.pubmed_api._tag_article", new_callable=AsyncMock), \
             patch("app.services.pubmed_api._update_lit_from_article") as mock_update:
            mock_search.return_value = [article]

            stats = await _run_pipeline(
                mock_db, max_per_query=100,
                use_majr=True, use_broad=False,
                use_europe_pmc=True,
            )
            # Should be updated, not new
            assert stats["new"] == 0
            assert stats["updated"] == 1
            mock_update.assert_called_once_with(existing_lit, article)

    @pytest.mark.asyncio
    async def test_doi_dedup_no_action_when_no_doi(self):
        """Articles without DOI bypass dedup and create new records"""
        from app.services.pubmed_api import _run_pipeline

        mock_result = MagicMock()
        mock_result.scalars.return_value.all.return_value = []
        mock_result.scalar.return_value = None
        mock_db = AsyncMock()
        mock_db.execute = AsyncMock(return_value=mock_result)

        article = {"pmid": 88888, "title": "No DOI Article",
                   "pub_types": [], "source": "europe_pmc"}

        with patch("app.services.pubmed_api.search_europe_pmc_articles", new_callable=AsyncMock) as mock_search, \
             patch("app.services.pubmed_api.generate_feeds_for_literature", new_callable=AsyncMock) as mock_feed, \
             patch("app.services.pubmed_api._tag_article", new_callable=AsyncMock):
            mock_search.return_value = [article]
            mock_feed.return_value = 0

            stats = await _run_pipeline(
                mock_db, max_per_query=100,
                use_majr=True, use_broad=False,
                use_europe_pmc=True,
            )
            assert stats["new"] == 1
            assert stats["updated"] == 0


__all__ = []
