# tests/unit/services/test_message_attachment_service.py
"""
Unit tests for MessageAttachmentService.
"""

import os
import tempfile
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi import UploadFile

from app.modules.messaging.services.message_attachment_service import (
    DEFAULT_MAX_FILE_SIZE_MB,
    IMAGE_MIME_TYPES,
    MessageAttachmentService,
)


@pytest.fixture
def attachment_service():
    """Create a MessageAttachmentService instance with temp storage."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield MessageAttachmentService(storage_base=tmpdir)


@pytest.fixture
def mock_upload_file():
    """Create a mock UploadFile."""

    def _create_upload_file(
        content: bytes = b"test content",
        filename: str = "test.txt",
        content_type: str = "text/plain",
    ):
        file = MagicMock(spec=UploadFile)
        file.filename = filename
        file.content_type = content_type
        file.read = AsyncMock(return_value=content)
        return file

    return _create_upload_file


@pytest.mark.unit
class TestMessageAttachmentServiceValidation:
    """Tests for file validation methods."""

    def test_validate_file_type_allowed_image(self, attachment_service):
        """Test image MIME types are allowed."""
        for mime_type in IMAGE_MIME_TYPES:
            assert attachment_service.validate_file_type(mime_type) is True

    def test_validate_file_type_allowed_documents(self, attachment_service):
        """Test document MIME types are allowed."""
        document_types = [
            "application/pdf",
            "application/msword",
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
            "application/vnd.ms-excel",
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        ]
        for mime_type in document_types:
            assert attachment_service.validate_file_type(mime_type) is True

    def test_validate_file_type_allowed_others(self, attachment_service):
        """Test other allowed MIME types."""
        other_types = ["application/zip", "text/plain", "text/csv"]
        for mime_type in other_types:
            assert attachment_service.validate_file_type(mime_type) is True

    def test_validate_file_type_not_allowed(self, attachment_service):
        """Test disallowed MIME types."""
        disallowed_types = [
            "application/javascript",
            "application/x-executable",
            "text/html",
            "video/mp4",
            "audio/mpeg",
        ]
        for mime_type in disallowed_types:
            assert attachment_service.validate_file_type(mime_type) is False

    def test_is_image_true(self, attachment_service):
        """Test image detection for actual images."""
        for mime_type in IMAGE_MIME_TYPES:
            assert attachment_service.is_image(mime_type) is True

    def test_is_image_false(self, attachment_service):
        """Test image detection for non-images."""
        non_images = ["application/pdf", "text/plain", "application/zip"]
        for mime_type in non_images:
            assert attachment_service.is_image(mime_type) is False


@pytest.mark.unit
class TestMessageAttachmentServiceMaxFileSize:
    """Tests for max file size retrieval."""

    def test_get_max_file_size_from_settings(self, db, attachment_service):
        """Test retrieving max file size from platform settings."""
        with patch(
            "app.modules.messaging.services.message_attachment_service.admin_settings_service"
        ) as mock_settings:
            mock_settings.get_setting_value.return_value = 15
            max_size = attachment_service.get_max_file_size_bytes(db)
            assert max_size == 15 * 1024 * 1024  # 15 MB in bytes

    def test_get_max_file_size_default(self, db, attachment_service):
        """Test default max file size when setting not found."""
        with patch(
            "app.modules.messaging.services.message_attachment_service.admin_settings_service"
        ) as mock_settings:
            mock_settings.get_setting_value.return_value = DEFAULT_MAX_FILE_SIZE_MB
            max_size = attachment_service.get_max_file_size_bytes(db)
            assert max_size == DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024

    def test_get_max_file_size_invalid_value(self, db, attachment_service):
        """Test handling of invalid setting value."""
        with patch(
            "app.modules.messaging.services.message_attachment_service.admin_settings_service"
        ) as mock_settings:
            mock_settings.get_setting_value.return_value = "invalid"
            max_size = attachment_service.get_max_file_size_bytes(db)
            assert max_size == DEFAULT_MAX_FILE_SIZE_MB * 1024 * 1024


@pytest.mark.unit
class TestMessageAttachmentServiceValidateAndStore:
    """Tests for validate_and_store method."""

    @pytest.mark.asyncio
    async def test_validate_and_store_success(
        self, db, attachment_service, mock_upload_file
    ):
        """Test successful file storage."""
        file = mock_upload_file(
            content=b"test file content",
            filename="document.pdf",
            content_type="application/pdf",
        )

        with patch(
            "app.modules.messaging.services.message_attachment_service.admin_settings_service"
        ) as mock_settings:
            mock_settings.get_setting_value.return_value = 10

            result = await attachment_service.validate_and_store(
                db=db,
                file=file,
                conversation_id=1,
            )

        assert result["original_filename"] == "document.pdf"
        assert result["mime_type"] == "application/pdf"
        assert result["file_size"] == len(b"test file content")
        assert result["is_image"] is False
        assert result["filename"].endswith(".pdf")
        assert os.path.exists(result["file_path"])

    @pytest.mark.asyncio
    async def test_validate_and_store_image(
        self, db, attachment_service, mock_upload_file
    ):
        """Test storage of image file."""
        # Create a minimal valid PNG
        png_header = (
            b"\x89PNG\r\n\x1a\n"  # PNG signature
            + b"\x00\x00\x00\rIHDR"  # IHDR chunk header
            + b"\x00\x00\x00\x01"  # width = 1
            + b"\x00\x00\x00\x01"  # height = 1
            + b"\x08\x02"  # bit depth = 8, color type = RGB
            + b"\x00\x00\x00"  # compression, filter, interlace
        )

        file = mock_upload_file(
            content=png_header,
            filename="image.png",
            content_type="image/png",
        )

        with patch(
            "app.modules.messaging.services.message_attachment_service.admin_settings_service"
        ) as mock_settings:
            mock_settings.get_setting_value.return_value = 10

            result = await attachment_service.validate_and_store(
                db=db,
                file=file,
                conversation_id=1,
            )

        assert result["original_filename"] == "image.png"
        assert result["mime_type"] == "image/png"
        assert result["is_image"] is True
        assert result["filename"].endswith(".png")

    @pytest.mark.asyncio
    async def test_validate_and_store_invalid_type(
        self, db, attachment_service, mock_upload_file
    ):
        """Test rejection of invalid file type."""
        file = mock_upload_file(
            content=b"<script>alert('xss')</script>",
            filename="script.js",
            content_type="application/javascript",
        )

        with patch(
            "app.modules.messaging.services.message_attachment_service.admin_settings_service"
        ) as mock_settings:
            mock_settings.get_setting_value.return_value = 10

            with pytest.raises(ValueError, match="File type.*not allowed"):
                await attachment_service.validate_and_store(
                    db=db,
                    file=file,
                    conversation_id=1,
                )

    @pytest.mark.asyncio
    async def test_validate_and_store_file_too_large(
        self, db, attachment_service, mock_upload_file
    ):
        """Test rejection of oversized file."""
        # Create content larger than max size
        large_content = b"x" * (11 * 1024 * 1024)  # 11 MB
        file = mock_upload_file(
            content=large_content,
            filename="large.pdf",
            content_type="application/pdf",
        )

        with patch(
            "app.modules.messaging.services.message_attachment_service.admin_settings_service"
        ) as mock_settings:
            mock_settings.get_setting_value.return_value = 10  # 10 MB limit

            with pytest.raises(ValueError, match="exceeds maximum allowed size"):
                await attachment_service.validate_and_store(
                    db=db,
                    file=file,
                    conversation_id=1,
                )

    @pytest.mark.asyncio
    async def test_validate_and_store_no_filename(
        self, db, attachment_service, mock_upload_file
    ):
        """Test handling of file without filename."""
        file = mock_upload_file(
            content=b"test content",
            filename=None,
            content_type="text/plain",
        )
        file.filename = None  # Ensure it's None

        with patch(
            "app.modules.messaging.services.message_attachment_service.admin_settings_service"
        ) as mock_settings:
            mock_settings.get_setting_value.return_value = 10

            result = await attachment_service.validate_and_store(
                db=db,
                file=file,
                conversation_id=1,
            )

        assert result["original_filename"] == "attachment"

    @pytest.mark.asyncio
    async def test_validate_and_store_no_content_type(
        self, db, attachment_service, mock_upload_file
    ):
        """Test handling of file without content type (falls back to octet-stream)."""
        file = mock_upload_file(
            content=b"test content",
            filename="file.bin",
            content_type=None,
        )
        file.content_type = None

        with patch(
            "app.modules.messaging.services.message_attachment_service.admin_settings_service"
        ) as mock_settings:
            mock_settings.get_setting_value.return_value = 10

            # Should reject application/octet-stream as not allowed
            with pytest.raises(ValueError, match="File type.*not allowed"):
                await attachment_service.validate_and_store(
                    db=db,
                    file=file,
                    conversation_id=1,
                )


@pytest.mark.unit
class TestMessageAttachmentServiceFileOperations:
    """Tests for file operation methods."""

    def test_delete_attachment_success(self, attachment_service):
        """Test successful attachment deletion."""
        # Create a temp file
        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(b"test content")
            file_path = f.name

        assert os.path.exists(file_path)

        result = attachment_service.delete_attachment(file_path)

        assert result is True
        assert not os.path.exists(file_path)

    def test_delete_attachment_with_thumbnail(self, attachment_service):
        """Test deletion of attachment with thumbnail."""
        # Create temp files
        with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as f:
            f.write(b"image content")
            file_path = f.name

        with tempfile.NamedTemporaryFile(delete=False, suffix="_thumb.png") as f:
            f.write(b"thumbnail content")
            thumb_path = f.name

        result = attachment_service.delete_attachment(file_path, thumb_path)

        assert result is True
        assert not os.path.exists(file_path)
        assert not os.path.exists(thumb_path)

    def test_delete_attachment_file_not_exists(self, attachment_service):
        """Test deletion when file doesn't exist."""
        result = attachment_service.delete_attachment("/nonexistent/file.pdf")
        assert result is True  # No error, just returns True

    def test_get_download_url(self, attachment_service):
        """Test download URL generation."""
        url = attachment_service.get_download_url("uploads/messages/2025/01/1/abc.pdf")
        assert url == "/uploads/messages/2025/01/1/abc.pdf"

    def test_get_file_content_success(self, attachment_service):
        """Test reading file content."""
        test_content = b"test file content"
        with tempfile.NamedTemporaryFile(delete=False) as f:
            f.write(test_content)
            file_path = f.name

        try:
            result = attachment_service.get_file_content(file_path)
            assert result == test_content
        finally:
            os.unlink(file_path)

    def test_get_file_content_not_found(self, attachment_service):
        """Test reading non-existent file."""
        result = attachment_service.get_file_content("/nonexistent/file.pdf")
        assert result is None


@pytest.mark.unit
class TestMessageAttachmentServiceThumbnail:
    """Tests for thumbnail creation."""

    def test_create_thumbnail_pil_not_installed(self, attachment_service):
        """Test graceful handling when PIL is not available."""
        with patch.dict("sys.modules", {"PIL": None}):
            # This should not raise an error, just return empty dict
            result = attachment_service._create_thumbnail(
                b"fake image content", "/tmp/test.png"
            )
            # When PIL import fails, it returns empty dict
            assert isinstance(result, dict)

    def test_create_thumbnail_invalid_image(self, attachment_service):
        """Test handling of invalid image data."""
        with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as f:
            f.write(b"not an image")
            file_path = f.name

        try:
            result = attachment_service._create_thumbnail(b"not an image", file_path)
            # Should return empty dict on error
            assert isinstance(result, dict)
        finally:
            os.unlink(file_path)
