"""TDD tests for axm_bib.models — written BEFORE implementation."""

from __future__ import annotations

import re

import pytest
from pydantic import ValidationError

from axm_bib.models import BibEntry, DownloadResult, ExtractionResult, FigureInfo, Paper


class TestPaper:
    """Paper model validation."""

    def test_paper_minimal(self) -> None:
        """Minimal valid Paper: title, authors (1+), source."""
        p = Paper(title="Title", authors=["Author"], source="crossref")
        assert p.title == "Title"
        assert p.authors == ["Author"]
        assert p.source == "crossref"

    def test_paper_full_fields(self) -> None:
        """All optional fields populate correctly."""
        p = Paper(
            title="Deep Learning",
            authors=["John Smith", "Jane Doe"],
            year=2024,
            doi="10.1145/123",
            arxiv_id="2401.00001",
            abstract="An abstract.",
            venue="KDD",
            citation_count=42,
            open_access_pdf="https://example.com/paper.pdf",
            source="semantic_scholar",
        )
        assert p.year == 2024
        assert p.doi == "10.1145/123"
        assert p.arxiv_id == "2401.00001"
        assert p.abstract == "An abstract."
        assert p.venue == "KDD"
        assert p.citation_count == 42
        assert p.open_access_pdf == "https://example.com/paper.pdf"

    def test_paper_optional_fields_default_none(self) -> None:
        """Optional fields default to None."""
        p = Paper(title="T", authors=["A"], source="crossref")
        assert p.year is None
        assert p.doi is None
        assert p.arxiv_id is None
        assert p.abstract is None
        assert p.venue is None
        assert p.citation_count is None
        assert p.open_access_pdf is None

    def test_paper_empty_authors_rejected(self) -> None:
        """authors must have at least 1 entry."""
        with pytest.raises(ValidationError):
            Paper(title="T", authors=[], source="crossref")

    def test_paper_serialization_roundtrip(self, sample_paper: Paper) -> None:
        """model_dump → model_validate is identity."""
        data = sample_paper.model_dump()
        reconstructed = Paper.model_validate(data)
        assert reconstructed == sample_paper

    def test_paper_json_roundtrip(self, sample_paper: Paper) -> None:
        """JSON serialize → deserialize is identity."""
        json_str = sample_paper.model_dump_json()
        reconstructed = Paper.model_validate_json(json_str)
        assert reconstructed == sample_paper


class TestBibEntry:
    """BibEntry model validation."""

    def test_bibentry_minimal(self) -> None:
        """Minimal valid BibEntry."""
        b = BibEntry(key="smith2024deep", entry_type="article", raw="@article{...}")
        assert b.key == "smith2024deep"
        assert b.entry_type == "article"
        assert b.raw == "@article{...}"

    def test_bibentry_key_format(self) -> None:
        """Key matches the smith2024deep pattern."""
        b = BibEntry(key="smith2024deep", entry_type="article", raw="raw")
        assert re.match(r"^[a-z]+\d{4}[a-z]+$", b.key)

    def test_bibentry_raw_preserved(self, sample_bib_entry: BibEntry) -> None:
        """raw field stores exact BibTeX string verbatim."""
        assert "@inproceedings{smith2024deep" in sample_bib_entry.raw
        assert "doi = {10.1145/3292500.3330919}" in sample_bib_entry.raw

    def test_bibentry_optional_fields(self) -> None:
        """Optional fields default properly."""
        b = BibEntry(key="k", entry_type="article", raw="r")
        assert b.doi is None
        assert b.title is None
        assert b.authors == []
        assert b.year is None

    def test_bibentry_serialization_roundtrip(self, sample_bib_entry: BibEntry) -> None:
        """model_dump → model_validate is identity."""
        data = sample_bib_entry.model_dump()
        reconstructed = BibEntry.model_validate(data)
        assert reconstructed == sample_bib_entry


class TestDownloadResult:
    """DownloadResult model validation."""

    def test_download_result_fields(self) -> None:
        """All fields set correctly."""
        d = DownloadResult(
            doi="10.1145/123",
            path="/abs/path/paper.pdf",
            size_bytes=2048,
            is_open_access=True,
        )
        assert d.doi == "10.1145/123"
        assert d.path == "/abs/path/paper.pdf"
        assert d.size_bytes == 2048
        assert d.is_open_access is True

    def test_download_result_zero_size(self) -> None:
        """size_bytes=0 is valid (empty/error case)."""
        d = DownloadResult(
            doi="10.1145/123", path="/p.pdf", size_bytes=0, is_open_access=False
        )
        assert d.size_bytes == 0


class TestSchemaStrictness:
    """Verify extra='forbid' and Literal source type."""

    def test_paper_extra_field_rejected(self) -> None:
        """Paper rejects unexpected extra fields."""
        with pytest.raises(ValidationError, match="extra_field"):
            Paper(
                title="T",
                authors=["A"],
                source="semantic_scholar",
                extra_field="boom",  # type: ignore[call-arg]
            )

    def test_paper_invalid_source_rejected(self) -> None:
        """Paper.source rejects unknown source values."""
        with pytest.raises(ValidationError, match="source"):
            Paper(title="T", authors=["A"], source="google_scholar")  # type: ignore[arg-type]

    def test_paper_valid_sources(self) -> None:
        """Both 'semantic_scholar' and 'crossref' are valid."""
        p1 = Paper(title="T", authors=["A"], source="semantic_scholar")
        p2 = Paper(title="T", authors=["A"], source="crossref")
        assert p1.source == "semantic_scholar"
        assert p2.source == "crossref"

    def test_bibentry_extra_field_rejected(self) -> None:
        """BibEntry rejects unexpected extra fields."""
        with pytest.raises(ValidationError, match="extra_field"):
            BibEntry(
                key="k",
                entry_type="article",
                raw="@article{k}",
                extra_field="x",  # type: ignore[call-arg]
            )

    def test_download_result_extra_field_rejected(self) -> None:
        """DownloadResult rejects unexpected extra fields."""
        with pytest.raises(ValidationError, match="extra_field"):
            DownloadResult(
                doi="10.1/x",
                path="/p.pdf",
                size_bytes=100,
                is_open_access=True,
                extra_field="x",  # type: ignore[call-arg]
            )

    def test_figure_info_extra_field_rejected(self) -> None:
        """FigureInfo rejects unexpected extra fields."""
        with pytest.raises(ValidationError, match="extra_field"):
            FigureInfo(
                index=1,
                filename="f.png",
                page=0,
                width=100,
                height=100,
                xref=1,
                extra_field="x",  # type: ignore[call-arg]
            )

    def test_extraction_result_extra_field_rejected(self) -> None:
        """ExtractionResult rejects unexpected extra fields."""
        with pytest.raises(ValidationError, match="extra_field"):
            ExtractionResult(
                source_pdf="/p.pdf",
                output_dir="/o",
                markdown_path="/o/c.md",
                page_count=1,
                word_count=10,
                char_count=60,
                extra_field="x",  # type: ignore[call-arg]
            )
