from unittest.mock import MagicMock, patch

import pikepdf
import pytest

import pdftl.core.constants as c
from pdftl.core.registry import registry
from pdftl.fluent import PdfPipeline, pipeline


@pytest.fixture
def mock_pdf():
    return MagicMock(spec=pikepdf.Pdf)


def test_pipeline_initialization(mock_pdf):
    """Verify the pipeline wraps a PDF object correctly."""
    pipeline = PdfPipeline(mock_pdf)
    assert pipeline._pdf == mock_pdf
    assert pipeline.get() == mock_pdf


@patch("pdftl.fluent.api.call")
def test_fluent_method_dispatch(mock_api_call, mock_pdf):
    """
    Verify that calling a dynamic method on the pipeline
    routes correctly to the API and updates internal state.
    """
    mock_result_pdf = MagicMock(spec=pikepdf.Pdf)
    mock_api_call.return_value = mock_result_pdf

    pipeline = PdfPipeline(mock_pdf)

    # We assume 'cat' exists in the registry
    result = pipeline.cat(operation_args=["1-5"])

    # CORRECTED ASSERTION:
    # The new fluent implementation passes the pipeline's PDF
    # as the first item in the 'inputs' list.
    mock_api_call.assert_called_once_with(
        "cat",
        operation_args=["1-5"],
        inputs=[mock_pdf],  # This is the key change
    )

    # Ensure state update happened
    assert pipeline._pdf == mock_result_pdf
    assert result is pipeline


def test_pipeline_chaining_flow(mock_pdf):
    """Verify multiple operations can be chained in one line."""
    with patch("pdftl.fluent.api.call") as mock_api:
        # Mocking a sequence of different PDF objects
        pdf2 = MagicMock(spec=pikepdf.Pdf)
        pdf3 = MagicMock(spec=pikepdf.Pdf)
        mock_api.side_effect = [pdf2, pdf3]

        pipeline = PdfPipeline(mock_pdf)
        pipeline.rotate(operation_args=["90"]).cat(operation_args=["1-2"])

        assert pipeline.get() == pdf3
        assert mock_api.call_count == 2


def test_save_terminal_operation(mock_pdf):
    """Verify that .save() triggers the underlying pikepdf save."""
    import pikepdf

    pipeline = PdfPipeline(mock_pdf)
    pipeline.save("out.pdf")
    mock_pdf.save.assert_called_once_with(
        "out.pdf",
        linearize=False,
        encryption=False,
        compress_streams=True,
        object_stream_mode=pikepdf.ObjectStreamMode.generate,
    )


def test_registry_injection():
    """Ensure every registry operation exists as a method on PdfPipeline."""
    # This relies on registry being populated.
    pipeline = PdfPipeline(MagicMock())
    # We check a few known operations to ensure the dynamic dispatch is active
    known_ops = ["cat", "rotate", "dump_data"]
    for op in known_ops:
        if op in registry.operations:
            assert hasattr(pipeline, op), f"Operation {op} not found on PdfPipeline"


# --- merged from test_fluent_coverage.py ---


class TestFluentApi:
    @patch("pikepdf.open")
    def test_pipeline_open_variants(self, mock_open):
        """Hit lines 24-27: PdfPipeline.open with and without password."""
        mock_pdf = MagicMock(spec=pikepdf.Pdf)
        mock_open.return_value = mock_pdf

        # Test simple open
        pipe = PdfPipeline.open("test.pdf")
        assert isinstance(pipe, PdfPipeline)
        mock_open.assert_called_with("test.pdf")

        # Test open with password (line 26 branch)
        pipe_pw = PdfPipeline.open("protected.pdf", password="secret_pass")
        mock_open.assert_called_with("protected.pdf", password="secret_pass")
        assert pipe_pw._pdf == mock_pdf

    def test_pipeline_helper(self):
        """Hit line 64: The pipeline() helper function."""
        mock_pdf = MagicMock(spec=pikepdf.Pdf)
        pipe = pipeline(mock_pdf)
        assert isinstance(pipe, PdfPipeline)
        assert pipe.native == mock_pdf

    def test_fluent_properties_and_save(self):
        """Hit lines 30, 57, 60: save, native property, and get()."""
        import pikepdf

        mock_pdf = MagicMock(spec=pikepdf.Pdf)
        pipe = PdfPipeline(mock_pdf)

        # Test .native (line 57)
        assert pipe.native == mock_pdf
        # Test .get() (line 60)
        assert pipe.get() == mock_pdf

        # Test .save() (line 30)
        pipe.save("out.pdf")
        mock_pdf.save.assert_called_once_with(
            "out.pdf",
            linearize=False,
            encryption=False,
            compress_streams=True,
            object_stream_mode=pikepdf.ObjectStreamMode.generate,
        )

    def test_getattr_attribute_error(self):
        """Hit line 53: AttributeError for unknown operations."""
        mock_pdf = MagicMock(spec=pikepdf.Pdf)
        pipe = PdfPipeline(mock_pdf)
        with pytest.raises(
            AttributeError, match="'PdfPipeline' object has no attribute 'non_existent_op'"
        ):
            _ = pipe.non_existent_op

    @patch("pdftl.api.call")
    def test_fluent_chaining_logic(self, mock_call):
        """Hit lines 45-48: Chaining vs Returning data."""
        mock_pdf_1 = MagicMock(spec=pikepdf.Pdf)
        mock_pdf_2 = MagicMock(spec=pikepdf.Pdf)
        pipe = PdfPipeline(mock_pdf_1)

        # Manually register the mock operation
        registry.operations["mock_op"] = MagicMock()

        # 1. Operation returns a PDF (Line 47: Chaining)
        mock_call.return_value = mock_pdf_2
        result = pipe.mock_op()
        assert result is pipe  # Chaining
        assert pipe.native is mock_pdf_2

        # 2. Operation returns non-PDF data (Line 48: Terminal)
        mock_call.return_value = {"page_count": 5}
        result = pipe.mock_op()
        assert result == {"page_count": 5}
        # PDF state remains mock_pdf_2 from previous step
        assert pipe.native is mock_pdf_2

    def test_fluent_method_naming(self):
        """Hit line 50: fluent_method.__name__ assignment."""
        mock_pdf = MagicMock(spec=pikepdf.Pdf)
        pipe = PdfPipeline(mock_pdf)

        # Manually register an op for this test
        registry.operations["name_check_op"] = MagicMock()

        # Accessing the method should give us a function with the right name
        method = pipe.name_check_op
        assert method.__name__ == "name_check_op"


def test_fluent_dir_discovery(mocker):
    # Coverage for lines 34-36
    # Mock registry to ensure there is at least one dynamic op
    mocker.patch("pdftl.fluent.registry.operations", {"mock_op": MagicMock()})
    p = pipeline(MagicMock())

    attrs = dir(p)
    assert "mock_op" in attrs
    assert "save" in attrs  # Ensure default attrs are still there


def test_map_fluent_args_edge_cases():
    mock_pdf = MagicMock()
    p = PdfPipeline(mock_pdf)

    # Coverage for line 69: Pass INPUTS as a single string, not a list
    op_data = MagicMock()
    op_data.args = ([], {})
    res = p._map_fluent_args(op_data, (), {c.INPUTS: "single_file.pdf"})
    assert res[c.INPUTS] == [mock_pdf, "single_file.pdf"]

    # Coverage for line 83: c.INPUTS is a positional target
    # Setup: op expects (INPUT_PDF, INPUTS)
    op_data_multi = MagicMock()
    op_data_multi.args = ([c.INPUT_PDF, c.INPUTS], {})

    # Calling p.cmd("extra.pdf")
    # self._pdf satisfies INPUT_PDF, so "extra.pdf" should hit line 83
    res = p._map_fluent_args(op_data_multi, ("extra.pdf",), {})
    assert res[c.INPUTS] == [mock_pdf, "extra.pdf"]


def test_apply_metadata_inconsistency(mocker):
    # Coverage for lines 100-102
    # We want a case where registry has 'missing_op' but api module does not
    mocker.patch("pdftl.fluent.registry.operations", {"missing_op": MagicMock()})
    p = pipeline(MagicMock())

    # Patch api to ensure 'missing_op' is missing
    with patch("pdftl.api.missing_op", side_effect=AttributeError):
        # Accessing the attribute triggers __getattr__ -> _apply_metadata
        func = p.missing_op
        assert func.__name__ == "missing_op"
        # Verify it didn't crash and returned the function even without docstrings


def test_fluent_metadata_attribute_error_silence(monkeypatch):
    import pikepdf

    from pdftl.core.executor import registry
    from pdftl.fluent import PdfPipeline

    # 1. Create a dummy PDF
    pdf = pikepdf.new()
    pipe = PdfPipeline(pdf)

    # 2. Inject a fake operation into the registry that is NOT in the api module
    monkeypatch.setitem(registry.operations, "ghost_op", type("Op", (), {"args": ([], {})})())

    # 3. Accessing it should not crash, even though _apply_metadata fails to find api.ghost_op
    # This triggers the 'except AttributeError: pass' block
    method = pipe.ghost_op
    assert callable(method)
    assert method.__name__ == "ghost_op"
