"""Unit tests for axm_bib.merge — deterministic merge + coherence check."""

from __future__ import annotations

import pytest

from axm_bib.exceptions import MergeConflict
from axm_bib.merge import (
    _extract_surname,
    _normalize_title,
    check_coherence,
    merge_entries,
)
from axm_bib.models import BibEntry

# ── Fixtures ─────────────────────────────────────────────────────────────


def _make_entry(
    key: str = "smith2024deep",
    *,
    doi: str | None = None,
    title: str | None = "Deep Learning for Verification",
    authors: list[str] | None = None,
    year: int | None = 2024,
) -> BibEntry:
    """Build a BibEntry with sensible defaults."""
    return BibEntry(
        key=key,
        entry_type="article",
        raw=f"@article{{{key}, title={{{title}}}}}",
        doi=doi,
        title=title,
        authors=authors or ["John Smith"],
        year=year,
    )


# ── merge_entries tests ─────────────────────────────────────────────────


class TestMergeEntries:
    """merge_entries() tests."""

    def test_merge_two_entries(self) -> None:
        """CrossRef entry + arXiv entry → merged has DOI + eprint."""
        crossref = _make_entry(doi="10.1234/test", title="Deep Learning")
        arxiv = _make_entry(
            key="arxiv2024deep",
            title="Deep Learning for Formal Verification",
        )

        merged = merge_entries([crossref, arxiv])

        assert merged.key == "smith2024deep"  # first entry's key
        assert merged.doi == "10.1234/test"  # from CrossRef
        # Longest title wins
        assert merged.title == "Deep Learning for Formal Verification"
        assert merged.year == 2024

    def test_merge_single_entry(self) -> None:
        """Single entry returns it unchanged."""
        entry = _make_entry(doi="10.1234/test")
        result = merge_entries([entry])

        assert result is entry

    def test_merge_empty_raises(self) -> None:
        """Empty list raises ValueError."""
        with pytest.raises(MergeConflict, match="empty"):
            merge_entries([])

    def test_merge_doi_from_second(self) -> None:
        """DOI from second entry fills gap in first."""
        no_doi = _make_entry(doi=None)
        with_doi = _make_entry(doi="10.5678/test")

        merged = merge_entries([no_doi, with_doi])

        assert merged.doi == "10.5678/test"

    def test_merge_authors_longest_list(self) -> None:
        """Authors list from entry with most authors wins."""
        short = _make_entry(authors=["Smith"])
        long = _make_entry(authors=["Smith", "Jones", "Brown"])

        merged = merge_entries([short, long])

        assert len(merged.authors) == 3

    def test_merge_year_from_any(self) -> None:
        """Year from first non-None entry."""
        no_year = _make_entry(year=None)
        with_year = _make_entry(year=2023)

        merged = merge_entries([no_year, with_year])

        assert merged.year == 2023

    def test_merge_raw_single_entry(self) -> None:
        """arXiv @misc + CrossRef @article → single @ block."""
        a = BibEntry(
            key="a",
            entry_type="misc",
            raw="@misc{a, title={Deep Learning}, eprint={2210.02406}}",
            title="Deep Learning",
            authors=["Smith"],
        )
        b = BibEntry(
            key="b",
            entry_type="article",
            raw="@article{b, title={Deep Learning}, doi={10.1234/x}}",
            title="Deep Learning",
            authors=["Smith"],
        )
        merged = merge_entries([a, b])
        # AC1: exactly one @type{...} block
        assert merged.raw.count("@") == 1

    def test_merge_raw_preserves_eprint(self) -> None:
        """arXiv entry with eprint → eprint in merged raw."""
        arxiv = BibEntry(
            key="a",
            entry_type="misc",
            raw="@misc{a, title={Paper}, eprint={2210.02406}}",
            title="Paper",
            authors=["Smith"],
        )
        crossref = BibEntry(
            key="b",
            entry_type="article",
            raw="@article{b, title={Paper}, doi={10.1234/x}}",
            title="Paper",
            authors=["Smith"],
        )
        merged = merge_entries([arxiv, crossref])
        assert "eprint" in merged.raw
        assert "2210.02406" in merged.raw

    def test_merge_raw_preserves_doi(self) -> None:
        """CrossRef with DOI, arXiv without → doi in merged raw."""
        arxiv = BibEntry(
            key="a",
            entry_type="misc",
            raw="@misc{a, title={Paper}, eprint={2210.02406}}",
            title="Paper",
            authors=["Smith"],
        )
        crossref = BibEntry(
            key="b",
            entry_type="article",
            raw="@article{b, title={Paper}, doi={10.1234/x}}",
            title="Paper",
            authors=["Smith"],
        )
        merged = merge_entries([arxiv, crossref])
        assert "doi" in merged.raw
        assert "10.1234/x" in merged.raw

    def test_merge_raw_type_promotion(self) -> None:
        """@misc + @inproceedings → raw starts with @inproceedings."""
        misc = BibEntry(
            key="a",
            entry_type="misc",
            raw="@misc{a, title={Paper}}",
            title="Paper",
            authors=["Smith"],
        )
        conf = BibEntry(
            key="b",
            entry_type="inproceedings",
            raw="@inproceedings{b, title={Paper}, booktitle={ICLR}}",
            title="Paper",
            authors=["Smith"],
        )
        merged = merge_entries([misc, conf])
        assert merged.raw.startswith("@inproceedings{")
        assert merged.entry_type == "inproceedings"

    def test_merge_single_identity(self) -> None:
        """Single entry → raw unchanged (identity merge)."""
        entry = BibEntry(
            key="a",
            entry_type="article",
            raw="@article{a, title={Paper}}",
            title="Paper",
            authors=["Smith"],
        )
        result = merge_entries([entry])
        assert result.raw == entry.raw

    def test_merge_duplicate_fields_keeps_base(self) -> None:
        """Both entries have title field → base version kept."""
        a = BibEntry(
            key="a",
            entry_type="article",
            raw="@article{a, title={Base Title}}",
            title="Base Title",
            authors=["Smith"],
        )
        b = BibEntry(
            key="b",
            entry_type="misc",
            raw="@misc{b, title={Other Title}, doi={10.1/x}}",
            title="Other Title",
            authors=["Smith"],
        )
        merged = merge_entries([a, b])
        # title field from base preserved, doi from b injected
        assert "Base Title" in merged.raw
        assert "doi" in merged.raw

    def test_merge_malformed_raw_fallback(self) -> None:
        """Entry with no closing brace → graceful fallback."""
        a = BibEntry(
            key="a",
            entry_type="article",
            raw="@article a title Deep Learning",  # no braces at all
            title="Paper",
            authors=["Smith"],
        )
        b = BibEntry(
            key="b",
            entry_type="misc",
            raw="@misc{b, title={Paper}}",
            title="Paper",
            authors=["Smith"],
        )
        merged = merge_entries([a, b])
        # Fallback: concatenated (both present)
        assert "@article a title" in merged.raw
        assert "@misc{b" in merged.raw

    def test_merge_three_entries(self) -> None:
        """arXiv + CrossRef + DBLP → single entry with all fields."""
        arxiv = BibEntry(
            key="a",
            entry_type="misc",
            raw="@misc{a, title={Paper}, eprint={2210.02406}}",
            title="Paper",
            authors=["Smith"],
        )
        crossref = BibEntry(
            key="b",
            entry_type="article",
            raw="@article{b, title={Paper}, doi={10.1234/x}}",
            title="Paper",
            authors=["Smith"],
        )
        dblp = BibEntry(
            key="c",
            entry_type="inproceedings",
            raw=("@inproceedings{c, title={Paper}, booktitle={ICLR}}"),
            title="Paper",
            authors=["Smith"],
        )
        merged = merge_entries([arxiv, crossref, dblp])
        assert merged.raw.count("@") == 1
        assert "eprint" in merged.raw
        assert "doi" in merged.raw
        assert "booktitle" in merged.raw
        assert merged.entry_type == "article"  # article > inproceedings

    def test_merge_preprint_to_published(self) -> None:
        """arXiv preprint + conference → merge works."""
        arxiv = _make_entry(
            title="Attention Is All You Need",
            authors=["Vaswani, Ashish"],
            doi=None,
        )
        crossref = _make_entry(
            title="Attention is all you need",
            authors=["Ashish Vaswani"],
            doi="10.5555/3295222.3295349",
        )
        merged = merge_entries([arxiv, crossref])
        assert merged.doi == "10.5555/3295222.3295349"
        assert merged.title is not None


# ── check_coherence tests ───────────────────────────────────────────────


class TestCheckCoherence:
    """check_coherence() tests."""

    def test_coherence_pass(self) -> None:
        """Same title, same author → True."""
        a = _make_entry(title="Deep Learning for Verification", authors=["John Smith"])
        b = _make_entry(
            title="Deep Learning for Verification",
            authors=["Smith, John"],
        )

        assert check_coherence([a, b]) is True

    def test_coherence_fail_title(self) -> None:
        """Different titles (ratio < 0.8) → False."""
        a = _make_entry(title="Deep Learning for Verification")
        b = _make_entry(title="Quantum Computing in Finance")

        assert check_coherence([a, b]) is False

    def test_coherence_fail_author(self) -> None:
        """Same title, different 1st author surname → False."""
        a = _make_entry(
            title="Deep Learning for Verification",
            authors=["John Smith"],
        )
        b = _make_entry(
            title="Deep Learning for Verification",
            authors=["Jane Doe"],
        )

        assert check_coherence([a, b]) is False

    def test_coherence_single_entry(self) -> None:
        """Single entry is always coherent."""
        a = _make_entry()
        assert check_coherence([a]) is True

    def test_coherence_similar_titles(self) -> None:
        """Slightly different titles (ratio ≥ 0.8) → True."""
        a = _make_entry(
            title="Attention Is All You Need",
            authors=["Vaswani, Ashish"],
        )
        b = _make_entry(
            title="Attention is All You Need",
            authors=["Ashish Vaswani"],
        )

        assert check_coherence([a, b]) is True


# ── Private helpers tests ────────────────────────────────────────────────


class TestNormalizeTitle:
    """_normalize_title() tests."""

    def test_lowercase_and_strip(self) -> None:
        assert _normalize_title("  Deep Learning  ") == "deep learning"

    def test_remove_punctuation(self) -> None:
        assert _normalize_title("Hello, World!") == "hello world"

    def test_unicode(self) -> None:
        """Unicode characters are preserved."""
        result = _normalize_title("Réseau neuronal")
        assert "seau" in result


class TestExtractSurname:
    """_extract_surname() tests."""

    def test_last_first_format(self) -> None:
        assert _extract_surname("Smith, John") == "Smith"

    def test_first_last_format(self) -> None:
        assert _extract_surname("John Smith") == "Smith"

    def test_first_middle_last(self) -> None:
        assert _extract_surname("John Q. Smith") == "Smith"

    def test_empty_string(self) -> None:
        assert _extract_surname("") == ""

    def test_unicode_author(self) -> None:
        """Unicode author names handled."""
        assert _extract_surname("Florian Tramèr") == "Tramèr"

    def test_single_name(self) -> None:
        assert _extract_surname("Vaswani") == "Vaswani"
