import pytest
from ebooklib import epub
from pathlib import Path
from datetime import date
from chitai.services.metadata_extractor import (
    EpubExtractor,
    Extractor,
    PdfExtractor,
    split_edition,
)


@pytest.mark.asyncio()
class TestEpubExtractor:
    async def test_extraction_by_path(self):
        path = Path("tests/data_files/Moby Dick; Or, The Whale - Herman Melville.epub")

        metadata = await EpubExtractor.extract_metadata(path)

        assert metadata["title"] == "Moby Dick; Or, The Whale"
        assert metadata["authors"] == ["Herman Melville"]
        assert metadata["published_date"] == date(year=2001, month=7, day=1)


EPUB = Path("tests/data_files/Metamorphosis - Franz Kafka.epub")
PDF = Path("tests/data_files/Calculus Made Easy - Silvanus Thompson.pdf")


@pytest.mark.asyncio()
class TestIdentifierMerging:
    """A book's formats each contribute identifiers; none of them replaces the rest."""

    async def test_every_format_contributes(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """
        Identifiers are a collection, not a single value.

        Merging the whole dict meant the last format to report won outright: an EPUB
        declaring an ASIN, a Google volume id and a Calibre id kept none of them once
        a PDF contributed one ISBN.
        """

        async def epub(_file):
            return {
                "title": "How Linux Works",
                "identifiers": {"isbn-13": "9781718500419", "asin": "1718500408"},
            }

        async def pdf(_file):
            return {
                "identifiers": {"isbn-13": "9781718500402", "isbn-10": "1593270356"}
            }

        monkeypatch.setattr(EpubExtractor, "extract_metadata", epub)
        monkeypatch.setattr(PdfExtractor, "extract_metadata", pdf)

        metadata = await Extractor.extract_metadata(
            [Path("How Linux Works.epub"), Path("How Linux Works.pdf")]
        )

        assert metadata["identifiers"] == {
            # Declared by the publisher's toolchain, so it outranks the PDF's, which
            # was scraped off a copyright page that also prints the print edition's.
            "isbn-13": "9781718500419",
            "asin": "1718500408",
            "isbn-10": "1593270356",
        }

    async def test_a_second_format_that_finds_nothing_erases_nothing(self) -> None:
        """The PDF fixture carries no ISBN, so it must leave the EPUB's alone."""
        metadata = await Extractor.extract_metadata([EPUB, PDF])

        assert metadata["identifiers"] == {"id": "http://www.gutenberg.org/5200"}

    async def test_one_format_on_its_own_is_unaffected(self) -> None:
        metadata = await Extractor.extract_metadata([EPUB])

        assert metadata["identifiers"] == {"id": "http://www.gutenberg.org/5200"}

    async def test_no_identifiers_anywhere_leaves_the_field_absent(self) -> None:
        """An empty dict would count as extracted metadata and overwrite nothing."""
        metadata = await Extractor.extract_metadata([PDF])

        assert "identifiers" not in metadata


class TestSplitEdition:
    """An edition is a field on the book, not part of what the book is called."""

    @pytest.mark.parametrize(
        ("title", "stripped", "edition"),
        [
            # Every form below is one that turned up in a real library.
            ("Fluent Python, 2nd Edition", "Fluent Python", 2),
            ("Building Microservices, 2E", "Building Microservices", 2),
            ("Digital Image Processing, 4e", "Digital Image Processing", 4),
            (
                "Network Security Essentials: Applications and Standards/6e",
                "Network Security Essentials: Applications and Standards",
                6,
            ),
            (
                "Refactoring: Improving the Design of Existing Code (2nd edition)",
                "Refactoring: Improving the Design of Existing Code",
                2,
            ),
            # Ordinal words, including a qualifier sitting inside the statement.
            (
                "The Art of Computer Programming: Volume 1 / Fundamental Algorithms, Third Edition",
                "The Art of Computer Programming: Volume 1 / Fundamental Algorithms",
                3,
            ),
            (
                "Introduction to the Theory of Computation, Third International Edition",
                "Introduction to the Theory of Computation",
                3,
            ),
            # Mid-title, before a subtitle and before a trailing author.
            (
                "How Linux Works, 3rd Edition: What Every Superuser Should Know",
                "How Linux Works: What Every Superuser Should Know",
                3,
            ),
            (
                "Code Complete, 2nd Edition - Steve McConnell",
                "Code Complete - Steve McConnell",
                2,
            ),
            # An underscore between the number and the "e", beside an unnumbered
            # qualifier that has nowhere to go in an integer column and so stays put.
            (
                "Cryptography and Network Security, Global Edition, 8_e - Stallings",
                "Cryptography and Network Security, Global Edition - Stallings",
                8,
            ),
        ],
    )
    def test_editions_are_split_out(
        self, title: str, stripped: str, edition: int
    ) -> None:
        assert split_edition(title) == (stripped, edition)

    @pytest.mark.parametrize(
        "title",
        [
            # A number alone is never an edition — these are titles.
            "Catch 22",
            "Fahrenheit 451",
            "Blade Runner 2049",
            "1984",
            "Apollo 13",
            "Slaughterhouse 5",
            "The Art of Computer Programming: Volume 1",
            # "Edition" with no number cannot be stored, so it stays where it can
            # still be read.
            "Cryptography and Network Security: Principles and Practice, Global Edition",
            "Building Microservices",
        ],
    )
    def test_titles_are_left_alone(self, title: str) -> None:
        assert split_edition(title) == (title, None)

    def test_a_title_that_is_only_an_edition_is_kept(self) -> None:
        """Stripping must never leave a book with no title at all."""
        assert split_edition("2nd Edition") == ("2nd Edition", None)

    @pytest.mark.parametrize("title", ["", None])
    def test_nothing_yields_nothing(self, title: str | None) -> None:
        assert split_edition(title) == (title, None)


@pytest.mark.asyncio()
class TestEditionFromFiles:
    async def test_extraction_moves_the_edition_off_the_title(self) -> None:
        """The PDF fixture calls itself a 2nd edition in its own metadata title."""
        metadata = await Extractor.extract_metadata([PDF])

        assert (
            metadata["title"]
            == "The Project Gutenberg eBook #33283: Calculus Made Easy"
        )
        assert metadata["edition"] == 2


class TestEpubPublisher:
    """The publisher was looked up and then dropped on the floor."""

    def test_a_declared_publisher_is_returned(self) -> None:
        """
        The lookup discarded its own result and fell off the end of the function, so
        every EPUB reported no publisher no matter what it said.
        """
        book = epub.EpubBook()
        book.add_metadata("DC", "publisher", "No Starch Press")

        assert EpubExtractor._extract_publisher(book) == "No Starch Press"

    def test_no_publisher_is_none(self) -> None:
        assert EpubExtractor._extract_publisher(epub.EpubBook()) is None
