"""
Deep coverage tests for PubMed search engine.

Targets statements NOT covered by test_search_engine_pubmed_coverage.py:
- _get_result_count: API key inclusion, exception path returning 0
- _extract_core_terms: field-tag removal, operator removal, short-word filtering
- _simplify_query: Mesh tag removal, Publication Type removal, no-op case
- _is_historical_focused: no-LLM keyword path, historical year detection
- _adaptive_search: historical_focus path (no time filter branch)
- _get_previews: simplified-query retry when first adaptive_search returns empty
- _get_full_content: item missing _pmid key
- _get_article_abstracts: labeled abstract sections, exception returning {}
- _get_article_detailed_metadata: full XML parsing (pub types, MeSH, keywords,
  affiliations, grants, pmc_id, COI), exception returning {}
- _find_pmc_ids: happy path, get_full_text=False short-circuit
- _get_pmc_full_text: happy path with title/abstract/body, exception returning ""
- search_by_author, search_by_journal, search_recent, advanced_search
"""

from unittest.mock import Mock, patch


MODULE = "local_deep_research.web_search_engines.engines.search_engine_pubmed"


def _make_engine(**kwargs):
    from local_deep_research.web_search_engines.engines.search_engine_pubmed import (
        PubMedSearchEngine,
    )

    return PubMedSearchEngine(**kwargs)


# ---------------------------------------------------------------------------
# _get_result_count
# ---------------------------------------------------------------------------


class TestGetResultCount:
    def test_includes_api_key_in_params(self):
        engine = _make_engine(api_key="SECRET123")
        with patch(f"{MODULE}.safe_get") as mock_get:
            mock_resp = Mock()
            mock_resp.json.return_value = {"esearchresult": {"count": "42"}}
            mock_resp.raise_for_status = Mock()
            mock_get.return_value = mock_resp
            count = engine._get_result_count("cancer")
            call_kwargs = mock_get.call_args[1]
            assert call_kwargs["params"]["api_key"] == "SECRET123"
            assert count == 42

    def test_returns_zero_on_exception(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get", side_effect=RuntimeError("network")):
            count = engine._get_result_count("cancer")
        assert count == 0


# ---------------------------------------------------------------------------
# _extract_core_terms
# ---------------------------------------------------------------------------


class TestExtractCoreTerms:
    def test_removes_field_tags(self):
        engine = _make_engine()
        # The regex \[\w+\] strips simple alphanumeric tags like [Mesh] but not
        # compound tags like [Title/Abstract] (slash is not a \w char).
        # [Mesh] should be gone; verify [Mesh] is removed:
        result = engine._extract_core_terms("cancer AND treatment[Mesh]")
        assert "[Mesh]" not in result
        assert "cancer" in result
        assert "treatment" in result

    def test_removes_boolean_operators(self):
        engine = _make_engine()
        result = engine._extract_core_terms("alpha AND beta OR gamma NOT delta")
        for op in ("AND", "OR", "NOT"):
            assert op not in result

    def test_filters_short_words(self):
        engine = _make_engine()
        # "a", "in", "of" are < 4 chars and should be dropped
        result = engine._extract_core_terms("a cancer in of biology")
        assert "cancer" in result
        assert "biology" in result
        # single-char and 2-char words dropped
        terms = result.split()
        assert all(len(t) >= 4 for t in terms)

    def test_limits_to_five_terms(self):
        engine = _make_engine()
        long_query = " ".join(
            [f"term{i}word" for i in range(10)]
        )  # all >= 4 chars
        result = engine._extract_core_terms(long_query)
        assert len(result.split()) <= 5


# ---------------------------------------------------------------------------
# _simplify_query
# ---------------------------------------------------------------------------


class TestSimplifyQuery:
    def test_removes_mesh_tags(self):
        engine = _make_engine()
        simplified = engine._simplify_query(
            "cancer[Mesh] AND treatment[Title/Abstract]"
        )
        assert "[Mesh]" not in simplified

    def test_removes_publication_type_tags(self):
        engine = _make_engine()
        simplified = engine._simplify_query(
            '"Review"[Publication Type] AND cancer'
        )
        assert "[Publication Type]" not in simplified

    def test_no_op_query_returned_unchanged_content(self):
        engine = _make_engine()
        # A query with no [Mesh] or [Publication Type] stays the same
        original = "cancer treatment outcomes"
        simplified = engine._simplify_query(original)
        assert simplified == original


# ---------------------------------------------------------------------------
# _is_historical_focused (no-LLM keyword/year paths)
# ---------------------------------------------------------------------------


class TestIsHistoricalFocusedNoLLM:
    def test_keyword_history_triggers_historical(self):
        engine = _make_engine(llm=None)
        assert engine._is_historical_focused("history of antibiotics") is True

    def test_keyword_origins_triggers_historical(self):
        engine = _make_engine(llm=None)
        assert engine._is_historical_focused("origins of the vaccine") is True

    def test_historical_year_in_query_triggers_true(self):
        engine = _make_engine(llm=None)
        # Any year 1900-2019 in query
        assert engine._is_historical_focused("cancer research 1985") is True

    def test_modern_query_is_not_historical(self):
        engine = _make_engine(llm=None)
        assert (
            engine._is_historical_focused("new cancer immunotherapy") is False
        )


# ---------------------------------------------------------------------------
# _adaptive_search: historical focus branch
# ---------------------------------------------------------------------------


class TestAdaptiveSearchHistoricalFocus:
    def test_historical_query_runs_without_time_filter(self):
        engine = _make_engine(llm=None)
        with patch.object(engine, "_get_result_count", return_value=9999):
            with patch.object(
                engine,
                "_is_historical_focused",
                return_value=True,
            ):
                with patch.object(
                    engine,
                    "_search_pubmed",
                    return_value=["1", "2"],
                ) as mock_search:
                    results, strategy = engine._adaptive_search(
                        "history of penicillin"
                    )
        assert strategy == "historical_focus"
        assert results == ["1", "2"]
        # The query passed to _search_pubmed must NOT contain [pdat]
        call_arg = mock_search.call_args[0][0]
        assert "[pdat]" not in call_arg


# ---------------------------------------------------------------------------
# _get_previews: simplified-query retry path
# ---------------------------------------------------------------------------


class TestGetPreviewsSimplifiedRetry:
    def test_retries_simplified_query_when_first_returns_empty(self):
        engine = _make_engine(llm=None)
        call_count = {"n": 0}

        def adaptive_side_effect(query):
            call_count["n"] += 1
            if call_count["n"] == 1:
                return [], "no_results"
            return ["1"], "rare_topic"

        summary = {
            "id": "1",
            "title": "T",
            "link": "https://pubmed.ncbi.nlm.nih.gov/1/",
            "pubdate": "2024",
            "authors": [],
            "journal": "J",
            "pubtype": [],
            "lang": [],
            "doi": "",
        }
        # _simplify_query must return a DIFFERENT string to trigger the retry branch
        with patch.object(
            engine, "_simplify_query", return_value="rare orphan"
        ):
            with patch.object(
                engine, "_adaptive_search", side_effect=adaptive_side_effect
            ):
                with patch.object(
                    engine, "_get_article_summaries", return_value=[summary]
                ):
                    with patch.object(
                        engine, "_get_article_abstracts", return_value={}
                    ):
                        previews = engine._get_previews("rare orphan disease")
        assert len(previews) == 1
        assert call_count["n"] == 2  # retried with simplified query


# ---------------------------------------------------------------------------
# _get_full_content: item missing _pmid key
# ---------------------------------------------------------------------------


class TestGetFullContentMissingPmid:
    def test_item_without_pmid_still_included_in_results(self):
        engine = _make_engine(get_abstracts=False, get_full_text=False)
        item = {
            "id": "X",
            "title": "No PMID item",
            "link": "https://example.com",
            "snippet": "test",
        }
        # No _pmid key on item
        results = engine._get_full_content([item])
        assert len(results) == 1
        assert results[0]["title"] == "No PMID item"
        # Temp fields that don't exist shouldn't cause errors
        assert "_pmid" not in results[0]


# ---------------------------------------------------------------------------
# _get_article_abstracts: labeled sections
# ---------------------------------------------------------------------------


class TestGetArticleAbstractsLabeledSections:
    def test_concatenates_labeled_sections(self):
        engine = _make_engine()
        xml_response = """<?xml version="1.0"?>
        <PubmedArticleSet>
            <PubmedArticle>
                <MedlineCitation>
                    <PMID>33333</PMID>
                    <Article>
                        <Abstract>
                            <AbstractText Label="BACKGROUND">Background info.</AbstractText>
                            <AbstractText Label="METHODS">Methods used here.</AbstractText>
                            <AbstractText Label="CONCLUSION">Conclusions drawn.</AbstractText>
                        </Abstract>
                    </Article>
                </MedlineCitation>
            </PubmedArticle>
        </PubmedArticleSet>"""
        with patch(f"{MODULE}.safe_get") as mock_get:
            mock_resp = Mock()
            mock_resp.text = xml_response
            mock_resp.raise_for_status = Mock()
            mock_get.return_value = mock_resp
            result = engine._get_article_abstracts(["33333"])
        assert "33333" in result
        assert "BACKGROUND:" in result["33333"]
        assert "METHODS:" in result["33333"]
        assert "CONCLUSION:" in result["33333"]

    def test_returns_empty_dict_on_exception(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get", side_effect=RuntimeError("timeout")):
            result = engine._get_article_abstracts(["99999"])
        assert result == {}


# ---------------------------------------------------------------------------
# _get_article_detailed_metadata: full XML parsing
# ---------------------------------------------------------------------------


class TestGetArticleDetailedMetadata:
    _XML = """<?xml version="1.0"?>
    <PubmedArticleSet>
        <PubmedArticle>
            <MedlineCitation>
                <PMID>12345</PMID>
                <Article>
                    <PublicationTypeList>
                        <PublicationType>Clinical Trial</PublicationType>
                        <PublicationType>Journal Article</PublicationType>
                    </PublicationTypeList>
                    <AuthorList>
                        <Author>
                            <Affiliation>Harvard Medical School, Boston, MA.</Affiliation>
                        </Author>
                    </AuthorList>
                    <KeywordList>
                        <Keyword>CRISPR</Keyword>
                        <Keyword>gene editing</Keyword>
                    </KeywordList>
                </Article>
                <MeshHeadingList>
                    <MeshHeading>
                        <DescriptorName>Neoplasms</DescriptorName>
                    </MeshHeading>
                </MeshHeadingList>
                <GrantList>
                    <Grant>
                        <GrantID>R01-CA-123456</GrantID>
                        <Agency>NIH</Agency>
                    </Grant>
                </GrantList>
                <CoiStatement>Dr. Smith received honoraria from Pharma Corp.</CoiStatement>
            </MedlineCitation>
            <PubmedData>
                <ArticleIdList>
                    <ArticleId IdType="pmc">PMC9876543</ArticleId>
                </ArticleIdList>
            </PubmedData>
        </PubmedArticle>
    </PubmedArticleSet>"""

    def test_extracts_publication_types(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get") as mock_get:
            mock_resp = Mock()
            mock_resp.text = self._XML
            mock_resp.raise_for_status = Mock()
            mock_get.return_value = mock_resp
            result = engine._get_article_detailed_metadata(["12345"])
        assert "publication_types" in result["12345"]
        assert "Clinical Trial" in result["12345"]["publication_types"]

    def test_extracts_mesh_terms(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get") as mock_get:
            mock_resp = Mock()
            mock_resp.text = self._XML
            mock_resp.raise_for_status = Mock()
            mock_get.return_value = mock_resp
            result = engine._get_article_detailed_metadata(["12345"])
        assert "Neoplasms" in result["12345"]["mesh_terms"]

    def test_extracts_keywords(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get") as mock_get:
            mock_resp = Mock()
            mock_resp.text = self._XML
            mock_resp.raise_for_status = Mock()
            mock_get.return_value = mock_resp
            result = engine._get_article_detailed_metadata(["12345"])
        assert "CRISPR" in result["12345"]["keywords"]

    def test_extracts_affiliations(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get") as mock_get:
            mock_resp = Mock()
            mock_resp.text = self._XML
            mock_resp.raise_for_status = Mock()
            mock_get.return_value = mock_resp
            result = engine._get_article_detailed_metadata(["12345"])
        assert "Harvard Medical School" in result["12345"]["affiliations"][0]

    def test_extracts_grants(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get") as mock_get:
            mock_resp = Mock()
            mock_resp.text = self._XML
            mock_resp.raise_for_status = Mock()
            mock_get.return_value = mock_resp
            result = engine._get_article_detailed_metadata(["12345"])
        grants = result["12345"]["grants"]
        assert grants[0]["agency"] == "NIH"
        assert grants[0]["id"] == "R01-CA-123456"

    def test_extracts_coi_statement(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get") as mock_get:
            mock_resp = Mock()
            mock_resp.text = self._XML
            mock_resp.raise_for_status = Mock()
            mock_get.return_value = mock_resp
            result = engine._get_article_detailed_metadata(["12345"])
        assert "Pharma Corp" in result["12345"]["conflict_of_interest"]

    def test_returns_empty_dict_on_exception(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get", side_effect=RuntimeError("bad")):
            result = engine._get_article_detailed_metadata(["12345"])
        assert result == {}


# ---------------------------------------------------------------------------
# _find_pmc_ids
# ---------------------------------------------------------------------------


class TestFindPmcIds:
    def test_returns_empty_when_get_full_text_false(self):
        engine = _make_engine(get_full_text=False)
        result = engine._find_pmc_ids(["111"])
        assert result == {}

    def test_maps_pmid_to_pmcid(self):
        engine = _make_engine(get_full_text=True)
        response_data = {
            "linksets": [
                {
                    "ids": [111],
                    "linksetdbs": [
                        {
                            "linkname": "pubmed_pmc",
                            "links": [654321],
                        }
                    ],
                }
            ]
        }
        with patch(f"{MODULE}.safe_get") as mock_get:
            mock_resp = Mock()
            mock_resp.json.return_value = response_data
            mock_resp.raise_for_status = Mock()
            mock_get.return_value = mock_resp
            result = engine._find_pmc_ids(["111"])
        assert result["111"] == "PMC654321"

    def test_returns_empty_on_exception(self):
        engine = _make_engine(get_full_text=True)
        with patch(f"{MODULE}.safe_get", side_effect=RuntimeError("timeout")):
            result = engine._find_pmc_ids(["111"])
        assert result == {}


# ---------------------------------------------------------------------------
# _get_pmc_full_text
# ---------------------------------------------------------------------------


class TestGetPmcFullText:
    _PMC_XML = """<?xml version="1.0"?>
    <pmc-articleset>
        <article>
            <front>
                <article-meta>
                    <title-group>
                        <article-title>Amazing Study</article-title>
                    </title-group>
                </article-meta>
            </front>
            <front-stub>
                <abstract>
                    <p>This is the abstract paragraph.</p>
                </abstract>
            </front-stub>
            <body>
                <sec>
                    <title>Introduction</title>
                    <p>This is the introduction text.</p>
                </sec>
            </body>
        </article>
    </pmc-articleset>"""

    def test_extracts_title_and_sections(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get") as mock_get:
            mock_resp = Mock()
            mock_resp.text = self._PMC_XML
            mock_resp.raise_for_status = Mock()
            mock_get.return_value = mock_resp
            result = engine._get_pmc_full_text("PMC123456")
        assert "Amazing Study" in result
        assert "Introduction" in result
        assert "introduction text" in result

    def test_returns_empty_string_on_exception(self):
        engine = _make_engine()
        with patch(f"{MODULE}.safe_get", side_effect=RuntimeError("network")):
            result = engine._get_pmc_full_text("PMC999")
        assert result == ""


# ---------------------------------------------------------------------------
# search_by_author / search_by_journal / search_recent / advanced_search
# ---------------------------------------------------------------------------


class TestSearchByAuthor:
    def test_builds_author_query_and_calls_run(self):
        engine = _make_engine()
        with patch.object(engine, "run", return_value=[]) as mock_run:
            engine.search_by_author("Smith J", max_results=5)
        mock_run.assert_called_once()
        assert "Smith J[Author]" in mock_run.call_args[0][0]

    def test_restores_max_results_after_call(self):
        engine = _make_engine()
        original = engine.max_results
        with patch.object(engine, "run", return_value=[]):
            engine.search_by_author("Smith J", max_results=3)
        assert engine.max_results == original


class TestSearchByJournal:
    def test_builds_journal_query_and_calls_run(self):
        engine = _make_engine()
        with patch.object(engine, "run", return_value=[]) as mock_run:
            engine.search_by_journal("Nature Medicine", max_results=10)
        mock_run.assert_called_once()
        assert "Nature Medicine[Journal]" in mock_run.call_args[0][0]

    def test_restores_max_results_after_call(self):
        engine = _make_engine()
        original = engine.max_results
        with patch.object(engine, "run", return_value=[]):
            engine.search_by_journal("Nature", max_results=5)
        assert engine.max_results == original


class TestSearchRecent:
    def test_sets_days_limit_and_calls_run(self):
        engine = _make_engine()
        with patch.object(engine, "run", return_value=[]):
            engine.search_recent("cancer", days=14, max_results=5)
        # days_limit should be restored after call
        assert engine.days_limit is None  # default

    def test_restores_days_limit_and_max_results_after_call(self):
        engine = _make_engine(days_limit=60)
        original_days = engine.days_limit
        original_max = engine.max_results
        with patch.object(engine, "run", return_value=[]):
            engine.search_recent("cancer", days=7)
        assert engine.days_limit == original_days
        assert engine.max_results == original_max


class TestAdvancedSearch:
    def test_builds_field_specific_query(self):
        engine = _make_engine()
        with patch.object(engine, "run", return_value=[]) as mock_run:
            engine.advanced_search(
                {"Author": "Smith J", "Journal": "Nature"}, max_results=5
            )
        query = mock_run.call_args[0][0]
        assert "Smith J[Author]" in query
        assert "Nature[Journal]" in query
        assert " AND " in query

    def test_restores_max_results_after_call(self):
        engine = _make_engine()
        original = engine.max_results
        with patch.object(engine, "run", return_value=[]):
            engine.advanced_search({"Author": "Jones A"}, max_results=3)
        assert engine.max_results == original
