"""
Tests for the SemanticScholarSearchEngine class.

Tests cover:
- Initialization and configuration
- Session creation with retry strategy
- Rate limiting
- Query optimization
- Direct search
- Adaptive search with fallback strategies
- Paper details retrieval
- Preview generation
- Full content retrieval
"""

from unittest.mock import Mock, patch
import pytest


class TestSemanticScholarSearchEngineInit:
    """Tests for SemanticScholarSearchEngine initialization."""

    def test_init_with_defaults(self):
        """Initialize with default values."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            assert engine.max_results == 10
            assert engine.api_key is None
            assert engine.year_range is None
            assert engine.get_abstracts is True
            assert engine.get_references is False
            assert engine.get_citations is False
            assert engine.get_embeddings is False
            assert engine.get_tldr is True
            assert engine.optimize_queries is True

    def test_init_with_api_key(self):
        """Initialize with API key."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(api_key="test-api-key")

            assert engine.api_key == "test-api-key"

    def test_init_with_api_key_from_settings(self):
        """Initialize with API key from settings."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            with patch(
                "local_deep_research.config.search_config.get_setting_from_snapshot",
                return_value="settings-api-key",
            ):
                engine = SemanticScholarSearchEngine(
                    # search.tool present so the inference PEP (now fail-closed
                    # on a missing primary) doesn't raise during engine init.
                    settings_snapshot={"key": "value", "search.tool": "searxng"}
                )

                assert engine.api_key == "settings-api-key"

    def test_init_with_year_range(self):
        """Initialize with year range filter."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(year_range=(2020, 2024))

            assert engine.year_range == (2020, 2024)

    def test_init_with_custom_limits(self):
        """Initialize with custom citation and reference limits."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(
                citation_limit=50, reference_limit=30
            )

            assert engine.citation_limit == 50
            assert engine.reference_limit == 30

    def test_init_with_fields_of_study(self):
        """Initialize with fields of study filter."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(
                fields_of_study=["Computer Science", "Medicine"]
            )

            assert engine.fields_of_study == ["Computer Science", "Medicine"]

    def test_init_with_publication_types(self):
        """Initialize with publication types filter."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(
                publication_types=["JournalArticle", "Conference"]
            )

            assert engine.publication_types == ["JournalArticle", "Conference"]

    def test_init_with_retry_settings(self):
        """Initialize with custom retry settings."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(
                max_retries=10, retry_backoff_factor=2.0
            )

            assert engine.max_retries == 10
            assert engine.retry_backoff_factor == 2.0

    def test_init_with_llm(self):
        """Initialize with LLM."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        mock_llm = Mock()
        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(llm=mock_llm)

            assert engine.llm is mock_llm

    def test_init_normalizes_json_string_fields_of_study(self):
        """JSON-encoded fields_of_study are normalized to a list."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(
                fields_of_study='["Computer Science", "Medicine"]',
            )

            assert engine.fields_of_study == ["Computer Science", "Medicine"]

    def test_init_normalizes_json_string_publication_types(self):
        """JSON-encoded publication_types are normalized to a list."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(
                publication_types='["JournalArticle", "Conference"]',
            )

            assert engine.publication_types == ["JournalArticle", "Conference"]

    def test_init_preserves_none_fields_of_study(self):
        """None fields_of_study is preserved (means no filter)."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(
                fields_of_study=None,
                publication_types=None,
            )

            assert engine.fields_of_study is None
            assert engine.publication_types is None

    def test_init_api_urls(self):
        """Initialize sets up API URLs correctly."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            assert engine.base_url == "https://api.semanticscholar.org/graph/v1"
            assert "paper/search" in engine.paper_search_url


class TestCreateSession:
    """Tests for _create_session method."""

    def test_create_session_returns_session(self):
        """Create session returns a session object."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        engine = SemanticScholarSearchEngine()

        assert engine.session is not None

    def test_create_session_with_api_key_sets_header(self):
        """Create session sets API key header when provided."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        engine = SemanticScholarSearchEngine(api_key="test-key")

        assert "x-api-key" in engine.session.headers
        assert engine.session.headers["x-api-key"] == "test-key"

    def test_create_session_without_api_key(self):
        """Create session without API key doesn't set header."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        engine = SemanticScholarSearchEngine()

        assert "x-api-key" not in engine.session.headers


class TestMakeRequest:
    """Tests for _make_request method."""

    def test_make_request_get(self):
        """Make GET request returns response."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()
            mock_response = Mock()
            mock_response.status_code = 200
            mock_response.json.return_value = {"data": []}
            engine.session = Mock()
            engine.session.get.return_value = mock_response

            result = engine._make_request(
                "https://api.test.com", {"query": "test"}
            )

            assert result == {"data": []}
            engine.session.get.assert_called_once()

    def test_make_request_post(self):
        """Make POST request returns response."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()
            mock_response = Mock()
            mock_response.status_code = 200
            mock_response.json.return_value = {"data": []}
            engine.session = Mock()
            engine.session.post.return_value = mock_response

            result = engine._make_request(
                "https://api.test.com", method="POST", data={"ids": ["123"]}
            )

            assert result == {"data": []}
            engine.session.post.assert_called_once()

    def test_make_request_rate_limit_error(self):
        """Make request raises RateLimitError on 429."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )
        from local_deep_research.web_search_engines.rate_limiting import (
            RateLimitError,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()
            mock_response = Mock()
            mock_response.status_code = 429
            engine.session = Mock()
            engine.session.get.return_value = mock_response

            with pytest.raises(RateLimitError):
                engine._make_request("https://api.test.com")

    def test_make_request_invalid_method(self):
        """Make request raises error for invalid method."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()
            engine.session = Mock()

            with pytest.raises(ValueError) as exc_info:
                engine._make_request("https://api.test.com", method="DELETE")

            assert "Unsupported HTTP method" in str(exc_info.value)


class TestOptimizeQuery:
    """Tests for _optimize_query method."""

    def test_optimize_query_without_llm(self):
        """Optimize query returns original without LLM."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            result = engine._optimize_query("What is machine learning?")

            assert result == "What is machine learning?"

    def test_optimize_query_with_optimization_disabled(self):
        """Optimize query returns original when disabled."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        mock_llm = Mock()
        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(
                llm=mock_llm, optimize_queries=False
            )

            result = engine._optimize_query("What is machine learning?")

            assert result == "What is machine learning?"
            mock_llm.invoke.assert_not_called()

    def test_optimize_query_with_llm(self):
        """Optimize query uses LLM when available."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content="machine learning")

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(llm=mock_llm)

            result = engine._optimize_query("What is machine learning?")

            assert result == "machine learning"
            mock_llm.invoke.assert_called_once()

    def test_optimize_query_falls_back_on_error(self):
        """Optimize query falls back to original on error."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        mock_llm = Mock()
        mock_llm.invoke.side_effect = Exception("LLM error")

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(llm=mock_llm)

            result = engine._optimize_query("What is machine learning?")

            assert result == "What is machine learning?"


class TestDirectSearch:
    """Tests for _direct_search method."""

    def test_direct_search_returns_papers(self):
        """Direct search returns paper data."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            mock_response = {
                "data": [
                    {"paperId": "123", "title": "Test Paper"},
                ]
            }

            with patch.object(
                engine, "_make_request", return_value=mock_response
            ):
                result = engine._direct_search("machine learning")

                assert len(result) == 1
                assert result[0]["title"] == "Test Paper"

    def test_direct_search_with_year_range(self):
        """Direct search includes year range in params."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(year_range=(2020, 2024))

            with patch.object(
                engine, "_make_request", return_value={"data": []}
            ) as mock_request:
                engine._direct_search("test query")

                call_args = mock_request.call_args
                assert call_args[0][1]["year"] == "2020-2024"

    def test_direct_search_empty_response(self):
        """Direct search handles empty response."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            with patch.object(engine, "_make_request", return_value={}):
                result = engine._direct_search("nonexistent query")

                assert result == []


class TestAdaptiveSearch:
    """Tests for _adaptive_search method."""

    def test_adaptive_search_returns_standard_results(self):
        """Adaptive search returns results with standard strategy."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            mock_papers = [{"paperId": "123", "title": "Test Paper"}]

            with patch.object(
                engine, "_direct_search", return_value=mock_papers
            ):
                papers, strategy = engine._adaptive_search("machine learning")

                assert len(papers) == 1
                assert strategy == "standard"

    def test_adaptive_search_tries_unquoted_fallback(self):
        """Adaptive search tries unquoted query as fallback."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            mock_papers = [{"paperId": "123", "title": "Test Paper"}]

            # First call (with quotes) returns empty, second returns results
            with patch.object(
                engine, "_direct_search", side_effect=[[], mock_papers]
            ):
                papers, strategy = engine._adaptive_search('"machine learning"')

                assert len(papers) == 1
                assert strategy == "unquoted"


class TestGetPreviews:
    """Tests for _get_previews method."""

    def test_get_previews_returns_formatted_results(self):
        """Get previews returns formatted preview data."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            mock_papers = [
                {
                    "paperId": "abc123",
                    "title": "Test Paper",
                    "abstract": "This is a test abstract",
                    "url": "https://semanticscholar.org/paper/abc123",
                    "authors": [{"name": "John Doe"}],
                    "venue": "Test Conference",
                    "year": 2023,
                    "externalIds": {"DOI": "10.1234/test"},
                    "tldr": {"text": "Test TLDR"},
                }
            ]

            with patch.object(
                engine,
                "_adaptive_search",
                return_value=(mock_papers, "standard"),
            ):
                previews = engine._get_previews("test query")

                assert len(previews) == 1
                assert previews[0]["id"] == "abc123"
                assert previews[0]["title"] == "Test Paper"
                assert previews[0]["source"] == "Semantic Scholar"
                assert previews[0]["tldr"] == "Test TLDR"

    def test_get_previews_handles_missing_fields(self):
        """Get previews handles papers with missing fields."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            mock_papers = [
                {
                    "paperId": "abc123",
                    "title": "Test Paper",
                    # Missing abstract, authors, etc.
                }
            ]

            with patch.object(
                engine,
                "_adaptive_search",
                return_value=(mock_papers, "standard"),
            ):
                previews = engine._get_previews("test query")

                assert len(previews) == 1
                assert previews[0]["snippet"] == ""
                assert previews[0]["authors"] == []

    def test_get_previews_truncates_long_abstract(self):
        """Get previews truncates long abstracts."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            mock_papers = [
                {
                    "paperId": "abc123",
                    "title": "Test Paper",
                    "abstract": "x" * 300,  # Long abstract
                }
            ]

            with patch.object(
                engine,
                "_adaptive_search",
                return_value=(mock_papers, "standard"),
            ):
                previews = engine._get_previews("test query")

                assert len(previews[0]["snippet"]) == 253  # 250 + "..."

    def test_get_previews_empty_results(self):
        """Get previews handles no results."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            with patch.object(
                engine, "_adaptive_search", return_value=([], "standard")
            ):
                previews = engine._get_previews("nonexistent query")

                assert previews == []


class TestGetFullContent:
    """Tests for _get_full_content method."""

    def test_get_full_content_returns_items(self):
        """Get full content returns items."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            items = [
                {
                    "id": "abc123",
                    "title": "Test Paper",
                    "_paper_id": "abc123",
                    "_search_strategy": "standard",
                    "_full_paper": {},
                }
            ]

            results = engine._get_full_content(items)

            assert len(results) == 1
            assert results[0]["title"] == "Test Paper"
            # Temporary fields should be removed
            assert "_paper_id" not in results[0]
            assert "_search_strategy" not in results[0]

    def test_get_full_content_with_citations(self):
        """Get full content fetches citations when enabled."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine(get_citations=True)

            items = [
                {
                    "id": "abc123",
                    "title": "Test Paper",
                    "_paper_id": "abc123",
                    "_search_strategy": "standard",
                    "_full_paper": {},
                }
            ]

            mock_details = {
                "citations": [{"paperId": "cite1", "title": "Citing Paper"}]
            }

            with patch.object(
                engine, "_get_paper_details", return_value=mock_details
            ):
                results = engine._get_full_content(items)

                assert "citations" in results[0]
                assert len(results[0]["citations"]) == 1

    def test_get_full_content_without_paper_id(self):
        """Get full content handles items without paper ID."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        with patch.object(SemanticScholarSearchEngine, "_create_session"):
            engine = SemanticScholarSearchEngine()

            items = [{"id": "", "title": "Test Paper"}]

            results = engine._get_full_content(items)

            assert len(results) == 1


class TestClassAttributes:
    """Tests for class attributes."""

    def test_is_public(self):
        """SemanticScholarSearchEngine is marked as public."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        assert SemanticScholarSearchEngine.is_public is True

    def test_is_scientific(self):
        """SemanticScholarSearchEngine is marked as scientific."""
        from local_deep_research.web_search_engines.engines.search_engine_semantic_scholar import (
            SemanticScholarSearchEngine,
        )

        assert SemanticScholarSearchEngine.is_scientific is True
