from pathlib import Path
from unittest.mock import MagicMock, call, patch

import pikepdf
import pytest

# --- Import Exceptions ---
from pdftl.exceptions import InvalidArgumentError, MissingArgumentError

# --- Import module and functions to test ---
from pdftl.operations.attach_files import (
    Attachment,
    ParsedAttachment,
    _attach_attachment,
    _attach_attachment_to_page,
    _attachment_rect,
    _get_attachments_from_args,
    _raise_exception_if_invalid_after_keyword,
    _resolve_attachments,
    _set_page_specs_in_parsed_attachments,
    _validate_topage_and_convert_to_ints,
    attach_files,
)

# --- Import Mocks ---
from pdftl.utils.user_input import UserInputContext

# --- Fixtures (Unchanged) ---


@pytest.fixture
def mock_input_context():
    """Mock for the UserInputContext."""
    mock = MagicMock(spec=UserInputContext)
    mock.get_input = MagicMock()
    return mock


@pytest.fixture
def mock_pdf():
    """Mock for a pikepdf.Pdf object."""
    pdf = MagicMock(spec=pikepdf.Pdf)
    pdf.pages = []
    pdf.attachments = {}
    return pdf


@pytest.fixture(autouse=True)
def patch_logging(mocker):
    """Patch logging for all tests in this module."""
    mocker.patch("pdftl.operations.attach_files.logging")


# --- Test Cases ---


def test_raise_exception_if_invalid_after_keyword_missing_file():
    with pytest.raises(MissingArgumentError, match="Missing filename before 'to_page'"):
        _raise_exception_if_invalid_after_keyword(
            ["to_page", "1"], 0, attachments_is_empty=True, keyword="to_page"
        )


def test_raise_exception_if_invalid_after_keyword_missing_arg():
    with pytest.raises(MissingArgumentError, match="Missing argument after 'relation'"):
        _raise_exception_if_invalid_after_keyword(
            ["file.pdf", "relation"], 1, attachments_is_empty=False, keyword="relation"
        )


def test_set_page_specs_in_parsed_attachments():
    p1 = ParsedAttachment(path="a.pdf")
    p2 = ParsedAttachment(path="b.pdf", page_spec="1")
    p3 = ParsedAttachment(path="c.pdf")

    attachments_to_set = [p1, p2, p3]
    _set_page_specs_in_parsed_attachments("even", attachments_to_set)

    assert p1.page_spec == "even"
    assert p2.page_spec == "1"
    assert p3.page_spec == "even"


## _validate_topage_and_convert_to_ints ##


@patch("pdftl.operations.attach_files.page_numbers_matching_page_spec", return_value=[1, 3, 5])
def test_validate_topage_and_convert_to_ints_success(mock_page_numbers):
    assert _validate_topage_and_convert_to_ints("1,3,5", 5) == [1, 3, 5]


@patch("pdftl.operations.attach_files.page_numbers_matching_page_spec", return_value=[])
def test_validate_topage_and_convert_to_ints_no_matches(mock_page_numbers):
    with pytest.raises(InvalidArgumentError, match="did not yield any pages"):
        _validate_topage_and_convert_to_ints("99", 5)


@patch(
    "pdftl.operations.attach_files.page_numbers_matching_page_spec",
    side_effect=ValueError("bad spec"),
)
def test_validate_topage_and_convert_to_ints_parser_error(mock_page_numbers):
    with pytest.raises(InvalidArgumentError, match="gave an error: bad spec"):
        _validate_topage_and_convert_to_ints("foo", 5)


## _resolve_attachments ##


@patch("pdftl.operations.attach_files.can_read_file", return_value=True)
@patch("pdftl.operations.attach_files._validate_topage_and_convert_to_ints", return_value=[1, 2])
def test_resolve_attachments_happy_path(mock_validate, mock_can_read, mock_input_context):
    parsed_items = [
        ParsedAttachment(path="a.pdf", page_spec="1-2"),
        ParsedAttachment(path="b.pdf", relationship="source"),
    ]
    num_pages = 5

    resolved = _resolve_attachments(parsed_items, num_pages, mock_input_context)

    assert len(resolved) == 2

    assert resolved[0].path == Path("a.pdf")
    assert resolved[0].pages == [1, 2]
    mock_validate.assert_called_once_with("1-2", num_pages)

    assert resolved[1].path == Path("b.pdf")
    assert resolved[1].relationship == "Source"


@patch(
    "pdftl.operations.attach_files.can_read_file", side_effect=[True, True]
)  # Needs two True values now
def test_resolve_attachments_file_prompt(mock_can_read, mock_input_context):
    """
    Tests that a 'PROMPT' path triggers the interactive prompter.
    """
    mock_get_input = MagicMock()
    mock_get_input.return_value = "prompted.pdf"

    parsed_items = [ParsedAttachment(path="PROMPT")]

    resolved = _resolve_attachments(parsed_items, 5, mock_get_input)

    # Check that _can_read_file was called twice
    assert mock_can_read.call_count == 2
    mock_can_read.assert_has_calls(
        [
            call("prompted.pdf"),  # First call inside get_filename
            call("prompted.pdf"),  # Second call in main flow
        ]
    )

    mock_get_input.assert_called_once()

    assert len(resolved) == 1
    assert resolved[0].path == Path("prompted.pdf")


@patch("pdftl.operations.attach_files.can_read_file", return_value=True)
def test_resolve_attachments_invalid_relation(mock_can_read, mock_input_context):
    parsed_items = [ParsedAttachment(path="a.pdf", relationship="Friend")]

    with pytest.raises(InvalidArgumentError, match="Invalid attachment relationship"):
        _resolve_attachments(parsed_items, 5, mock_input_context)


@patch("pdftl.operations.attach_files._attach_attachment_to_page")
def test_attach_attachment_to_pages(mock_attach_to_page, mock_pdf):
    att = Attachment(Path("f.txt"), pages=[1, 3])
    num_attached = [0, 0, 0]

    _attach_attachment(mock_pdf, att, num_attached)

    assert mock_attach_to_page.call_count == 2
    mock_attach_to_page.assert_has_calls(
        [
            call(mock_pdf, att, 1, 0),
            call(mock_pdf, att, 3, 0),
        ]
    )
    assert num_attached == [1, 0, 1]


@patch("pikepdf.AttachedFileSpec.from_filepath")
def test_attach_attachment_to_pdf(mock_from_filepath, mock_pdf):
    mock_spec = MagicMock()
    mock_from_filepath.return_value = mock_spec
    att = Attachment(Path("dir/f.txt"), pages=None, relationship="Source")

    _attach_attachment(mock_pdf, att, [])

    mock_from_filepath.assert_called_once_with(mock_pdf, att.path)
    assert mock_spec.relationship == pikepdf.Name("/Source")
    assert mock_pdf.attachments["f.txt"] == mock_spec


@patch("pikepdf.Annotation")
@patch("pikepdf.Dictionary")
def test_attach_attachment_to_page_existing_spec(mock_Dict, mock_Annotation, mock_pdf):
    mock_page = MagicMock(cropbox=[0, 0, 100, 100])
    mock_page.Annots = MagicMock()
    mock_pdf.pages = [mock_page]
    mock_file_spec_obj = MagicMock()
    mock_pdf.attachments["f.txt"] = MagicMock(obj=mock_file_spec_obj)

    att = Attachment(Path("f.txt"))

    _attach_attachment_to_page(mock_pdf, att, 1, 0)

    expected_rect = _attachment_rect(mock_page.cropbox, 0)
    expected_dict = {
        "/Contents": pikepdf.String("f.txt"),
        "/Subtype": pikepdf.Name.FileAttachment,
        "/FS": mock_file_spec_obj,
        "/Rect": expected_rect,
    }

    mock_Dict.assert_called_once_with(expected_dict)
    mock_Annotation.assert_called_once_with(mock_Dict.return_value)
    mock_page.Annots.append.assert_called_once_with(mock_Annotation.return_value.obj)


@patch("pdftl.operations.attach_files._attach_attachment")
@patch("pdftl.operations.attach_files._get_attachments_from_args")
def test_attach_files_orchestration(mock_get_options, mock_attach, mock_input_context, mock_pdf):
    mock_pdf.pages = [MagicMock(), MagicMock(), MagicMock()]
    num_pages = len(mock_pdf.pages)

    att1 = Attachment(Path("a.txt"))
    att2 = Attachment(Path("b.txt"), pages=[1])
    mock_get_options.return_value = [att1, att2]

    options = {"attach_files": ["..."]}

    attach_files(mock_pdf, options, mock_input_context)

    mock_get_options.assert_called_once_with(options, num_pages, mock_input_context)
    assert mock_attach.call_count == 2


@patch("pikepdf.Annotation")
@patch("pikepdf.Dictionary")
@patch("pikepdf.AttachedFileSpec.from_filepath")
def test_page_attachment_with_relationship(
    mock_from_filepath, mock_Dictionary, mock_Annotation, mock_pdf, tmp_path
):
    mock_spec = MagicMock(spec=pikepdf.AttachedFileSpec)
    mock_from_filepath.return_value = mock_spec

    mock_page = MagicMock(cropbox=[0, 0, 100, 100])
    mock_page.Annots = MagicMock()
    mock_pdf.pages = [mock_page]
    mock_pdf.attachments = {}

    att = Attachment(path=Path(tmp_path / "t.pdf"), pages=[1], relationship="Source")

    _attach_attachment(mock_pdf, att, num_attached_by_page=[0])

    assert mock_spec.relationship == pikepdf.Name("/Source")
    mock_page.Annots.append.assert_called_once()


def test_unreadable_attachment_warning(caplog):
    # Covers lines 219-220
    parsed = [ParsedAttachment(path="non_existent_file.pdf")]
    ctx = MagicMock()
    with patch("pdftl.operations.attach_files.can_read_file", return_value=False):
        results = _resolve_attachments(parsed, 10, ctx)
        assert len(results) == 0
        assert "Cannot read attachment" in caplog.text


def test_recursive_prompt_on_invalid_file():
    # Covers line 258
    mock_get_input = MagicMock()
    # First side effect: invalid file, Second side effect: valid file
    mock_get_input.side_effect = ["invalid.pdf", "valid.pdf"]

    with patch("pdftl.operations.attach_files.can_read_file") as mock_read:
        mock_read.side_effect = [False, True, True]
        parsed = [ParsedAttachment(path="PROMPT")]
        results = _resolve_attachments(parsed, 10, mock_get_input)

        assert len(results) == 1
        assert mock_get_input.call_count == 2


# tests/test_attach_files_coverage.py


def test_get_attachments_from_args_coverage():
    """
    Directly tests _get_attachments_from_args to ensure lines 137-138 are executed.
    These lines handle parsing specs to intent and resolving them.
    """
    # Setup dummy arguments
    args = ["dummy_file.txt", "to_page", "1"]
    num_pages = 5
    mock_input_context = MagicMock()

    # We patch the internal calls to isolate just the flow of 137-138
    # 1. patch _parse_attach_specs_to_intent (Line 137)
    # 2. patch _resolve_attachments (Line 138)
    with (
        patch("pdftl.operations.attach_files._parse_attach_specs_to_intent") as mock_parse,
        patch("pdftl.operations.attach_files._resolve_attachments") as mock_resolve,
    ):
        # Setup return values
        mock_parsed_items = [MagicMock()]
        mock_parse.return_value = mock_parsed_items

        # Execute the function
        _get_attachments_from_args(args, num_pages, mock_input_context)

        # Assertions to prove lines were hit
        mock_parse.assert_called_once_with(args)
        mock_resolve.assert_called_once_with(mock_parsed_items, num_pages, mock_input_context)
