"""Phase 1.5.5a: PDF page extractor tests.

Tests for extract_page_as_pdf() and extract_pages_as_pdf() which extract
individual pages from a PDF and save them as separate single-page PDF files.

These extracted "chunk PDFs" serve as clickable source references in RAG responses.
"""
from io import BytesIO
from pathlib import Path

import pytest
from pypdf import PdfReader

from app.utils.pdf_extractor import extract_page_as_pdf, extract_pages_as_pdf


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _create_multipage_pdf(path: Path, page_texts: list[str]) -> None:
    """Create a multi-page PDF at *path* with the given text per page."""
    from reportlab.pdfgen import canvas

    buf = BytesIO()
    c = canvas.Canvas(buf)
    for text in page_texts:
        c.drawString(72, 720, text)
        c.showPage()
    c.save()
    path.write_bytes(buf.getvalue())


def _page_count(pdf_path: str) -> int:
    """Return the number of pages in a PDF file."""
    return len(PdfReader(pdf_path).pages)


def _page_text(pdf_path: str, page_index: int = 0) -> str:
    """Return extracted text from a single page (0-indexed)."""
    reader = PdfReader(pdf_path)
    return reader.pages[page_index].extract_text() or ""


# ---------------------------------------------------------------------------
# Fixture
# ---------------------------------------------------------------------------

@pytest.fixture()
def multipage_pdf(tmp_path: Path) -> str:
    """3-page PDF saved to tmp, returns its string path."""
    pdf_path = tmp_path / "source.pdf"
    _create_multipage_pdf(pdf_path, ["Alpha", "Bravo", "Charlie"])
    return str(pdf_path)


# ---------------------------------------------------------------------------
# extract_page_as_pdf
# ---------------------------------------------------------------------------

class TestExtractPageAsPdf:
    """Tests for extract_page_as_pdf()."""

    def test_valid_page_creates_single_page_pdf(self, tmp_path: Path, multipage_pdf: str):
        """Extracting page 2 produces a PDF with exactly 1 page."""
        out = str(tmp_path / "out.pdf")
        result = extract_page_as_pdf(multipage_pdf, 2, out)
        assert result == out
        assert _page_count(out) == 1

    def test_page_content_matches_source(self, tmp_path: Path, multipage_pdf: str):
        """Extracted page 1 contains 'Alpha', page 2 contains 'Bravo', etc."""
        out = str(tmp_path / "page1.pdf")
        extract_page_as_pdf(multipage_pdf, 1, out)
        assert "Alpha" in _page_text(out, 0)

        out2 = str(tmp_path / "page2.pdf")
        extract_page_as_pdf(multipage_pdf, 2, out2)
        assert "Bravo" in _page_text(out2, 0)

        out3 = str(tmp_path / "page3.pdf")
        extract_page_as_pdf(multipage_pdf, 3, out3)
        assert "Charlie" in _page_text(out3, 0)

    def test_page_out_of_range_raises_value_error(self, tmp_path: Path, multipage_pdf: str):
        """Page number beyond PDF length raises ValueError."""
        out = str(tmp_path / "never.pdf")
        with pytest.raises(ValueError, match="out of range"):
            extract_page_as_pdf(multipage_pdf, 99, out)

    def test_page_zero_raises_value_error(self, tmp_path: Path, multipage_pdf: str):
        """Page number 0 (pages are 1-indexed) raises ValueError."""
        out = str(tmp_path / "never.pdf")
        with pytest.raises(ValueError, match="out of range"):
            extract_page_as_pdf(multipage_pdf, 0, out)

    def test_negative_page_raises_value_error(self, tmp_path: Path, multipage_pdf: str):
        """Negative page number raises ValueError."""
        out = str(tmp_path / "never.pdf")
        with pytest.raises(ValueError, match="out of range"):
            extract_page_as_pdf(multipage_pdf, -1, out)

    def test_nonexistent_source_raises_file_not_found(self, tmp_path: Path):
        """Missing source file raises FileNotFoundError."""
        out = str(tmp_path / "out.pdf")
        with pytest.raises(FileNotFoundError):
            extract_page_as_pdf("/no/such/file.pdf", 1, out)

    def test_invalid_pdf_raises_value_error(self, tmp_path: Path):
        """Corrupt / non-PDF content raises ValueError."""
        bad = tmp_path / "bad.pdf"
        bad.write_bytes(b"not a pdf")
        out = str(tmp_path / "out.pdf")
        with pytest.raises(ValueError, match="Invalid PDF"):
            extract_page_as_pdf(str(bad), 1, out)


# ---------------------------------------------------------------------------
# extract_pages_as_pdf
# ---------------------------------------------------------------------------

class TestExtractPagesAsPdf:
    """Tests for extract_pages_as_pdf()."""

    def test_multiple_pages_creates_correct_files(
        self, tmp_path: Path, multipage_pdf: str
    ):
        """Extracting pages [1, 3] from a 3-page PDF creates 2 files."""
        out_dir = str(tmp_path / "pages")
        result = extract_pages_as_pdf(multipage_pdf, [1, 3], out_dir, "doc")
        assert len(result) == 2
        assert _page_count(str(Path(out_dir) / result[0])) == 1
        assert _page_count(str(Path(out_dir) / result[1])) == 1

    def test_naming_convention(self, tmp_path: Path, multipage_pdf: str):
        """Output files follow {stem}_page_{n}.pdf naming."""
        out_dir = str(tmp_path / "pages")
        result = extract_pages_as_pdf(
            multipage_pdf, [1, 3], out_dir, "NEC4 ACC"
        )
        assert "NEC4 ACC_page_1.pdf" in result
        assert "NEC4 ACC_page_3.pdf" in result

    def test_empty_page_numbers_returns_empty(self, tmp_path: Path, multipage_pdf: str):
        """Empty page_numbers list returns empty list and creates no files."""
        out_dir = str(tmp_path / "pages")
        result = extract_pages_as_pdf(multipage_pdf, [], out_dir, "doc")
        assert result == []
        # output dir may or may not be created for empty input — both are fine

    def test_creates_output_dir_if_missing(self, tmp_path: Path, multipage_pdf: str):
        """Function creates output_dir when it does not yet exist."""
        out_dir = str(tmp_path / "nested" / "deep" / "dir")
        assert not Path(out_dir).exists()
        result = extract_pages_as_pdf(multipage_pdf, [2], out_dir, "test")
        assert Path(out_dir).is_dir()
        assert len(result) == 1

    def test_returns_relative_filenames(self, tmp_path: Path, multipage_pdf: str):
        """Returned paths are just filenames, not full paths."""
        out_dir = str(tmp_path / "pages")
        result = extract_pages_as_pdf(multipage_pdf, [1, 2], out_dir, "mydoc")
        for name in result:
            assert Path(name).name == name  # no directory components

    def test_out_of_range_page_raises_value_error(
        self, tmp_path: Path, multipage_pdf: str
    ):
        """Any page number out of range raises ValueError."""
        out_dir = str(tmp_path / "pages")
        with pytest.raises(ValueError, match="out of range"):
            extract_pages_as_pdf(multipage_pdf, [1, 50], out_dir, "doc")

    def test_nonexistent_source_raises_file_not_found(self, tmp_path: Path):
        """Missing source file raises FileNotFoundError."""
        out_dir = str(tmp_path / "pages")
        with pytest.raises(FileNotFoundError):
            extract_pages_as_pdf("/no/such/file.pdf", [1], out_dir, "doc")

    def test_invalid_pdf_raises_value_error(self, tmp_path: Path):
        """Corrupt source raises ValueError."""
        bad = tmp_path / "bad.pdf"
        bad.write_bytes(b"garbage")
        out_dir = str(tmp_path / "pages")
        with pytest.raises(ValueError, match="Invalid PDF"):
            extract_pages_as_pdf(str(bad), [1], out_dir, "doc")

    def test_extracted_content_correct(self, tmp_path: Path, multipage_pdf: str):
        """Each extracted file contains the right page text."""
        out_dir = str(tmp_path / "pages")
        result = extract_pages_as_pdf(
            multipage_pdf, [1, 3], out_dir, "content_test"
        )
        p1 = str(Path(out_dir) / result[0])
        p3 = str(Path(out_dir) / result[1])
        assert "Alpha" in _page_text(p1, 0)
        assert "Charlie" in _page_text(p3, 0)
