"""Tests for the Confluence attachments module."""

import json
import os
import tempfile
from unittest.mock import AsyncMock, MagicMock, Mock, mock_open, patch

import pytest
from mcp.types import EmbeddedResource, TextContent

from mcp_atlassian.confluence.attachments import AttachmentsMixin

# Test scenarios for AttachmentsMixin
#
# 1. Single Attachment Upload (upload_attachment method):
#    - Success case: Uploads attachment correctly
#    - Path handling: Converts relative path to absolute path
#    - Error cases:
#      - No content ID provided
#      - No file path provided
#      - File not found
#      - API error during upload
#
# 2. Multiple Attachments Upload (upload_attachments method):
#    - Success case: Uploads multiple files correctly
#    - Partial success: Some files upload successfully, others fail
#    - Error cases:
#      - Empty list of file paths
#      - No content ID provided
#
# 3. Single Attachment Download (download_attachment method):
#    - Success case: Downloads attachment correctly with proper HTTP response
#    - Path handling: Converts relative path to absolute path
#    - Error cases:
#      - No URL provided
#      - HTTP error during download
#      - File write error
#      - File not created after write operation
#
# 4. Content Attachments Download (download_content_attachments method):
#    - Success case: Downloads all attachments for content
#    - Path handling: Converts relative target directory to absolute path
#    - Edge cases:
#      - Content has no attachments
#      - API error retrieving attachments
#      - Some attachments fail to download
#      - Attachment has missing download URL
#
# 5. Get Content Attachments (get_content_attachments method):
#    - Success case: Retrieves all attachments for content
#    - Pagination: Handles paginated results
#    - Error cases:
#      - No content ID provided
#      - API error retrieving attachments
#      - Empty results


class TestAttachmentsMixin:
    """Tests for the AttachmentsMixin class."""

    @pytest.fixture
    def attachments_mixin(self, confluence_client) -> AttachmentsMixin:
        """Create an AttachmentsMixin instance for testing."""
        # AttachmentsMixin inherits from ConfluenceClient, so we need to create it properly
        with patch(
            "mcp_atlassian.confluence.attachments.ConfluenceClient.__init__"
        ) as mock_init:
            mock_init.return_value = None
            mixin = AttachmentsMixin()
            # Copy the necessary attributes from our mocked client
            mixin.confluence = confluence_client.confluence
            mixin.config = confluence_client.config
            mixin.preprocessor = confluence_client.preprocessor
            return mixin

    def _mock_rest_api_upload(
        self, attachments_mixin, response_data=None, raise_error=None
    ):
        """Helper to mock the direct REST API upload call.

        Args:
            attachments_mixin: The mixin fixture
            response_data: Dict to return from API (default: successful attachment)
            raise_error: Exception to raise from API call (default: None)

        Returns:
            The mock response object
        """
        if response_data is None:
            response_data = {
                "results": [
                    {
                        "id": "att12345",
                        "type": "attachment",
                        "title": "test_file.txt",
                        "extensions": {"mediaType": "text/plain", "fileSize": 100},
                        "_links": {
                            "download": "/download/attachments/123/test_file.txt"
                        },
                        "version": {"number": 1},
                    }
                ]
            }

        mock_response = Mock()
        if raise_error:
            # Changed from .post to .put to match implementation
            attachments_mixin.confluence._session.put.side_effect = raise_error
        else:
            mock_response.json.return_value = response_data
            mock_response.raise_for_status.return_value = None
            # Changed from .post to .put to match implementation
            attachments_mixin.confluence._session.put.return_value = mock_response

        return mock_response

    # Tests for upload_attachment method

    def test_upload_attachment_success(self, attachments_mixin: AttachmentsMixin):
        """Test successful attachment upload."""
        # Mock the REST API call
        self._mock_rest_api_upload(attachments_mixin)

        # Mock file operations
        with (
            patch("os.path.exists") as mock_exists,
            patch("os.path.getsize") as mock_getsize,
            patch("os.path.isabs") as mock_isabs,
            patch("os.path.abspath") as mock_abspath,
            patch("os.path.basename") as mock_basename,
            patch("builtins.open", mock_open(read_data=b"test content")),
        ):
            mock_exists.return_value = True
            mock_getsize.return_value = 100
            mock_isabs.return_value = True
            mock_abspath.return_value = "/absolute/path/test_file.txt"
            mock_basename.return_value = "test_file.txt"

            # Call the method
            result = attachments_mixin.upload_attachment(
                "123456",
                "/absolute/path/test_file.txt",
                comment="Test comment",
                minor_edit=False,
            )

            # Assertions
            assert result["success"] is True
            assert result["content_id"] == "123456"
            assert result["filename"] == "test_file.txt"
            assert result["size"] == 100
            assert result["id"] == "att12345"

            # Verify the REST API was called with correct parameters
            # Changed from .post to .put to match implementation
            attachments_mixin.confluence._session.put.assert_called_once()
            call_args = attachments_mixin.confluence._session.put.call_args

            # Check URL
            assert "/rest/api/content/123456/child/attachment" in call_args[0][0]

            # Check headers include X-Atlassian-Token
            assert call_args[1]["headers"]["X-Atlassian-Token"] == "nocheck"

            # Check minorEdit was passed in data
            assert call_args[1]["data"]["minorEdit"] == "false"
            # Note: comment is now in files dict as multipart form data, not in data dict

    def test_upload_attachment_relative_path(self, attachments_mixin: AttachmentsMixin):
        """Test attachment upload with a relative path."""
        # Mock the REST API call
        self._mock_rest_api_upload(attachments_mixin)

        # Mock file operations
        with (
            patch("os.path.exists") as mock_exists,
            patch("os.path.getsize") as mock_getsize,
            patch("os.path.isabs") as mock_isabs,
            patch("os.path.abspath") as mock_abspath,
            patch("os.path.basename") as mock_basename,
            patch("builtins.open", mock_open(read_data=b"test content")),
        ):
            mock_exists.return_value = True
            mock_getsize.return_value = 100
            mock_isabs.return_value = False
            mock_abspath.return_value = "/absolute/path/test_file.txt"
            mock_basename.return_value = "test_file.txt"

            # Call the method with a relative path
            result = attachments_mixin.upload_attachment("123456", "test_file.txt")

            # Assertions
            assert result["success"] is True
            mock_isabs.assert_called_once_with("test_file.txt")
            mock_abspath.assert_called_once_with("test_file.txt")

    def test_upload_attachment_no_content_id(self, attachments_mixin: AttachmentsMixin):
        """Test attachment upload with no content ID."""
        result = attachments_mixin.upload_attachment("", "/path/to/file.txt")

        # Assertions
        assert result["success"] is False
        assert "No content ID provided" in result["error"]
        # Should not call API at all
        attachments_mixin.confluence._session.post.assert_not_called()

    def test_upload_attachment_no_file_path(self, attachments_mixin: AttachmentsMixin):
        """Test attachment upload with no file path."""
        result = attachments_mixin.upload_attachment("123456", "")

        # Assertions
        assert result["success"] is False
        assert "No file path provided" in result["error"]
        # Should not call API at all
        attachments_mixin.confluence._session.post.assert_not_called()

    def test_upload_attachment_file_not_found(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test attachment upload when file doesn't exist."""
        # Mock file operations
        with (
            patch("os.path.exists") as mock_exists,
            patch("os.path.isabs") as mock_isabs,
            patch("os.path.abspath") as mock_abspath,
        ):
            mock_exists.return_value = False
            mock_isabs.return_value = True
            mock_abspath.return_value = "/absolute/path/test_file.txt"

            result = attachments_mixin.upload_attachment(
                "123456", "/absolute/path/test_file.txt"
            )

            # Assertions
            assert result["success"] is False
            assert "File not found" in result["error"]
            # Should not call API if file doesn't exist
            attachments_mixin.confluence._session.post.assert_not_called()

    def test_upload_attachment_api_error(self, attachments_mixin: AttachmentsMixin):
        """Test attachment upload with an API error."""
        # Mock the REST API to raise an exception
        from requests.exceptions import HTTPError

        self._mock_rest_api_upload(
            attachments_mixin, raise_error=HTTPError("API Error")
        )

        # Mock file operations
        with (
            patch("os.path.exists") as mock_exists,
            patch("os.path.isabs") as mock_isabs,
            patch("os.path.abspath") as mock_abspath,
            patch("os.path.basename") as mock_basename,
            patch("os.path.getsize") as mock_getsize,
            patch("builtins.open", mock_open(read_data=b"test content")),
        ):
            mock_exists.return_value = True
            mock_isabs.return_value = True
            mock_abspath.return_value = "/absolute/path/test_file.txt"
            mock_basename.return_value = "test_file.txt"
            mock_getsize.return_value = 100

            result = attachments_mixin.upload_attachment(
                "123456", "/absolute/path/test_file.txt"
            )

            # Assertions
            assert result["success"] is False
            # When direct API fails, we get generic failure message
            assert "Failed to upload attachment" in result["error"]

    # Tests for upload_attachments method

    def test_upload_attachments_success(self, attachments_mixin: AttachmentsMixin):
        """Test successful upload of multiple attachments."""
        file_paths = [
            "/path/to/file1.txt",
            "/path/to/file2.pdf",
            "/path/to/file3.jpg",
        ]

        # Create mock successful results for each file
        mock_results = [
            {
                "success": True,
                "content_id": "123456",
                "filename": f"file{i + 1}.{ext}",
                "size": 100 * (i + 1),
                "id": f"att{i + 1}",
            }
            for i, ext in enumerate(["txt", "pdf", "jpg"])
        ]

        with patch.object(
            attachments_mixin, "upload_attachment", side_effect=mock_results
        ) as mock_upload:
            # Call the method
            result = attachments_mixin.upload_attachments("123456", file_paths)

            # Assertions
            assert result["success"] is True
            assert result["content_id"] == "123456"
            assert result["total"] == 3
            assert len(result["uploaded"]) == 3
            assert len(result["failed"]) == 0

            # Check that upload_attachment was called for each file
            assert mock_upload.call_count == 3
            # Calls are made with positional args, not keyword args
            mock_upload.assert_any_call("123456", "/path/to/file1.txt", None, True)  # noqa: FBT003
            mock_upload.assert_any_call("123456", "/path/to/file2.pdf", None, True)  # noqa: FBT003
            mock_upload.assert_any_call("123456", "/path/to/file3.jpg", None, True)  # noqa: FBT003

            # Verify uploaded files details
            assert result["uploaded"][0]["filename"] == "file1.txt"
            assert result["uploaded"][1]["filename"] == "file2.pdf"
            assert result["uploaded"][2]["filename"] == "file3.jpg"

    def test_upload_attachments_mixed_results(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test upload of multiple attachments with mixed success and failure."""
        file_paths = [
            "/path/to/file1.txt",  # Will succeed
            "/path/to/file2.pdf",  # Will fail
            "/path/to/file3.jpg",  # Will succeed
        ]

        # Create mock results with mixed success/failure
        mock_results = [
            {
                "success": True,
                "content_id": "123456",
                "filename": "file1.txt",
                "size": 100,
                "id": "att1",
            },
            {"success": False, "error": "File not found: /path/to/file2.pdf"},
            {
                "success": True,
                "content_id": "123456",
                "filename": "file3.jpg",
                "size": 300,
                "id": "att3",
            },
        ]

        with patch.object(
            attachments_mixin, "upload_attachment", side_effect=mock_results
        ) as mock_upload:
            # Call the method
            result = attachments_mixin.upload_attachments("123456", file_paths)

            # Assertions
            assert result["success"] is True
            assert result["content_id"] == "123456"
            assert result["total"] == 3
            assert len(result["uploaded"]) == 2
            assert len(result["failed"]) == 1
            assert mock_upload.call_count == 3

            # Verify failed file details
            assert result["failed"][0]["filename"] == "file2.pdf"
            assert "File not found" in result["failed"][0]["error"]

    def test_upload_attachments_empty_list(self, attachments_mixin: AttachmentsMixin):
        """Test upload with an empty list of file paths."""
        result = attachments_mixin.upload_attachments("123456", [])

        # Assertions
        assert result["success"] is False
        assert "No file paths provided" in result["error"]

    def test_upload_attachments_no_content_id(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test upload with no content ID provided."""
        result = attachments_mixin.upload_attachments("", ["/path/to/file.txt"])

        # Assertions
        assert result["success"] is False
        assert "No content ID provided" in result["error"]

    # Tests for download_attachment method

    def test_download_attachment_success(self, attachments_mixin: AttachmentsMixin):
        """Test successful attachment download."""
        # Mock the response
        mock_response = MagicMock()
        mock_response.iter_content.return_value = [b"test content"]
        mock_response.raise_for_status = MagicMock()
        attachments_mixin.confluence._session.get.return_value = mock_response

        # Use platform-independent temp path for cross-platform testing
        test_path = os.path.join(tempfile.gettempdir(), "test_file.txt")

        # Mock file operations
        with (
            patch("builtins.open", mock_open()) as mock_file,
            patch("os.path.exists") as mock_exists,
            patch("os.path.getsize") as mock_getsize,
            patch("os.makedirs") as mock_makedirs,
        ):
            mock_exists.return_value = True
            mock_getsize.return_value = 12  # Length of "test content"

            # Call the method
            result = attachments_mixin.download_attachment(
                "https://test.url/attachment", test_path
            )

            # Assertions
            assert result is True
            attachments_mixin.confluence._session.get.assert_called_once_with(
                "https://test.url/attachment", stream=True
            )
            # Path should remain unchanged since it's already absolute
            mock_file.assert_called_once_with(test_path, "wb")
            mock_file().write.assert_called_once_with(b"test content")
            mock_makedirs.assert_called_once()

    def test_download_attachment_relative_path(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test attachment download with a relative path."""
        # Mock the response
        mock_response = MagicMock()
        mock_response.iter_content.return_value = [b"test content"]
        mock_response.raise_for_status = MagicMock()
        attachments_mixin.confluence._session.get.return_value = mock_response

        # Mock file operations
        with (
            patch("builtins.open", mock_open()) as mock_file,
            patch("os.path.exists") as mock_exists,
            patch("os.path.getsize") as mock_getsize,
            patch("os.makedirs") as mock_makedirs,
            patch("os.path.abspath") as mock_abspath,
            patch("os.path.isabs") as mock_isabs,
        ):
            mock_exists.return_value = True
            mock_getsize.return_value = 12
            mock_isabs.return_value = False
            mock_abspath.return_value = "/absolute/path/test_file.txt"

            # Call the method with a relative path
            result = attachments_mixin.download_attachment(
                "https://test.url/attachment", "test_file.txt"
            )

            # Assertions
            assert result is True
            mock_isabs.assert_called_once_with("test_file.txt")
            mock_abspath.assert_called_once_with("test_file.txt")
            mock_file.assert_called_once_with("/absolute/path/test_file.txt", "wb")

    def test_download_attachment_no_url(self, attachments_mixin: AttachmentsMixin):
        """Test attachment download with no URL."""
        result = attachments_mixin.download_attachment("", "/tmp/test_file.txt")
        assert result is False

    def test_download_attachment_http_error(self, attachments_mixin: AttachmentsMixin):
        """Test attachment download with an HTTP error."""
        # Mock the response to raise an HTTP error
        mock_response = MagicMock()
        mock_response.raise_for_status.side_effect = Exception("HTTP Error")
        attachments_mixin.confluence._session.get.return_value = mock_response

        result = attachments_mixin.download_attachment(
            "https://test.url/attachment", "/tmp/test_file.txt"
        )
        assert result is False

    def test_download_attachment_file_write_error(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test attachment download with a file write error."""
        # Mock the response
        mock_response = MagicMock()
        mock_response.iter_content.return_value = [b"test content"]
        mock_response.raise_for_status = MagicMock()
        attachments_mixin.confluence._session.get.return_value = mock_response

        # Mock file operations to raise an exception during write
        with (
            patch("builtins.open", mock_open()) as mock_file,
            patch("os.makedirs") as mock_makedirs,
        ):
            mock_file().write.side_effect = OSError("Write error")

            result = attachments_mixin.download_attachment(
                "https://test.url/attachment", "/tmp/test_file.txt"
            )
            assert result is False

    def test_download_attachment_file_not_created(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test attachment download when file is not created."""
        # Mock the response
        mock_response = MagicMock()
        mock_response.iter_content.return_value = [b"test content"]
        mock_response.raise_for_status = MagicMock()
        attachments_mixin.confluence._session.get.return_value = mock_response

        # Mock file operations
        with (
            patch("builtins.open", mock_open()) as mock_file,
            patch("os.path.exists") as mock_exists,
            patch("os.makedirs") as mock_makedirs,
        ):
            mock_exists.return_value = False  # File doesn't exist after write

            result = attachments_mixin.download_attachment(
                "https://test.url/attachment", "/tmp/test_file.txt"
            )
            assert result is False

    # Tests for download_content_attachments method

    def test_download_content_attachments_success(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test successful download of all content attachments."""
        # Mock the get_content_attachments response
        mock_attachments = [
            {
                "id": "att1",
                "title": "test1.txt",
                "extensions": {"fileSize": 100},
                "_links": {"download": "/download/test1.txt"},
            },
            {
                "id": "att2",
                "title": "test2.txt",
                "extensions": {"fileSize": 200},
                "_links": {"download": "/download/test2.txt"},
            },
        ]

        # Mock ConfluenceAttachment.from_api_response
        mock_attachment1 = MagicMock()
        mock_attachment1.title = "test1.txt"
        mock_attachment1.download_url = "/download/test1.txt"
        mock_attachment1.file_size = 100

        mock_attachment2 = MagicMock()
        mock_attachment2.title = "test2.txt"
        mock_attachment2.download_url = "/download/test2.txt"
        mock_attachment2.file_size = 200

        # Mock methods
        with (
            patch.object(
                attachments_mixin,
                "get_content_attachments",
                return_value={"success": True, "attachments": mock_attachments},
            ) as mock_get,
            patch.object(
                attachments_mixin, "download_attachment", return_value=True
            ) as mock_download,
            patch("pathlib.Path.mkdir") as mock_mkdir,
            patch(
                "mcp_atlassian.models.confluence.ConfluenceAttachment.from_api_response",
                side_effect=[mock_attachment1, mock_attachment2],
            ),
        ):
            result = attachments_mixin.download_content_attachments(
                "123456", "/tmp/attachments"
            )

            # Assertions
            assert result["success"] is True
            assert len(result["downloaded"]) == 2
            assert len(result["failed"]) == 0
            assert result["total"] == 2
            assert result["content_id"] == "123456"
            assert mock_download.call_count == 2
            mock_mkdir.assert_called_once()

    def test_download_content_attachments_relative_path(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test download content attachments with a relative path."""
        # Mock the get_content_attachments response
        mock_attachments = [
            {
                "id": "att1",
                "title": "test1.txt",
                "_links": {"download": "/download/test1.txt"},
            }
        ]

        # Mock attachment
        mock_attachment = MagicMock()
        mock_attachment.title = "test1.txt"
        mock_attachment.download_url = "/download/test1.txt"
        mock_attachment.file_size = 100

        # Mock path operations
        with (
            patch.object(
                attachments_mixin,
                "get_content_attachments",
                return_value={"success": True, "attachments": mock_attachments},
            ),
            patch.object(attachments_mixin, "download_attachment", return_value=True),
            patch("pathlib.Path.mkdir") as mock_mkdir,
            patch(
                "mcp_atlassian.models.confluence.ConfluenceAttachment.from_api_response",
                return_value=mock_attachment,
            ),
            patch("os.path.isabs") as mock_isabs,
            patch("os.path.abspath") as mock_abspath,
        ):
            mock_isabs.return_value = False
            mock_abspath.return_value = "/absolute/path/attachments"

            result = attachments_mixin.download_content_attachments(
                "123456", "attachments"
            )

            # Assertions
            assert result["success"] is True
            mock_isabs.assert_called_once_with("attachments")
            mock_abspath.assert_called_once_with("attachments")

    def test_download_content_attachments_no_attachments(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test download when content has no attachments."""
        # Mock the get_content_attachments response with empty list
        with (
            patch.object(
                attachments_mixin,
                "get_content_attachments",
                return_value={"success": True, "attachments": []},
            ),
            patch("pathlib.Path.mkdir") as mock_mkdir,
        ):
            result = attachments_mixin.download_content_attachments(
                "123456", "/tmp/attachments"
            )

            # Assertions
            assert result["success"] is True
            assert "No attachments found" in result["message"]
            assert len(result["downloaded"]) == 0
            assert len(result["failed"]) == 0
            mock_mkdir.assert_called_once()

    def test_download_content_attachments_api_error(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test download when API error occurs retrieving attachments."""
        # Mock the get_content_attachments to return error
        with patch.object(
            attachments_mixin,
            "get_content_attachments",
            return_value={"success": False, "error": "API Error"},
        ):
            result = attachments_mixin.download_content_attachments(
                "123456", "/tmp/attachments"
            )

            # Assertions
            assert result["success"] is False
            assert "API Error" in result["error"]

    def test_download_content_attachments_some_failures(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test download when some attachments fail to download."""
        # Mock the get_content_attachments response
        mock_attachments = [
            {
                "id": "att1",
                "title": "test1.txt",
                "_links": {"download": "/download/test1.txt"},
            },
            {
                "id": "att2",
                "title": "test2.txt",
                "_links": {"download": "/download/test2.txt"},
            },
        ]

        # Mock attachments
        mock_attachment1 = MagicMock()
        mock_attachment1.title = "test1.txt"
        mock_attachment1.download_url = "/download/test1.txt"
        mock_attachment1.file_size = 100

        mock_attachment2 = MagicMock()
        mock_attachment2.title = "test2.txt"
        mock_attachment2.download_url = "/download/test2.txt"
        mock_attachment2.file_size = 200

        # Mock the download_attachment method to succeed for first and fail for second
        with (
            patch.object(
                attachments_mixin,
                "get_content_attachments",
                return_value={"success": True, "attachments": mock_attachments},
            ),
            patch.object(
                attachments_mixin, "download_attachment", side_effect=[True, False]
            ) as mock_download,
            patch("pathlib.Path.mkdir") as mock_mkdir,
            patch(
                "mcp_atlassian.models.confluence.ConfluenceAttachment.from_api_response",
                side_effect=[mock_attachment1, mock_attachment2],
            ),
        ):
            result = attachments_mixin.download_content_attachments(
                "123456", "/tmp/attachments"
            )

            # Assertions
            assert result["success"] is True
            assert len(result["downloaded"]) == 1
            assert len(result["failed"]) == 1
            assert result["downloaded"][0]["filename"] == "test1.txt"
            assert result["failed"][0]["filename"] == "test2.txt"
            assert mock_download.call_count == 2

    def test_download_content_attachments_missing_url(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test download when an attachment has no download URL."""
        # Mock the get_content_attachments response
        mock_attachments = [
            {"id": "att1", "title": "test1.txt"}  # Missing _links
        ]

        # Mock attachment with no URL
        mock_attachment = MagicMock()
        mock_attachment.title = "test1.txt"
        mock_attachment.download_url = None  # No URL
        mock_attachment.file_size = 100

        # Mock methods
        with (
            patch.object(
                attachments_mixin,
                "get_content_attachments",
                return_value={"success": True, "attachments": mock_attachments},
            ),
            patch("pathlib.Path.mkdir") as mock_mkdir,
            patch(
                "mcp_atlassian.models.confluence.ConfluenceAttachment.from_api_response",
                return_value=mock_attachment,
            ),
        ):
            result = attachments_mixin.download_content_attachments(
                "123456", "/tmp/attachments"
            )

            # Assertions
            assert result["success"] is True
            assert len(result["downloaded"]) == 0
            assert len(result["failed"]) == 1
            assert result["failed"][0]["filename"] == "test1.txt"
            assert "No download URL available" in result["failed"][0]["error"]

    # Tests for get_content_attachments method

    def test_get_content_attachments_success(self, attachments_mixin: AttachmentsMixin):
        """Test successful retrieval of content attachments."""
        # Mock the Confluence API response
        mock_api_response = {
            "results": [
                {
                    "id": "att1",
                    "type": "attachment",
                    "title": "test1.txt",
                    "extensions": {"mediaType": "text/plain", "fileSize": 100},
                },
                {
                    "id": "att2",
                    "type": "attachment",
                    "title": "test2.pdf",
                    "extensions": {"mediaType": "application/pdf", "fileSize": 200},
                },
            ],
            "start": 0,
            "limit": 50,
            "size": 2,
        }
        attachments_mixin.confluence.get_attachments_from_content.return_value = (
            mock_api_response
        )

        # Call the method
        result = attachments_mixin.get_content_attachments("123456")

        # Assertions
        assert result["success"] is True
        assert result["content_id"] == "123456"
        assert len(result["attachments"]) == 2
        assert result["total"] == 2
        attachments_mixin.confluence.get_attachments_from_content.assert_called_once_with(
            "123456", start=0, limit=50
        )

    def test_get_content_attachments_with_pagination(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test retrieval with custom pagination parameters."""
        # Mock the Confluence API response
        mock_api_response = {
            "results": [{"id": "att1", "title": "test1.txt"}],
            "start": 25,
            "limit": 25,
            "size": 1,
        }
        attachments_mixin.confluence.get_attachments_from_content.return_value = (
            mock_api_response
        )

        # Call the method with custom pagination
        result = attachments_mixin.get_content_attachments("123456", start=25, limit=25)

        # Assertions
        assert result["success"] is True
        attachments_mixin.confluence.get_attachments_from_content.assert_called_once_with(
            "123456", start=25, limit=25
        )

    def test_get_content_attachments_empty_results(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test retrieval when no attachments exist."""
        # Mock the Confluence API response with empty results
        mock_api_response = {"results": [], "start": 0, "limit": 50, "size": 0}
        attachments_mixin.confluence.get_attachments_from_content.return_value = (
            mock_api_response
        )

        # Call the method
        result = attachments_mixin.get_content_attachments("123456")

        # Assertions
        assert result["success"] is True
        assert len(result["attachments"]) == 0
        assert result["total"] == 0

    def test_get_content_attachments_no_content_id(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test retrieval with no content ID."""
        result = attachments_mixin.get_content_attachments("")

        # Assertions
        assert result["success"] is False
        assert "No content ID provided" in result["error"]
        attachments_mixin.confluence.get_attachments_from_content.assert_not_called()

    def test_get_content_attachments_api_error(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test retrieval when API error occurs."""
        # Mock the Confluence API to raise an exception
        attachments_mixin.confluence.get_attachments_from_content.side_effect = (
            Exception("API Error")
        )

        # Call the method
        result = attachments_mixin.get_content_attachments("123456")

        # Assertions
        assert result["success"] is False
        assert "API Error" in result["error"]

    def test_get_content_attachments_v2_oauth(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test getting attachments using v2 API (OAuth)."""
        # Mock config URL to be cloud (contains .atlassian.net) and OAuth
        with patch.object(
            attachments_mixin.config, "url", "https://test.atlassian.net/wiki"
        ):
            attachments_mixin.config.auth_type = "oauth"

            # Mock the v2 API method
            mock_v2_get = Mock(
                return_value={
                    "results": [
                        {"id": "att1", "title": "file1.txt", "type": "attachment"},
                        {"id": "att2", "title": "file2.pdf", "type": "attachment"},
                    ],
                    "size": 2,
                    "start": 0,
                    "limit": 50,
                }
            )

            with patch(
                "mcp_atlassian.confluence.attachments.ConfluenceV2Adapter"
            ) as mock_adapter_class:
                mock_adapter = Mock()
                mock_adapter.get_page_attachments = mock_v2_get
                mock_adapter_class.return_value = mock_adapter

                # Call the method
                result = attachments_mixin.get_content_attachments("123456")

                # Assertions
                assert result["success"] is True
                assert result["content_id"] == "123456"
                assert len(result["attachments"]) == 2
                assert result["total"] == 2
                # Updated to include filename and media_type parameters added during UAT
                mock_v2_get.assert_called_once_with(
                    page_id="123456",
                    start=0,
                    limit=50,
                    filename=None,
                    media_type=None,
                )

    def test_get_content_attachments_v2_with_pagination(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test v2 API pagination parameters are passed correctly."""
        # Mock config URL to be cloud and OAuth
        with patch.object(
            attachments_mixin.config, "url", "https://test.atlassian.net/wiki"
        ):
            attachments_mixin.config.auth_type = "oauth"

            mock_v2_get = Mock(
                return_value={
                    "results": [],
                    "size": 0,
                    "start": 25,
                    "limit": 10,
                }
            )

            with patch(
                "mcp_atlassian.confluence.attachments.ConfluenceV2Adapter"
            ) as mock_adapter_class:
                mock_adapter = Mock()
                mock_adapter.get_page_attachments = mock_v2_get
                mock_adapter_class.return_value = mock_adapter

                # Call with custom pagination
                result = attachments_mixin.get_content_attachments(
                    "123456", start=25, limit=10
                )

                # Assertions
                assert result["success"] is True
                assert result["start"] == 25
                assert result["limit"] == 10
                # Updated to include filename and media_type parameters added during UAT
                mock_v2_get.assert_called_once_with(
                    page_id="123456",
                    start=25,
                    limit=10,
                    filename=None,
                    media_type=None,
                )

    def test_get_content_attachments_v2_error(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test error handling when v2 API fails."""
        # Mock config URL to be cloud and OAuth
        with patch.object(
            attachments_mixin.config, "url", "https://test.atlassian.net/wiki"
        ):
            attachments_mixin.config.auth_type = "oauth"

            with patch(
                "mcp_atlassian.confluence.attachments.ConfluenceV2Adapter"
            ) as mock_adapter_class:
                mock_adapter = Mock()
                mock_adapter.get_page_attachments.side_effect = ValueError(
                    "Page not found"
                )
                mock_adapter_class.return_value = mock_adapter

                # Call the method
                result = attachments_mixin.get_content_attachments("999999")

                # Assertions
                assert result["success"] is False
                assert "Page not found" in result["error"]

    # Delete attachment tests
    def test_delete_attachment_success_v1(self, attachments_mixin: AttachmentsMixin):
        """Test successful deletion using v1 API (non-OAuth)."""
        # Ensure non-OAuth (v1 path)
        attachments_mixin.config.auth_type = "basic"

        # Mock the session delete call
        mock_response = Mock()
        mock_response.raise_for_status.return_value = None
        attachments_mixin.confluence._session.delete.return_value = mock_response

        # Call the method
        result = attachments_mixin.delete_attachment("att123")

        # Assertions
        assert result["success"] is True
        assert result["attachment_id"] == "att123"
        assert "deleted successfully" in result["message"]
        attachments_mixin.confluence._session.delete.assert_called_once()

    def test_delete_attachment_success_v2(self, attachments_mixin: AttachmentsMixin):
        """Test successful deletion using v2 API (OAuth)."""
        # Mock config URL to be cloud and OAuth
        with patch.object(
            attachments_mixin.config, "url", "https://test.atlassian.net/wiki"
        ):
            attachments_mixin.config.auth_type = "oauth"

            with patch(
                "mcp_atlassian.confluence.attachments.ConfluenceV2Adapter"
            ) as mock_adapter_class:
                mock_adapter = Mock()
                mock_adapter.delete_attachment.return_value = None
                mock_adapter_class.return_value = mock_adapter

                # Call the method
                result = attachments_mixin.delete_attachment("att456")

                # Assertions
                assert result["success"] is True
                assert result["attachment_id"] == "att456"
                assert "deleted successfully" in result["message"]
                mock_adapter.delete_attachment.assert_called_once_with("att456")

    def test_delete_attachment_no_id(self, attachments_mixin: AttachmentsMixin):
        """Test deletion fails when no attachment ID is provided."""
        result = attachments_mixin.delete_attachment("")

        assert result["success"] is False
        assert "No attachment ID provided" in result["error"]

    def test_delete_attachment_v1_http_error(self, attachments_mixin: AttachmentsMixin):
        """Test deletion fails with HTTP error using v1 API."""
        # Ensure non-OAuth (v1 path)
        attachments_mixin.config.auth_type = "basic"

        # Mock session delete to raise HTTPError
        mock_response = Mock()
        mock_response.raise_for_status.side_effect = Exception("404 Not Found")
        attachments_mixin.confluence._session.delete.return_value = mock_response

        # Call the method
        result = attachments_mixin.delete_attachment("att999")

        # Assertions
        assert result["success"] is False
        assert "404 Not Found" in result["error"]

    def test_delete_attachment_v2_error(self, attachments_mixin: AttachmentsMixin):
        """Test deletion fails when v2 adapter raises error."""
        # Mock config URL to be cloud and OAuth
        with patch.object(
            attachments_mixin.config, "url", "https://test.atlassian.net/wiki"
        ):
            attachments_mixin.config.auth_type = "oauth"

            with patch(
                "mcp_atlassian.confluence.attachments.ConfluenceV2Adapter"
            ) as mock_adapter_class:
                mock_adapter = Mock()
                mock_adapter.delete_attachment.side_effect = ValueError(
                    "Attachment not found"
                )
                mock_adapter_class.return_value = mock_adapter

                # Call the method
                result = attachments_mixin.delete_attachment("att999")

                # Assertions
                assert result["success"] is False
                assert "Attachment not found" in result["error"]

    def test_delete_attachment_v1_network_error(
        self, attachments_mixin: AttachmentsMixin
    ):
        """Test deletion handles network errors gracefully."""
        # Ensure non-OAuth (v1 path)
        attachments_mixin.config.auth_type = "basic"

        # Mock session delete to raise connection error
        attachments_mixin.confluence._session.delete.side_effect = Exception(
            "Connection timeout"
        )

        # Call the method
        result = attachments_mixin.delete_attachment("att789")

        # Assertions
        assert result["success"] is False
        assert "Connection timeout" in result["error"]


class TestDownloadAttachmentServerTool:
    """Tests for the server-level download_attachment tool (EmbeddedResource return)."""

    @pytest.mark.asyncio
    async def test_returns_embedded_resource_on_success(self):
        mock_fetcher = MagicMock()
        mock_fetcher._v2_adapter = None
        mock_fetcher.config.url = "https://test.atlassian.net/wiki"

        meta_resp = MagicMock()
        meta_resp.json.return_value = {
            "title": "report.pdf",
            "_links": {"download": "/download/report.pdf"},
            "extensions": {"mediaType": "application/pdf", "fileSize": 100},
        }
        meta_resp.raise_for_status.return_value = None

        download_resp = MagicMock()
        download_resp.iter_content.return_value = [b"pdf content"]
        download_resp.raise_for_status.return_value = None

        mock_fetcher.confluence._session.get.side_effect = [meta_resp, download_resp]

        with patch(
            "mcp_atlassian.servers.confluence.get_confluence_fetcher",
            AsyncMock(return_value=mock_fetcher),
        ):
            from mcp_atlassian.servers.confluence import (
                download_attachment as server_download_attachment,
            )

            result = await server_download_attachment.fn(
                ctx=MagicMock(), attachment_id="att123456"
            )

        assert isinstance(result, EmbeddedResource)
        assert result.resource.mimeType == "application/pdf"
        assert result.resource.blob

    @pytest.mark.asyncio
    async def test_returns_text_on_missing_download_url(self):
        mock_fetcher = MagicMock()
        mock_fetcher._v2_adapter = None
        mock_fetcher.config.url = "https://test.atlassian.net/wiki"

        meta_resp = MagicMock()
        meta_resp.json.return_value = {
            "title": "report.pdf",
            "_links": {},
            "extensions": {"mediaType": "application/pdf", "fileSize": 100},
        }
        meta_resp.raise_for_status.return_value = None
        mock_fetcher.confluence._session.get.return_value = meta_resp

        with patch(
            "mcp_atlassian.servers.confluence.get_confluence_fetcher",
            AsyncMock(return_value=mock_fetcher),
        ):
            from mcp_atlassian.servers.confluence import (
                download_attachment as server_download_attachment,
            )

            result = await server_download_attachment.fn(
                ctx=MagicMock(), attachment_id="att123456"
            )

        assert isinstance(result, TextContent)
        data = json.loads(result.text)
        assert data["success"] is False
        assert "download URL" in data["error"]

    @pytest.mark.asyncio
    async def test_returns_text_on_size_exceeded(self):
        mock_fetcher = MagicMock()
        mock_fetcher._v2_adapter = None
        mock_fetcher.config.url = "https://test.atlassian.net/wiki"

        meta_resp = MagicMock()
        meta_resp.json.return_value = {
            "title": "huge.bin",
            "_links": {"download": "/download/huge.bin"},
            "extensions": {
                "mediaType": "application/octet-stream",
                "fileSize": 60 * 1024 * 1024,
            },
        }
        meta_resp.raise_for_status.return_value = None
        mock_fetcher.confluence._session.get.return_value = meta_resp

        with patch(
            "mcp_atlassian.servers.confluence.get_confluence_fetcher",
            AsyncMock(return_value=mock_fetcher),
        ):
            from mcp_atlassian.servers.confluence import (
                download_attachment as server_download_attachment,
            )

            result = await server_download_attachment.fn(
                ctx=MagicMock(), attachment_id="att_huge"
            )

        assert isinstance(result, TextContent)
        data = json.loads(result.text)
        assert data["success"] is False
        assert "50 MB" in data["error"]

    @pytest.mark.asyncio
    async def test_returns_text_on_exception(self):
        mock_fetcher = MagicMock()
        mock_fetcher._v2_adapter = None
        mock_fetcher.config.url = "https://test.atlassian.net/wiki"
        mock_fetcher.confluence._session.get.side_effect = Exception("Connection error")

        with patch(
            "mcp_atlassian.servers.confluence.get_confluence_fetcher",
            AsyncMock(return_value=mock_fetcher),
        ):
            from mcp_atlassian.servers.confluence import (
                download_attachment as server_download_attachment,
            )

            result = await server_download_attachment.fn(
                ctx=MagicMock(), attachment_id="att123456"
            )

        assert isinstance(result, TextContent)
        data = json.loads(result.text)
        assert data["success"] is False
        assert "Connection error" in data["error"]


class TestDownloadContentAttachmentsServerTool:
    """Tests for the server-level download_content_attachments tool (EmbeddedResource return)."""

    @pytest.mark.asyncio
    async def test_returns_summary_plus_embedded_resources(self):
        mock_fetcher = MagicMock()
        mock_fetcher.config.url = "https://test.atlassian.net/wiki"
        mock_fetcher.get_content_attachments.return_value = {
            "success": True,
            "attachments": [
                {
                    "id": "att1",
                    "title": "file1.txt",
                    "extensions": {"mediaType": "text/plain", "fileSize": 12},
                    "_links": {"download": "/download/file1.txt"},
                }
            ],
        }

        download_resp = MagicMock()
        download_resp.iter_content.return_value = [b"hello world!"]
        download_resp.raise_for_status.return_value = None
        mock_fetcher.confluence._session.get.return_value = download_resp

        with patch(
            "mcp_atlassian.servers.confluence.get_confluence_fetcher",
            AsyncMock(return_value=mock_fetcher),
        ):
            from mcp_atlassian.servers.confluence import (
                download_content_attachments as server_download_content,
            )

            results = await server_download_content.fn(
                ctx=MagicMock(), content_id="123456"
            )

        assert len(results) == 2
        assert isinstance(results[0], TextContent)
        summary = json.loads(results[0].text)
        assert summary["success"] is True
        assert summary["downloaded"] == 1
        assert isinstance(results[1], EmbeddedResource)
        assert results[1].resource.mimeType == "text/plain"

    @pytest.mark.asyncio
    async def test_returns_text_when_no_attachments(self):
        mock_fetcher = MagicMock()
        mock_fetcher.get_content_attachments.return_value = {
            "success": True,
            "attachments": [],
        }

        with patch(
            "mcp_atlassian.servers.confluence.get_confluence_fetcher",
            AsyncMock(return_value=mock_fetcher),
        ):
            from mcp_atlassian.servers.confluence import (
                download_content_attachments as server_download_content,
            )

            results = await server_download_content.fn(
                ctx=MagicMock(), content_id="123456"
            )

        assert len(results) == 1
        assert isinstance(results[0], TextContent)
        summary = json.loads(results[0].text)
        assert summary["success"] is True
        assert "No attachments" in summary["message"]

    @pytest.mark.asyncio
    async def test_returns_error_text_on_api_failure(self):
        mock_fetcher = MagicMock()
        mock_fetcher.get_content_attachments.return_value = {
            "success": False,
            "error": "API error occurred",
        }

        with patch(
            "mcp_atlassian.servers.confluence.get_confluence_fetcher",
            AsyncMock(return_value=mock_fetcher),
        ):
            from mcp_atlassian.servers.confluence import (
                download_content_attachments as server_download_content,
            )

            results = await server_download_content.fn(
                ctx=MagicMock(), content_id="123456"
            )

        assert len(results) == 1
        assert isinstance(results[0], TextContent)
        data = json.loads(results[0].text)
        assert data["success"] is False

    @pytest.mark.asyncio
    async def test_skips_attachment_over_size_limit(self):
        mock_fetcher = MagicMock()
        mock_fetcher.config.url = "https://test.atlassian.net/wiki"
        mock_fetcher.get_content_attachments.return_value = {
            "success": True,
            "attachments": [
                {
                    "id": "att_big",
                    "title": "huge.bin",
                    "extensions": {
                        "mediaType": "application/octet-stream",
                        "fileSize": 60 * 1024 * 1024,
                    },
                    "_links": {"download": "/download/huge.bin"},
                }
            ],
        }

        with patch(
            "mcp_atlassian.servers.confluence.get_confluence_fetcher",
            AsyncMock(return_value=mock_fetcher),
        ):
            from mcp_atlassian.servers.confluence import (
                download_content_attachments as server_download_content,
            )

            results = await server_download_content.fn(
                ctx=MagicMock(), content_id="123456"
            )

        assert len(results) == 1
        assert isinstance(results[0], TextContent)
        summary = json.loads(results[0].text)
        assert summary["downloaded"] == 0
        assert len(summary["failed"]) == 1
        assert "50 MB" in summary["failed"][0]["error"]
