# tests/operations/test_set_props.py

import logging
from unittest.mock import PropertyMock, patch

import pikepdf
import pytest

from pdftl.exceptions import InvalidArgumentError, OperationError
from pdftl.operations.set_props import set_props


@pytest.fixture
def blank_pdf():
    """Provides a 10-page blank PDF for testing page labels and open actions."""
    pdf = pikepdf.Pdf.new()
    for _ in range(10):
        pdf.add_blank_page()
    return pdf


def test_set_props_missing_args(blank_pdf):
    with pytest.raises(OperationError, match="requires at least one key=value pair"):
        set_props(blank_pdf, [])


def test_set_props_invalid_arg_format(blank_pdf):
    with pytest.raises(InvalidArgumentError, match="missing '=' in argument"):
        set_props(blank_pdf, ["lang_en-US"])


def test_set_props_metadata_legacy_docinfo(blank_pdf):
    """Tests that legacy /Info gets populated for PDF 1.x documents."""
    with patch("pikepdf.Pdf.pdf_version", new_callable=PropertyMock, return_value="1.7"):
        op_args = ["title=My Title", "trapped=true", "author=Jane Doe, John Smith"]
        res = set_props(blank_pdf, op_args)

        assert res.success is True
        assert str(blank_pdf.docinfo["/Title"]) == "My Title"
        assert str(blank_pdf.docinfo["/Trapped"]) == "/True"
        assert str(blank_pdf.docinfo["/Author"]) == "Jane Doe, John Smith"


def test_set_props_metadata_pdf20_deprecation(blank_pdf):
    """Tests that PDF 2.0 docs only get ModDate/CreationDate in /Info."""
    with patch("pikepdf.Pdf.pdf_version", new_callable=PropertyMock, return_value="2.0"):
        op_args = ["title=My Title", "moddate=2026-01-01", "trapped=false"]
        set_props(blank_pdf, op_args)

        # ISO 32000-2 deprecated these in the DocInfo dictionary
        assert "/Title" not in blank_pdf.docinfo
        assert "/Trapped" not in blank_pdf.docinfo
        # ModDate is explicitly exempt from deprecation
        assert "/ModDate" in blank_pdf.docinfo


def test_set_props_xmp_updates(blank_pdf):
    """Tests that XMP properties are updated correctly, including sequences."""
    set_props(blank_pdf, ["author=Jane, John", "trapped=Unknown"])

    with blank_pdf.open_metadata() as meta:
        assert meta["dc:creator"] == ["Jane", "John"]
        assert meta["pdf:Trapped"] == "Unknown"


def test_set_props_trapped_invalid(blank_pdf):
    """Tests that invalid trapped values are rejected."""
    with pytest.raises(OperationError, match="trapped must be True, False, or Unknown"):
        set_props(blank_pdf, ["trapped=yes"])


def test_set_props_standard_properties(blank_pdf):
    op_args = [" LANG = fr-FR ", "layout=TwoColumnRight", "mode=UseOutlines"]
    res = set_props(blank_pdf, op_args)
    assert res.success is True
    assert str(blank_pdf.Root.Lang) == "fr-FR"
    assert str(blank_pdf.Root.PageLayout) == "/TwoColumnRight"
    assert str(blank_pdf.Root.PageMode) == "/UseOutlines"


def test_set_props_viewer_prefs_valid(blank_pdf):
    op_args = [
        "hide_toolbar=true",
        "hide_menubar=1",
        "display_title=yes",
        "fit_window=false",
        "center_window=0",
        "hide_windowui=no",
    ]
    set_props(blank_pdf, op_args)
    prefs = blank_pdf.Root.ViewerPreferences
    assert bool(prefs.HideToolbar) is True
    assert bool(prefs.HideMenubar) is True
    assert bool(prefs.DisplayDocTitle) is True
    assert bool(prefs.FitWindow) is False
    assert bool(prefs.CenterWindow) is False
    assert bool(prefs.HideWindowUI) is False


def test_set_props_viewer_prefs_existing(blank_pdf):
    blank_pdf.Root.ViewerPreferences = pikepdf.Dictionary(FitWindow=False)
    set_props(blank_pdf, ["center_window=true"])
    prefs = blank_pdf.Root.ViewerPreferences
    assert bool(prefs.FitWindow) is False
    assert bool(prefs.CenterWindow) is True


def test_set_props_viewer_prefs_invalid(blank_pdf):
    with pytest.raises(OperationError, match="Invalid boolean value for hide_toolbar: 'banana'"):
        set_props(blank_pdf, ["hide_toolbar=banana"])


def test_set_props_open_action_simple(blank_pdf):
    """Tests the simplified open_action pointing to just a page number."""
    set_props(blank_pdf, ["open_action=5"])

    action = blank_pdf.Root.OpenAction
    # Ensure it resolves to the 5th page object (index 4) and defaults to /XYZ with nulls
    assert action[0] == blank_pdf.pages[4].obj
    assert str(action[1]) == "/XYZ"
    assert action[2] is None
    assert action[3] is None
    assert action[4] is None


def test_set_props_open_action_complex(blank_pdf):
    """Tests an open_action with a complex view mapping, handling nulls and floats."""
    set_props(blank_pdf, ["open_action=2,XYZ,null,15.5,2"])

    action = blank_pdf.Root.OpenAction
    assert action[0] == blank_pdf.pages[1].obj
    assert str(action[1]) == "/XYZ"
    assert action[2] is None
    assert action[3] == 15.5
    assert action[4] == 2


def test_set_props_open_action_auto_pad(blank_pdf):
    """Tests that view types with missing arguments are automatically padded with nulls."""
    # FitR expects 4 coordinates (left, bottom, right, top). We'll only provide 1.
    set_props(blank_pdf, ["open_action=5,FitR,10"])

    action = blank_pdf.Root.OpenAction
    assert action[0] == blank_pdf.pages[4].obj
    assert str(action[1]) == "/FitR"
    assert action[2] == 10
    # The remaining 3 should be padded with nulls
    assert action[3] is None
    assert action[4] is None
    assert action[5] is None


def test_set_props_open_action_clear(blank_pdf):
    """Tests safely removing the open_action."""
    set_props(blank_pdf, ["open_action=1"])
    assert "/OpenAction" in blank_pdf.Root

    set_props(blank_pdf, ["open_action="])
    assert "/OpenAction" not in blank_pdf.Root


def test_set_props_open_action_errors(blank_pdf):
    """Tests various OpenAction failure modes."""
    # Out of bounds
    with pytest.raises(OperationError, match="out of bounds"):
        set_props(blank_pdf, ["open_action=99"])

    # Bad page number
    with pytest.raises(OperationError, match="must start with a valid page number"):
        set_props(blank_pdf, ["open_action=banana,XYZ"])

    # Bad argument cast
    with pytest.raises(OperationError, match="Invalid open_action argument: bad_zoom"):
        set_props(blank_pdf, ["open_action=1,XYZ,null,null,bad_zoom"])


def test_set_props_pagelabels_parsing(blank_pdf):
    op_args = ["pagelabels=1r,, 5(Chap )D, 8(A-)D8, 10(Cover) ,"]
    set_props(blank_pdf, op_args)

    nt = pikepdf.NumberTree(blank_pdf.Root.PageLabels)
    assert str(nt[0].S) == "/r"
    assert nt[0].St == 1
    assert "/P" not in nt[0]

    assert str(nt[4].P) == "Chap "
    assert str(nt[4].S) == "/D"
    assert nt[4].St == 1

    assert str(nt[7].P) == "A-"
    assert str(nt[7].S) == "/D"
    assert nt[7].St == 8

    assert str(nt[9].P) == "Cover"
    assert "/S" not in nt[9]
    assert "/St" not in nt[9]


def test_set_props_pagelabels_invalid_syntax(blank_pdf):
    with pytest.raises(OperationError, match="Invalid page label syntax: 'invalid_stuff'"):
        set_props(blank_pdf, ["pagelabels=1r,invalid_stuff,5D"])


def test_set_props_pagelabels_clear(blank_pdf):
    assert "/PageLabels" not in blank_pdf.Root
    set_props(blank_pdf, ["pagelabels="])
    assert "/PageLabels" not in blank_pdf.Root

    set_props(blank_pdf, ["pagelabels=1r"])
    assert "/PageLabels" in blank_pdf.Root

    set_props(blank_pdf, ["pagelabels=   "])
    assert "/PageLabels" not in blank_pdf.Root


def test_set_props_metadata_legacy_date_format(blank_pdf):
    """Tests parsing of a legacy PDF date format (D:...). (Coverage for line 188)"""
    set_props(blank_pdf, ["creationdate=D:20260101120000Z"])
    # pikepdf natively formats this back to a standard string
    assert str(blank_pdf.docinfo["/CreationDate"]).startswith("D:20260101")


def test_set_props_metadata_backfill_error(blank_pdf, caplog):
    """Tests that backfilling gracefully catches format errors from existing docinfo. (Coverage for lines 264-267)"""
    caplog.set_level(logging.DEBUG)
    # Inject a malformed date into the original docinfo
    blank_pdf.docinfo["/ModDate"] = "InvalidDateString"
    # Trigger metadata update
    set_props(blank_pdf, ["title=New Title"])
    # Assert that the backfilling gracefully failed and logged the debug message
    assert "Failed to backfill xmp:ModifyDate" in caplog.text


def test_set_props_metadata_apply_error(blank_pdf):
    """Tests that general metadata extraction/update errors are caught and re-raised. (Coverage for lines 294-295)"""
    # Mock open_metadata to raise a PdfError, simulating corrupted XMP metadata
    with patch.object(
        blank_pdf, "open_metadata", side_effect=pikepdf.PdfError("Corrupted XMP stream")
    ):
        with pytest.raises(
            OperationError, match="Failed to set document metadata: Corrupted XMP stream"
        ):
            set_props(blank_pdf, ["title=Another Title"])
