"""
Coverage tests for ArXivSearchEngine.

Targets uncovered paths in search_engine_arxiv.py including:
- __init__ with/without journal filter
- _get_search_results with various sort options
- _get_previews success and error paths (rate limit patterns)
- _get_full_content: snippets-only mode, cache hit/miss, PDF download+extraction,
  PDF limit reached, download failure, PyPDF2 extraction, pdfplumber fallback,
  both-fail path, empty PDF text
- run() cleanup of _papers
- get_paper_details: found/not-found, snippet-only mode, full mode, PDF download
- search_by_author / search_by_category with/without custom max_results
"""

from datetime import datetime
from unittest.mock import MagicMock, Mock, patch, mock_open

import pytest


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_mock_author(name):
    a = Mock()
    a.name = name
    return a


_SENTINEL = object()


def _make_mock_paper(
    entry_id="http://arxiv.org/abs/2101.00001",
    title="Test Paper",
    summary="A short summary",
    authors=None,
    published=_SENTINEL,
    updated=_SENTINEL,
    journal_ref=None,
    pdf_url="http://arxiv.org/pdf/2101.00001",
    categories=None,
    comment=None,
    doi=None,
):
    paper = Mock()
    paper.entry_id = entry_id
    paper.title = title
    paper.summary = summary
    paper.authors = authors or [
        _make_mock_author("Author A"),
        _make_mock_author("Author B"),
    ]
    paper.published = (
        datetime(2021, 1, 1) if published is _SENTINEL else published
    )
    paper.updated = datetime(2021, 6, 1) if updated is _SENTINEL else updated
    paper.journal_ref = journal_ref
    paper.pdf_url = pdf_url
    paper.categories = categories or ["cs.AI"]
    paper.comment = comment
    paper.doi = doi
    paper.download_pdf = Mock(return_value="/tmp/paper.pdf")
    return paper


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def engine():
    """Create ArXivSearchEngine with mocked dependencies."""
    with patch(
        "local_deep_research.web_search_engines.engines.search_engine_arxiv.JournalReputationFilter"
    ) as mock_jrf:
        mock_jrf.create_default.return_value = None
        from local_deep_research.web_search_engines.engines.search_engine_arxiv import (
            ArXivSearchEngine,
        )

        eng = ArXivSearchEngine(max_results=10)
        yield eng


@pytest.fixture
def engine_with_pdf():
    """Engine configured for PDF download."""
    with patch(
        "local_deep_research.web_search_engines.engines.search_engine_arxiv.JournalReputationFilter"
    ) as mock_jrf:
        mock_jrf.create_default.return_value = None
        from local_deep_research.web_search_engines.engines.search_engine_arxiv import (
            ArXivSearchEngine,
        )

        eng = ArXivSearchEngine(
            max_results=10,
            include_full_text=True,
            download_dir="/tmp/papers",
            max_full_text=2,
        )
        yield eng


# ===========================================================================
# __init__ tests
# ===========================================================================


class TestInit:
    def test_default_init(self, engine):
        """Basic init sets expected attributes."""
        assert engine.sort_by == "relevance"
        assert engine.sort_order == "descending"
        assert engine.include_full_text is False
        assert engine.download_dir is None
        assert engine.max_full_text == 1
        # max_results is max(10, 25) = 25
        assert engine.max_results >= 25

    def test_init_with_journal_filter(self):
        """Journal filter is added to content_filters when created."""
        mock_filter = Mock()
        with patch(
            "local_deep_research.web_search_engines.engines.search_engine_arxiv.JournalReputationFilter"
        ) as mock_jrf:
            mock_jrf.create_default.return_value = mock_filter
            from local_deep_research.web_search_engines.engines.search_engine_arxiv import (
                ArXivSearchEngine,
            )

            eng = ArXivSearchEngine(max_results=5)
            assert mock_filter in eng._content_filters

    def test_init_custom_sort(self):
        """Custom sort_by and sort_order are stored."""
        with patch(
            "local_deep_research.web_search_engines.engines.search_engine_arxiv.JournalReputationFilter"
        ) as mock_jrf:
            mock_jrf.create_default.return_value = None
            from local_deep_research.web_search_engines.engines.search_engine_arxiv import (
                ArXivSearchEngine,
            )

            eng = ArXivSearchEngine(
                sort_by="submittedDate", sort_order="ascending"
            )
            assert eng.sort_by == "submittedDate"
            assert eng.sort_order == "ascending"

    def test_max_results_at_least_25(self):
        """max_results should be at least 25 even if lower value passed."""
        with patch(
            "local_deep_research.web_search_engines.engines.search_engine_arxiv.JournalReputationFilter"
        ) as mock_jrf:
            mock_jrf.create_default.return_value = None
            from local_deep_research.web_search_engines.engines.search_engine_arxiv import (
                ArXivSearchEngine,
            )

            eng = ArXivSearchEngine(max_results=5)
            assert eng.max_results >= 25


# ===========================================================================
# _get_search_results
# ===========================================================================


class TestGetSearchResults:
    def test_search_results_default_sort(self, engine):
        """_get_search_results uses default relevance sort."""
        import arxiv

        with (
            patch.object(arxiv, "Client") as mock_client_cls,
            patch.object(arxiv, "Search") as mock_search_cls,
        ):
            mock_client = Mock()
            mock_client.results.return_value = [_make_mock_paper()]
            mock_client_cls.return_value = mock_client

            results = engine._get_search_results("test query")
            assert len(results) == 1
            mock_search_cls.assert_called_once()

    def test_search_results_unknown_sort_fallback(self, engine):
        """Unknown sort_by/sort_order falls back to defaults."""
        import arxiv

        engine.sort_by = "unknown_sort"
        engine.sort_order = "unknown_order"
        with (
            patch.object(arxiv, "Client") as mock_client_cls,
            patch.object(arxiv, "Search"),
        ):
            mock_client = Mock()
            mock_client.results.return_value = []
            mock_client_cls.return_value = mock_client

            results = engine._get_search_results("q")
            # Should not raise, falls back to defaults
            assert results == []

    def test_search_results_submitted_date_ascending(self):
        """Sort by submittedDate ascending."""
        import arxiv

        with patch(
            "local_deep_research.web_search_engines.engines.search_engine_arxiv.JournalReputationFilter"
        ) as mock_jrf:
            mock_jrf.create_default.return_value = None
            from local_deep_research.web_search_engines.engines.search_engine_arxiv import (
                ArXivSearchEngine,
            )

            eng = ArXivSearchEngine(
                sort_by="submittedDate", sort_order="ascending"
            )

        with (
            patch.object(arxiv, "Client") as mock_client_cls,
            patch.object(arxiv, "Search") as mock_search_cls,
        ):
            mock_client = Mock()
            mock_client.results.return_value = []
            mock_client_cls.return_value = mock_client
            eng._get_search_results("q")
            call_kwargs = mock_search_cls.call_args[1]
            assert call_kwargs["sort_by"] == arxiv.SortCriterion.SubmittedDate
            assert call_kwargs["sort_order"] == arxiv.SortOrder.Ascending


# ===========================================================================
# _get_previews
# ===========================================================================


class TestGetPreviews:
    def test_previews_success(self, engine):
        """Successful previews returns formatted list."""
        paper = _make_mock_paper(summary="A" * 300)
        with patch.object(engine, "_get_search_results", return_value=[paper]):
            previews = engine._get_previews("test")
            assert len(previews) == 1
            assert previews[0]["title"] == "Test Paper"
            assert previews[0]["snippet"].endswith("...")
            assert previews[0]["source"] == "arXiv"
            assert hasattr(engine, "_papers")

    def test_previews_short_summary_no_ellipsis(self, engine):
        """Short summary is not truncated."""
        paper = _make_mock_paper(summary="Short")
        with patch.object(engine, "_get_search_results", return_value=[paper]):
            previews = engine._get_previews("test")
            assert previews[0]["snippet"] == "Short"

    def test_previews_no_published_date(self, engine):
        """Paper without published date has None."""
        paper = _make_mock_paper(published=None)
        with patch.object(engine, "_get_search_results", return_value=[paper]):
            previews = engine._get_previews("test")
            assert previews[0]["published"] is None

    def test_previews_generic_error_returns_empty(self, engine):
        """Generic exception returns empty list."""
        with patch.object(
            engine, "_get_search_results", side_effect=ValueError("oops")
        ):
            result = engine._get_previews("test")
            assert result == []

    def test_previews_429_raises_rate_limit(self, engine):
        """429 error raises RateLimitError."""
        from local_deep_research.web_search_engines.rate_limiting import (
            RateLimitError,
        )

        with patch.object(
            engine,
            "_get_search_results",
            side_effect=Exception("HTTP 429 error"),
        ):
            with pytest.raises(RateLimitError):
                engine._get_previews("test")

    def test_previews_too_many_requests_raises(self, engine):
        """'too many requests' raises RateLimitError."""
        from local_deep_research.web_search_engines.rate_limiting import (
            RateLimitError,
        )

        with patch.object(
            engine,
            "_get_search_results",
            side_effect=Exception("too many requests"),
        ):
            with pytest.raises(RateLimitError):
                engine._get_previews("test")

    def test_previews_rate_limit_phrase_raises(self, engine):
        """'rate limit' in message raises RateLimitError."""
        from local_deep_research.web_search_engines.rate_limiting import (
            RateLimitError,
        )

        with patch.object(
            engine,
            "_get_search_results",
            side_effect=Exception("rate limit exceeded"),
        ):
            with pytest.raises(RateLimitError):
                engine._get_previews("test")

    def test_previews_service_unavailable_raises(self, engine):
        """'service unavailable' raises RateLimitError."""
        from local_deep_research.web_search_engines.rate_limiting import (
            RateLimitError,
        )

        with patch.object(
            engine,
            "_get_search_results",
            side_effect=Exception("service unavailable"),
        ):
            with pytest.raises(RateLimitError):
                engine._get_previews("test")

    def test_previews_503_raises(self, engine):
        """503 error raises RateLimitError."""
        from local_deep_research.web_search_engines.rate_limiting import (
            RateLimitError,
        )

        with patch.object(
            engine,
            "_get_search_results",
            side_effect=Exception("503 Service Unavailable"),
        ):
            with pytest.raises(RateLimitError):
                engine._get_previews("test")

    def test_previews_authors_limited_to_3(self, engine):
        """Preview only includes first 3 authors."""
        paper = _make_mock_paper(
            authors=[_make_mock_author(f"Author {i}") for i in range(5)]
        )
        with patch.object(engine, "_get_search_results", return_value=[paper]):
            previews = engine._get_previews("test")
            assert len(previews[0]["authors"]) == 3


# ===========================================================================
# _get_full_content
# ===========================================================================


class TestGetFullContent:
    def test_snippets_only_mode(self, engine):
        """SEARCH_SNIPPETS_ONLY returns items unchanged."""
        items = [{"id": "1", "title": "T"}]
        with patch(
            "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
        ) as mock_cfg:
            mock_cfg.SEARCH_SNIPPETS_ONLY = True
            result = engine._get_full_content(items)
            assert result == items

    def test_no_paper_in_cache(self, engine):
        """Item not in _papers cache is returned as-is."""
        engine._papers = {}
        items = [{"id": "unknown_id", "title": "T"}]
        result = engine._get_full_content(items)
        assert len(result) == 1
        assert "content" not in result[0]

    def test_no_papers_attr(self, engine):
        """If _papers not set, item returned as-is."""
        if hasattr(engine, "_papers"):
            del engine._papers
        items = [{"id": "x", "title": "T"}]
        result = engine._get_full_content(items)
        assert len(result) == 1

    def test_paper_in_cache_no_pdf(self, engine):
        """Paper in cache adds full info; no PDF download when not configured."""
        paper = _make_mock_paper()
        engine._papers = {paper.entry_id: paper}
        items = [{"id": paper.entry_id, "title": paper.title}]
        result = engine._get_full_content(items)
        assert result[0]["content"] == paper.summary
        assert result[0]["pdf_url"] == paper.pdf_url
        assert result[0]["categories"] == ["cs.AI"]

    def test_paper_no_published_date(self, engine):
        """Paper without published/updated dates."""
        paper = _make_mock_paper(published=None, updated=None)
        engine._papers = {paper.entry_id: paper}
        items = [{"id": paper.entry_id, "title": "T"}]
        result = engine._get_full_content(items)
        assert result[0]["published"] is None
        assert result[0]["updated"] is None

    def test_pdf_download_and_pypdf2_extraction(self, engine_with_pdf):
        """PDF download + PyPDF2 text extraction succeeds."""
        paper = _make_mock_paper()
        engine_with_pdf._papers = {paper.entry_id: paper}
        items = [{"id": paper.entry_id, "title": "T"}]

        mock_page = Mock()
        mock_page.extract_text.return_value = "Extracted text"
        mock_reader = Mock()
        mock_reader.pages = [mock_page]

        with (
            patch("builtins.open", mock_open()),
            patch.dict("sys.modules", {"PyPDF2": MagicMock()}),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = False
            # We need to mock PyPDF2 inside the method
            import sys

            mock_pypdf2 = MagicMock()
            mock_pypdf2.PdfReader.return_value = mock_reader
            sys.modules["PyPDF2"] = mock_pypdf2

            try:
                result = engine_with_pdf._get_full_content(items)
                assert result[0]["pdf_path"] == "/tmp/paper.pdf"
                assert result[0]["content"] == "Extracted text\n\n"
            finally:
                del sys.modules["PyPDF2"]

    def test_pdf_download_pypdf2_empty_falls_back_to_summary(
        self, engine_with_pdf
    ):
        """PyPDF2 extracts empty text -> content stays as summary."""
        paper = _make_mock_paper()
        engine_with_pdf._papers = {paper.entry_id: paper}
        items = [{"id": paper.entry_id, "title": "T"}]

        mock_page = Mock()
        mock_page.extract_text.return_value = ""
        mock_reader = Mock()
        mock_reader.pages = [mock_page]

        with (
            patch("builtins.open", mock_open()),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = False
            import sys

            mock_pypdf2 = MagicMock()
            mock_pypdf2.PdfReader.return_value = mock_reader
            sys.modules["PyPDF2"] = mock_pypdf2

            try:
                result = engine_with_pdf._get_full_content(items)
                # Content should be the summary since extracted text is empty
                assert result[0]["content"] == paper.summary
            finally:
                del sys.modules["PyPDF2"]

    def test_pypdf2_fails_pdfplumber_succeeds(self, engine_with_pdf):
        """PyPDF2 import fails, pdfplumber works."""
        paper = _make_mock_paper()
        engine_with_pdf._papers = {paper.entry_id: paper}
        items = [{"id": paper.entry_id, "title": "T"}]

        mock_pdf_page = Mock()
        mock_pdf_page.extract_text.return_value = "Plumber text"
        mock_pdf = Mock()
        mock_pdf.pages = [mock_pdf_page]
        mock_pdf.__enter__ = Mock(return_value=mock_pdf)
        mock_pdf.__exit__ = Mock(return_value=False)

        with (
            patch("builtins.open", mock_open()),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = False
            import sys

            # PyPDF2 fails with ImportError
            mock_pypdf2 = MagicMock()
            mock_pypdf2.PdfReader.side_effect = ImportError("no PyPDF2")
            sys.modules["PyPDF2"] = mock_pypdf2

            mock_pdfplumber = MagicMock()
            mock_pdfplumber.open.return_value = mock_pdf
            sys.modules["pdfplumber"] = mock_pdfplumber

            try:
                result = engine_with_pdf._get_full_content(items)
                assert result[0]["content"] == "Plumber text\n\n"
            finally:
                del sys.modules["PyPDF2"]
                del sys.modules["pdfplumber"]

    def test_both_pdf_extractors_fail(self, engine_with_pdf):
        """Both PyPDF2 and pdfplumber fail -> summary used."""
        paper = _make_mock_paper()
        engine_with_pdf._papers = {paper.entry_id: paper}
        items = [{"id": paper.entry_id, "title": "T"}]

        with (
            patch("builtins.open", mock_open()),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = False
            import sys

            mock_pypdf2 = MagicMock()
            mock_pypdf2.PdfReader.side_effect = Exception("PyPDF2 broken")
            sys.modules["PyPDF2"] = mock_pypdf2

            mock_pdfplumber = MagicMock()
            mock_pdfplumber.open.side_effect = Exception("pdfplumber broken")
            sys.modules["pdfplumber"] = mock_pdfplumber

            try:
                result = engine_with_pdf._get_full_content(items)
                # Falls back to summary
                assert result[0]["content"] == paper.summary
            finally:
                del sys.modules["PyPDF2"]
                del sys.modules["pdfplumber"]

    def test_pdf_download_fails(self, engine_with_pdf):
        """Download failure sets pdf_path to None and decrements counter."""
        paper = _make_mock_paper()
        paper.download_pdf.side_effect = Exception("Network error")
        engine_with_pdf._papers = {paper.entry_id: paper}
        items = [{"id": paper.entry_id, "title": "T"}]

        with patch(
            "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
        ) as mock_cfg:
            mock_cfg.SEARCH_SNIPPETS_ONLY = False
            result = engine_with_pdf._get_full_content(items)
            assert result[0]["pdf_path"] is None

    def test_pdf_limit_reached(self, engine_with_pdf):
        """Once max_full_text PDFs processed, remaining use summary."""
        engine_with_pdf.max_full_text = 1
        paper1 = _make_mock_paper(entry_id="http://arxiv.org/abs/1")
        paper2 = _make_mock_paper(entry_id="http://arxiv.org/abs/2")
        engine_with_pdf._papers = {
            paper1.entry_id: paper1,
            paper2.entry_id: paper2,
        }
        items = [
            {"id": paper1.entry_id, "title": "P1"},
            {"id": paper2.entry_id, "title": "P2"},
        ]

        mock_page = Mock()
        mock_page.extract_text.return_value = "text"
        mock_reader = Mock()
        mock_reader.pages = [mock_page]

        with (
            patch("builtins.open", mock_open()),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = False
            import sys

            mock_pypdf2 = MagicMock()
            mock_pypdf2.PdfReader.return_value = mock_reader
            sys.modules["PyPDF2"] = mock_pypdf2

            try:
                result = engine_with_pdf._get_full_content(items)
                # Second paper should still have content (summary)
                assert result[1]["content"] == paper2.summary
            finally:
                del sys.modules["PyPDF2"]


# ===========================================================================
# run()
# ===========================================================================


class TestRun:
    def test_run_cleans_up_papers(self, engine):
        """run() deletes _papers after completion."""
        with patch.object(
            type(engine).__bases__[0], "run", return_value=[{"title": "T"}]
        ):
            engine._papers = {"id": "paper"}
            result = engine.run("test")
            assert not hasattr(engine, "_papers")
            assert len(result) == 1

    def test_run_no_papers_attr(self, engine):
        """run() does not fail if _papers was never set."""
        with patch.object(type(engine).__bases__[0], "run", return_value=[]):
            if hasattr(engine, "_papers"):
                del engine._papers
            result = engine.run("test")
            assert result == []


# ===========================================================================
# get_paper_details
# ===========================================================================


class TestGetPaperDetails:
    def test_paper_found_full_mode(self, engine):
        """Paper found with full content (not snippet-only)."""
        import arxiv

        paper = _make_mock_paper()
        with (
            patch.object(arxiv, "Client") as mock_client_cls,
            patch.object(arxiv, "Search"),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = False
            mock_client = Mock()
            mock_client.results.return_value = [paper]
            mock_client_cls.return_value = mock_client

            result = engine.get_paper_details("2101.00001")
            assert result["title"] == "Test Paper"
            assert result["content"] == paper.summary
            assert "pdf_url" in result

    def test_paper_not_found(self, engine):
        """No paper found returns empty dict."""
        import arxiv

        with (
            patch.object(arxiv, "Client") as mock_client_cls,
            patch.object(arxiv, "Search"),
        ):
            mock_client = Mock()
            mock_client.results.return_value = []
            mock_client_cls.return_value = mock_client

            result = engine.get_paper_details("9999.99999")
            assert result == {}

    def test_paper_details_exception(self, engine):
        """Exception returns empty dict."""
        import arxiv

        with patch.object(arxiv, "Client", side_effect=Exception("boom")):
            result = engine.get_paper_details("2101.00001")
            assert result == {}

    def test_paper_snippet_only_mode(self, engine):
        """Snippet-only mode returns basic info without full content."""
        import arxiv

        paper = _make_mock_paper(summary="A" * 300)
        with (
            patch.object(arxiv, "Client") as mock_client_cls,
            patch.object(arxiv, "Search"),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = True
            mock_client = Mock()
            mock_client.results.return_value = [paper]
            mock_client_cls.return_value = mock_client

            result = engine.get_paper_details("2101.00001")
            assert result["title"] == "Test Paper"
            assert result["snippet"].endswith("...")
            assert "content" not in result

    def test_paper_details_with_pdf_download(self, engine_with_pdf):
        """PDF download happens in get_paper_details when configured."""
        import arxiv

        paper = _make_mock_paper()
        with (
            patch.object(arxiv, "Client") as mock_client_cls,
            patch.object(arxiv, "Search"),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = False
            mock_client = Mock()
            mock_client.results.return_value = [paper]
            mock_client_cls.return_value = mock_client

            result = engine_with_pdf.get_paper_details("2101.00001")
            assert result["pdf_path"] == "/tmp/paper.pdf"
            paper.download_pdf.assert_called_once()

    def test_paper_details_pdf_download_fails(self, engine_with_pdf):
        """PDF download failure in get_paper_details is handled gracefully."""
        import arxiv

        paper = _make_mock_paper()
        paper.download_pdf.side_effect = Exception("download error")
        with (
            patch.object(arxiv, "Client") as mock_client_cls,
            patch.object(arxiv, "Search"),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = False
            mock_client = Mock()
            mock_client.results.return_value = [paper]
            mock_client_cls.return_value = mock_client

            result = engine_with_pdf.get_paper_details("2101.00001")
            assert result["title"] == "Test Paper"
            assert "pdf_path" not in result

    def test_paper_details_no_published_or_updated(self, engine):
        """Paper with no published/updated dates returns None for those fields."""
        import arxiv

        paper = _make_mock_paper(published=None, updated=None)
        with (
            patch.object(arxiv, "Client") as mock_client_cls,
            patch.object(arxiv, "Search"),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = False
            mock_client = Mock()
            mock_client.results.return_value = [paper]
            mock_client_cls.return_value = mock_client

            result = engine.get_paper_details("2101.00001")
            assert result["published"] is None
            assert result["updated"] is None

    def test_paper_details_short_summary_no_ellipsis(self, engine):
        """Short summary in get_paper_details doesn't get truncated."""
        import arxiv

        paper = _make_mock_paper(summary="Short")
        with (
            patch.object(arxiv, "Client") as mock_client_cls,
            patch.object(arxiv, "Search"),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_arxiv.search_config"
            ) as mock_cfg,
        ):
            mock_cfg.SEARCH_SNIPPETS_ONLY = True
            mock_client = Mock()
            mock_client.results.return_value = [paper]
            mock_client_cls.return_value = mock_client

            result = engine.get_paper_details("2101.00001")
            assert result["snippet"] == "Short"


# ===========================================================================
# search_by_author
# ===========================================================================


class TestSearchByAuthor:
    def test_search_by_author_default_max(self, engine):
        """search_by_author uses default max_results."""
        original = engine.max_results
        with patch.object(engine, "run", return_value=[]) as mock_run:
            engine.search_by_author("John Doe")
            mock_run.assert_called_once_with('au:"John Doe"')
            assert engine.max_results == original

    def test_search_by_author_custom_max(self, engine):
        """search_by_author temporarily sets custom max_results."""
        original = engine.max_results
        with patch.object(engine, "run", return_value=[]):
            engine.search_by_author("Jane Doe", max_results=50)
            # max_results should be restored
            assert engine.max_results == original

    def test_search_by_author_restores_on_exception(self, engine):
        """max_results restored even when run() raises."""
        original = engine.max_results
        with patch.object(engine, "run", side_effect=Exception("fail")):
            with pytest.raises(Exception):
                engine.search_by_author("Author", max_results=99)
            assert engine.max_results == original


# ===========================================================================
# search_by_category
# ===========================================================================


class TestSearchByCategory:
    def test_search_by_category_default_max(self, engine):
        """search_by_category uses default max_results."""
        original = engine.max_results
        with patch.object(engine, "run", return_value=[]) as mock_run:
            engine.search_by_category("cs.AI")
            mock_run.assert_called_once_with("cat:cs.AI")
            assert engine.max_results == original

    def test_search_by_category_custom_max(self, engine):
        """search_by_category temporarily sets custom max_results."""
        original = engine.max_results
        with patch.object(engine, "run", return_value=[]):
            engine.search_by_category("physics.optics", max_results=30)
            assert engine.max_results == original

    def test_search_by_category_restores_on_exception(self, engine):
        """max_results restored even when run() raises."""
        original = engine.max_results
        with patch.object(engine, "run", side_effect=Exception("fail")):
            with pytest.raises(Exception):
                engine.search_by_category("math.AG", max_results=15)
            assert engine.max_results == original


# ===========================================================================
# Class attributes
# ===========================================================================


class TestClassAttributes:
    def test_is_public(self):
        from local_deep_research.web_search_engines.engines.search_engine_arxiv import (
            ArXivSearchEngine,
        )

        assert ArXivSearchEngine.is_public is True

    def test_is_not_generic(self):
        from local_deep_research.web_search_engines.engines.search_engine_arxiv import (
            ArXivSearchEngine,
        )

        assert ArXivSearchEngine.is_generic is False

    def test_is_scientific(self):
        from local_deep_research.web_search_engines.engines.search_engine_arxiv import (
            ArXivSearchEngine,
        )

        assert ArXivSearchEngine.is_scientific is True
