"""Unit tests for BibSearchTool — input validation + error paths."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from axm.tools.base import ToolResult

from axm_bib.models import Paper, SearchResult
from axm_bib.tools.bib_search import BibSearchTool
from tests.conftest import SAMPLE_DOI


class TestBibSearchToolValidation:
    """Input validation tests."""

    def test_invalid_limit_zero(self) -> None:
        """limit=0 → success=False."""
        tool = BibSearchTool()
        result = tool.execute(query="test", limit=0)
        assert result.success is False
        assert result.error is not None
        assert "limit" in result.error.lower()

    def test_invalid_limit_negative(self) -> None:
        """limit=-5 → success=False."""
        tool = BibSearchTool()
        result = tool.execute(query="test", limit=-5)
        assert result.success is False
        assert "limit" in result.error.lower()  # type: ignore[union-attr]

    @patch("axm_bib.tools.bib_search.BibPipeline")
    def test_empty_query_succeeds(self, mock_cls: MagicMock) -> None:
        """Empty query is valid — delegates to pipeline."""
        mock_cls.return_value.search.return_value = SearchResult(
            papers=[],
            sources_queried=["S2"],
            sources_failed=[],
        )
        tool = BibSearchTool()
        result = tool.execute(query="")
        assert result.success is True
        assert result.data["papers"] == []
        assert result.data["count"] == 0


class TestBibSearchToolSuccess:
    """Successful execution (mocked pipeline)."""

    @patch("axm_bib.tools.bib_search.BibPipeline")
    def test_success_returns_papers(self, mock_cls: MagicMock) -> None:
        """Mocked search returns structured papers."""
        mock_pipeline = mock_cls.return_value
        mock_pipeline.search.return_value = SearchResult(
            papers=[
                Paper(
                    title="Deep Learning",
                    authors=["Smith"],
                    year=2024,
                    doi=SAMPLE_DOI,
                    source="semantic_scholar",
                ),
            ],
            sources_queried=["S2", "CrossRef"],
            sources_failed=[],
        )
        tool = BibSearchTool()
        result = tool.execute(query="Deep Learning", limit=5)
        assert result.success is True
        assert result.data["count"] == 1
        assert result.data["papers"][0]["title"] == "Deep Learning"
        mock_pipeline.search.assert_called_once_with(
            "Deep Learning",
            limit=5,
            venue=None,
            author=None,
            year=None,
        )

    @patch("axm_bib.tools.bib_search.BibPipeline")
    def test_result_is_tool_result(self, mock_cls: MagicMock) -> None:
        """Return type is ToolResult."""
        mock_cls.return_value.search.return_value = SearchResult(
            papers=[],
            sources_queried=[],
            sources_failed=[],
        )
        tool = BibSearchTool()
        result = tool.execute(query="x")
        assert isinstance(result, ToolResult)

    @patch("axm_bib.tools.bib_search.BibPipeline")
    def test_hint_present(self, mock_cls: MagicMock) -> None:
        """Success result includes usage hint."""
        mock_cls.return_value.search.return_value = SearchResult(
            papers=[],
            sources_queried=[],
            sources_failed=[],
        )
        tool = BibSearchTool()
        result = tool.execute(query="x")
        assert result.hint is not None


class TestBibSearchToolErrors:
    """Error handling tests."""

    @patch("axm_bib.tools.bib_search.BibPipeline")
    def test_pipeline_exception(self, mock_cls: MagicMock) -> None:
        """Pipeline exception → success=False with error message."""
        mock_cls.return_value.search.side_effect = Exception("API timeout")
        tool = BibSearchTool()
        result = tool.execute(query="test")
        assert result.success is False
        assert "API timeout" in result.error  # type: ignore[operator]

    @patch("axm_bib.tools.bib_search.BibPipeline")
    def test_pipeline_runtime_error(self, mock_cls: MagicMock) -> None:
        """RuntimeError propagated correctly."""
        mock_cls.return_value.search.side_effect = RuntimeError("config missing")
        tool = BibSearchTool()
        result = tool.execute(query="test")
        assert result.success is False
        assert "config missing" in result.error  # type: ignore[operator]


class TestBibSearchToolNameProperty:
    """Tool name registration."""

    def test_name_is_bib_search(self) -> None:
        """Tool name property returns 'bib_search'."""
        assert BibSearchTool().name == "bib_search"


class TestBibSearchToolSourceVisibility:
    """AXM-253: source failure visibility."""

    @patch("axm_bib.tools.bib_search.BibPipeline")
    def test_search_partial_failure_reports_sources(
        self,
        mock_cls: MagicMock,
    ) -> None:
        """Partial source failure surfaces in result data."""
        mock_cls.return_value.search.return_value = SearchResult(
            papers=[
                Paper(
                    title="Test",
                    authors=["A"],
                    year=2024,
                    doi=SAMPLE_DOI,
                    source="crossref",
                ),
            ],
            sources_queried=["S2", "CrossRef", "DBLP"],
            sources_failed=["S2"],
        )
        tool = BibSearchTool()
        result = tool.execute(query="test")
        assert result.success is True
        assert result.data["sources_failed"] == ["S2"]
        assert "S2" in result.data["sources_queried"]
        assert len(result.data["papers"]) == 1

    @patch("axm_bib.tools.bib_search.BibPipeline")
    def test_search_all_fail_reports_all(
        self,
        mock_cls: MagicMock,
    ) -> None:
        """All sources failing surfaces all names in sources_failed."""
        all_sources = ["S2", "CrossRef", "DBLP", "OpenReview", "arXiv", "OpenAlex"]
        mock_cls.return_value.search.return_value = SearchResult(
            papers=[],
            sources_queried=all_sources,
            sources_failed=all_sources,
        )
        tool = BibSearchTool()
        result = tool.execute(query="test")
        assert result.success is True
        assert result.data["count"] == 0
        assert len(result.data["sources_failed"]) == 6

    @patch("axm_bib.tools.bib_search.BibPipeline")
    def test_search_success_lists_queried(
        self,
        mock_cls: MagicMock,
    ) -> None:
        """All-success case has 6 queried sources and 0 failed."""
        all_sources = ["S2", "CrossRef", "DBLP", "OpenReview", "arXiv", "OpenAlex"]
        mock_cls.return_value.search.return_value = SearchResult(
            papers=[],
            sources_queried=all_sources,
            sources_failed=[],
        )
        tool = BibSearchTool()
        result = tool.execute(query="test")
        assert result.data["sources_queried"] == all_sources
        assert result.data["sources_failed"] == []
