"""Unit tests for axm-bib CLI — TDD RED phase."""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from axm_bib.models import BibEntry, DownloadResult, Paper

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

_PIPELINE = "axm_bib.cli.BibPipeline"


def _make_paper(**overrides: object) -> Paper:
    defaults: dict[str, object] = {
        "title": "Test Paper",
        "authors": ["Alice"],
        "year": 2024,
        "doi": "10.1234/test",
        "source": "crossref",
    }
    defaults.update(overrides)
    return Paper(**defaults)  # type: ignore[arg-type]


def _make_entry(**overrides: object) -> BibEntry:
    defaults: dict[str, object] = {
        "key": "alice2024test",
        "entry_type": "article",
        "title": "Test Paper",
        "raw": "@article{alice2024test, title={Test Paper}}",
        "doi": "10.1234/test",
    }
    defaults.update(overrides)
    return BibEntry(**defaults)  # type: ignore[arg-type]


# ═══════════════════════════════════════════════════════════════════════════
# search command
# ═══════════════════════════════════════════════════════════════════════════


class TestSearchCommand:
    """Tests for `axm-bib search`."""

    @patch(_PIPELINE)
    def test_search_prints_results(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Happy path: results printed to stdout."""
        mock_cls.return_value.search.return_value = [
            _make_paper(title="Paper A", doi="10.1/a"),
            _make_paper(title="Paper B", doi="10.1/b"),
        ]
        from axm_bib.cli import search

        search("loop invariants")
        out = capsys.readouterr().out
        assert "Paper A" in out
        assert "Paper B" in out

    @patch(_PIPELINE)
    def test_search_empty_query(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Empty query prints error message."""
        from axm_bib.cli import search

        with pytest.raises(SystemExit):
            search("")

    @patch(_PIPELINE)
    def test_search_no_results(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """No results → informative message, no crash."""
        mock_cls.return_value.search.return_value = []
        from axm_bib.cli import search

        search("xyznonexistent")
        out = capsys.readouterr().out
        assert "no results" in out.lower() or "0" in out

    @patch(_PIPELINE)
    def test_search_api_error(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Network failure → graceful error exit."""
        mock_cls.return_value.search.side_effect = RuntimeError("Connection refused")
        from axm_bib.cli import search

        with pytest.raises(SystemExit):
            search("test query")

    @patch(_PIPELINE)
    def test_search_custom_limit(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """`--limit 3` is forwarded to pipeline."""
        mock_cls.return_value.search.return_value = []
        from axm_bib.cli import search

        search("test", limit=3)
        mock_cls.return_value.search.assert_called_once_with("test", limit=3)

    @patch(_PIPELINE)
    def test_search_limit_zero_error(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """`--limit 0` → error exit."""
        from axm_bib.cli import search

        with pytest.raises(SystemExit):
            search("test", limit=0)
        err = capsys.readouterr().err
        assert "limit" in err.lower()

    @patch(_PIPELINE)
    def test_search_limit_negative_error(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """`--limit -1` → error exit."""
        from axm_bib.cli import search

        with pytest.raises(SystemExit):
            search("test", limit=-1)

    @patch(_PIPELINE)
    def test_search_limit_large_clamped(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """`--limit 200` is clamped to 100."""
        mock_cls.return_value.search.return_value = []
        from axm_bib.cli import search

        search("test", limit=200)
        mock_cls.return_value.search.assert_called_once_with("test", limit=100)

    @patch(_PIPELINE)
    def test_search_shows_full_abstract(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Full abstract shown by default (no truncation)."""
        long_abstract = "We present a deep learning approach. " * 10
        mock_cls.return_value.search.return_value = [
            _make_paper(abstract=long_abstract),
        ]
        from axm_bib.cli import search

        search("test")
        out = capsys.readouterr().out
        assert long_abstract.strip() in out

    @patch(_PIPELINE)
    def test_search_abstract_len_truncates(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """--abstract-len 50 truncates and adds '...'."""
        mock_cls.return_value.search.return_value = [
            _make_paper(abstract="A" * 100),
        ]
        from axm_bib.cli import search

        search("test", abstract_len=50)
        out = capsys.readouterr().out
        assert "A" * 50 in out
        assert "..." in out
        assert "A" * 100 not in out

    @patch(_PIPELINE)
    def test_search_no_abstract_flag(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """--no-abstract hides abstract from output."""
        mock_cls.return_value.search.return_value = [
            _make_paper(abstract="We present a deep learning approach"),
        ]
        from axm_bib.cli import search

        search("test", abstract=False)
        out = capsys.readouterr().out
        assert "We present" not in out


class TestDoiCommand:
    """Tests for `axm-bib doi`."""

    @patch(_PIPELINE)
    def test_doi_happy_path(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Prints BibTeX to stdout."""
        entry = _make_entry(raw="@article{a, title={X}}")
        mock_cls.return_value.resolve_bibtex.return_value = entry
        from axm_bib.cli import doi

        doi("10.1234/test")
        out = capsys.readouterr().out
        assert "@article" in out

    @patch(_PIPELINE)
    def test_doi_empty(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Empty DOI → error exit."""
        from axm_bib.cli import doi

        with pytest.raises(SystemExit):
            doi("")

    @patch(_PIPELINE)
    def test_doi_invalid_format(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """API error for invalid DOI → graceful exit."""
        mock_cls.return_value.resolve_bibtex.side_effect = RuntimeError("404 Not Found")
        from axm_bib.cli import doi

        with pytest.raises(SystemExit):
            doi("not-a-doi")

    @patch(_PIPELINE)
    def test_doi_404(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """DOI not found → error message."""
        mock_cls.return_value.resolve_bibtex.side_effect = RuntimeError(
            "CrossRef resolution failed"
        )
        from axm_bib.cli import doi

        with pytest.raises(SystemExit):
            doi("10.9999/nonexistent")


# ═══════════════════════════════════════════════════════════════════════════
# pdf command
# ═══════════════════════════════════════════════════════════════════════════


class TestPdfCommand:
    """Tests for `axm-bib pdf`."""

    @patch("axm_bib.cli.resolve_email", return_value="test@ci.com")
    @patch(_PIPELINE)
    def test_pdf_happy_path(
        self,
        mock_cls: MagicMock,
        _mock_email: MagicMock,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Downloads and prints path."""
        result = DownloadResult(
            path="/tmp/paper.pdf",
            size_bytes=42000,
            doi="10.1234/test",
            is_open_access=True,
        )
        mock_cls.return_value.download_pdf.return_value = result
        from axm_bib.cli import pdf

        pdf("10.1234/test")
        out = capsys.readouterr().out
        assert "/tmp/paper.pdf" in out

    @patch("axm_bib.cli.resolve_email", return_value="test@ci.com")
    @patch(_PIPELINE)
    def test_pdf_not_open_access(
        self,
        mock_cls: MagicMock,
        _mock_email: MagicMock,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Non-OA paper → informative message (no crash)."""
        mock_cls.return_value.download_pdf.return_value = None
        from axm_bib.cli import pdf

        pdf("10.1234/closed")
        out = capsys.readouterr().out
        assert "not" in out.lower() or "open access" in out.lower()

    @patch(_PIPELINE)
    def test_pdf_empty_doi(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Empty DOI → error exit."""
        from axm_bib.cli import pdf

        with pytest.raises(SystemExit):
            pdf("")

    @patch("axm_bib.cli.resolve_email", return_value="test@ci.com")
    @patch(_PIPELINE)
    def test_pdf_custom_output_dir(
        self,
        mock_cls: MagicMock,
        _mock_email: MagicMock,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """`--output-dir` is forwarded."""
        result = DownloadResult(
            path="/custom/paper.pdf",
            size_bytes=1000,
            doi="10.1234/test",
            is_open_access=True,
        )
        mock_cls.return_value.download_pdf.return_value = result
        from axm_bib.cli import pdf

        pdf("10.1234/test", output_dir=Path("/custom"))
        mock_cls.return_value.download_pdf.assert_called_once()

    @patch(_PIPELINE)
    def test_pdf_network_error(
        self, mock_cls: MagicMock, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Network failure → graceful exit."""
        mock_cls.return_value.download_pdf.side_effect = RuntimeError(
            "Connection timeout"
        )
        from axm_bib.cli import pdf

        with pytest.raises(SystemExit):
            pdf("10.1234/test")
