"""Unit tests for axm_bib.search — dedup, filtering, and relevance ranking."""

from __future__ import annotations

from typing import Literal

from axm_bib.models import Paper
from axm_bib.search import (
    RELEVANCE_THRESHOLD,
    dedup_papers,
    filter_papers,
    rank_by_relevance,
)

# ── Helpers ──────────────────────────────────────────────────────────────


# Source type alias for test helpers
type _Source = Literal[
    "semantic_scholar", "crossref", "dblp", "openreview", "arxiv", "openalex", "hal"
]


def _paper(  # noqa: PLR0913
    title: str = "Test Paper",
    *,
    doi: str | None = None,
    arxiv_id: str | None = None,
    venue: str | None = None,
    authors: list[str] | None = None,
    year: int | None = 2024,
    source: _Source = "semantic_scholar",
    abstract: str | None = None,
) -> Paper:
    return Paper(
        title=title,
        authors=authors or ["Author One"],
        year=year,
        doi=doi,
        arxiv_id=arxiv_id,
        venue=venue,
        source=source,
        abstract=abstract,
    )


# ── Deduplication ────────────────────────────────────────────────────────


class TestDedupPapers:
    """Tests for dedup_papers."""

    def test_no_duplicates(self) -> None:
        papers = [_paper("A", doi="10.1/a"), _paper("B", doi="10.1/b")]
        assert len(dedup_papers(papers)) == 2

    def test_dedup_by_doi(self) -> None:
        papers = [
            _paper("A", doi="10.1/a", source="semantic_scholar"),
            _paper("A", doi="10.1/a", source="crossref"),
        ]
        result = dedup_papers(papers)
        assert len(result) == 1

    def test_keeps_richest(self) -> None:
        sparse = _paper("A", doi="10.1/a")
        rich = _paper("A", doi="10.1/a", arxiv_id="2301.00001", venue="ICML")
        result = dedup_papers([sparse, rich])
        assert len(result) == 1
        assert result[0].venue == "ICML"

    def test_dedup_by_arxiv(self) -> None:
        papers = [
            _paper("A", arxiv_id="2301.00001"),
            _paper("A", arxiv_id="2301.00001"),
        ]
        assert len(dedup_papers(papers)) == 1

    def test_dedup_by_title(self) -> None:
        papers = [_paper("Same Title"), _paper("Same Title")]
        assert len(dedup_papers(papers)) == 1

    def test_empty_input(self) -> None:
        assert dedup_papers([]) == []


# ── Filtering ────────────────────────────────────────────────────────────


class TestFilterPapers:
    """Tests for filter_papers."""

    def test_no_filters(self) -> None:
        papers = [_paper("A"), _paper("B")]
        assert len(filter_papers(papers)) == 2

    def test_filter_by_venue(self) -> None:
        papers = [_paper("A", venue="ICML"), _paper("B", venue="NeurIPS")]
        result = filter_papers(papers, venue="icml")
        assert len(result) == 1
        assert result[0].venue == "ICML"

    def test_filter_by_author(self) -> None:
        papers = [
            _paper("A", authors=["Alice Smith"]),
            _paper("B", authors=["Bob Jones"]),
        ]
        result = filter_papers(papers, author="alice")
        assert len(result) == 1

    def test_filter_by_year(self) -> None:
        papers = [_paper("A", year=2023), _paper("B", year=2024)]
        result = filter_papers(papers, year=2024)
        assert len(result) == 1
        assert result[0].year == 2024

    def test_combined_filters(self) -> None:
        papers = [
            _paper("A", venue="ICML", year=2024),
            _paper("B", venue="ICML", year=2023),
            _paper("C", venue="NeurIPS", year=2024),
        ]
        result = filter_papers(papers, venue="icml", year=2024)
        assert len(result) == 1
        assert result[0].title == "A"

    def test_accent_safe_author_filter(self) -> None:
        papers = [_paper("A", authors=["José García"])]
        result = filter_papers(papers, author="jose garcia")
        assert len(result) == 1


# ── Relevance ranking ───────────────────────────────────────────────────


class TestRankByRelevance:
    """Tests for rank_by_relevance."""

    def test_empty_query_returns_all(self) -> None:
        papers = [_paper("A"), _paper("B")]
        assert rank_by_relevance("", papers) == papers

    def test_filters_irrelevant(self) -> None:
        papers = [
            _paper("Deep Learning for NLP"),
            _paper("Quantum Computing Advances"),
        ]
        result = rank_by_relevance("Deep Learning for NLP", papers)
        assert len(result) >= 1
        assert result[0].title == "Deep Learning for NLP"

    def test_sorts_by_similarity(self) -> None:
        papers = [
            _paper("Quantum Computing"),
            _paper("Attention Is All You Need"),
        ]
        result = rank_by_relevance("Attention Is All You Need", papers)
        assert result[0].title == "Attention Is All You Need"

    def test_threshold_respected(self) -> None:
        papers = [_paper("Completely Unrelated Topic XYZ")]
        result = rank_by_relevance(
            "Deep Learning for NLP",
            papers,
            threshold=RELEVANCE_THRESHOLD,
        )
        # Very different titles should be filtered out
        assert len(result) <= 1

    def test_empty_papers(self) -> None:
        assert rank_by_relevance("query", []) == []

    def test_prefers_published_at_equal_ratio(self) -> None:
        """Papers with a venue should rank higher at equal similarity."""
        p1 = _paper("Test Title", venue=None)
        p2 = _paper("Test Title", venue="ICML")
        result = rank_by_relevance("Test Title", [p1, p2])
        assert len(result) == 2
        assert result[0].venue == "ICML"
