"""
Unit tests for Slack file_share media extraction and channel-aware outbound routing.

Tests:
- file_share event detection (file_share subtype identified)
- MediaAttachment creation from Slack file metadata (correct media_type, filename, mime_type)
- MinIO upload key format: {tenant_id}/{agent_id}/{message_id}/{filename}
- Channel-aware outbound routing helper (Slack vs WhatsApp)
- Mock httpx/boto3 calls to avoid real API hits
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from shared.models.message import MediaType


# ---------------------------------------------------------------------------
# Helpers to build Slack event payloads
# ---------------------------------------------------------------------------

def make_file_share_event(
    file_id: str = "F12345",
    file_name: str = "screenshot.png",
    mime_type: str = "image/png",
    size: int = 4096,
    workspace_id: str = "T-WORKSPACE-1",
    channel: str = "C-CHANNEL-1",
    user: str = "U-USER-1",
    ts: str = "1711234567.000100",
    thread_ts: str | None = None,
) -> dict:
    """Build a minimal Slack file_share event dict."""
    event: dict = {
        "type": "message",
        "subtype": "file_share",
        "channel": channel,
        "channel_type": "channel",
        "user": user,
        "ts": ts,
        "files": [
            {
                "id": file_id,
                "name": file_name,
                "mimetype": mime_type,
                "size": size,
                "url_private_download": f"https://files.slack.com/files-pri/T-xxx/{file_name}",
            }
        ],
        "_workspace_id": workspace_id,
        "_bot_user_id": "U-BOT-1",
        "text": "",
    }
    if thread_ts:
        event["thread_ts"] = thread_ts
    return event


# ---------------------------------------------------------------------------
# Tests: file_share detection
# ---------------------------------------------------------------------------

class TestFileShareDetection:
    """Tests that file_share subtypes are correctly identified."""

    def test_is_file_share_event_true(self):
        """Event with subtype='file_share' returns True."""
        from gateway.channels.slack_media import is_file_share_event

        event = make_file_share_event()
        assert is_file_share_event(event) is True

    def test_is_file_share_event_false_for_regular_message(self):
        """Regular message event (no subtype) returns False."""
        from gateway.channels.slack_media import is_file_share_event

        event = {"type": "message", "text": "Hello", "user": "U123"}
        assert is_file_share_event(event) is False

    def test_is_file_share_event_false_for_bot_message(self):
        """Bot message subtype returns False."""
        from gateway.channels.slack_media import is_file_share_event

        event = {"type": "message", "subtype": "bot_message", "text": "Bot says hi"}
        assert is_file_share_event(event) is False

    def test_is_file_share_event_false_for_empty_event(self):
        """Empty event dict returns False."""
        from gateway.channels.slack_media import is_file_share_event

        assert is_file_share_event({}) is False


# ---------------------------------------------------------------------------
# Tests: MediaAttachment creation from Slack file metadata
# ---------------------------------------------------------------------------

class TestMediaAttachmentFromSlackFile:
    """Tests that Slack file metadata is correctly mapped to MediaAttachment."""

    def test_image_file_produces_image_media_type(self):
        """image/png MIME type maps to MediaType.IMAGE."""
        from gateway.channels.slack_media import media_type_from_mime

        assert media_type_from_mime("image/png") == MediaType.IMAGE

    def test_jpeg_file_produces_image_media_type(self):
        """image/jpeg MIME type maps to MediaType.IMAGE."""
        from gateway.channels.slack_media import media_type_from_mime

        assert media_type_from_mime("image/jpeg") == MediaType.IMAGE

    def test_gif_file_produces_image_media_type(self):
        """image/gif MIME type maps to MediaType.IMAGE."""
        from gateway.channels.slack_media import media_type_from_mime

        assert media_type_from_mime("image/gif") == MediaType.IMAGE

    def test_pdf_produces_document_media_type(self):
        """application/pdf MIME type maps to MediaType.DOCUMENT."""
        from gateway.channels.slack_media import media_type_from_mime

        assert media_type_from_mime("application/pdf") == MediaType.DOCUMENT

    def test_docx_produces_document_media_type(self):
        """application/vnd.openxmlformats-officedocument... maps to MediaType.DOCUMENT."""
        from gateway.channels.slack_media import media_type_from_mime

        result = media_type_from_mime(
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
        )
        assert result == MediaType.DOCUMENT

    def test_mp3_produces_audio_media_type(self):
        """audio/mpeg MIME type maps to MediaType.AUDIO."""
        from gateway.channels.slack_media import media_type_from_mime

        assert media_type_from_mime("audio/mpeg") == MediaType.AUDIO

    def test_mp4_produces_video_media_type(self):
        """video/mp4 MIME type maps to MediaType.VIDEO."""
        from gateway.channels.slack_media import media_type_from_mime

        assert media_type_from_mime("video/mp4") == MediaType.VIDEO

    def test_unknown_mime_defaults_to_document(self):
        """Unknown MIME type falls back to MediaType.DOCUMENT."""
        from gateway.channels.slack_media import media_type_from_mime

        assert media_type_from_mime("application/x-custom") == MediaType.DOCUMENT

    def test_build_attachment_from_slack_file(self):
        """MediaAttachment is correctly built from Slack file metadata."""
        from gateway.channels.slack_media import build_attachment_from_slack_file

        file_info = {
            "id": "F12345",
            "name": "screenshot.png",
            "mimetype": "image/png",
            "size": 4096,
            "url_private_download": "https://files.slack.com/files-pri/T-xxx/screenshot.png",
        }
        storage_key = "tenant-1/agent-1/msg-1/screenshot.png"

        attachment = build_attachment_from_slack_file(file_info, storage_key)

        assert attachment.media_type == MediaType.IMAGE
        assert attachment.filename == "screenshot.png"
        assert attachment.mime_type == "image/png"
        assert attachment.size_bytes == 4096
        assert attachment.storage_key == storage_key

    def test_build_attachment_pdf(self):
        """PDF file produces DOCUMENT media type attachment."""
        from gateway.channels.slack_media import build_attachment_from_slack_file

        file_info = {
            "id": "F99999",
            "name": "report.pdf",
            "mimetype": "application/pdf",
            "size": 102400,
            "url_private_download": "https://files.slack.com/files-pri/T-xxx/report.pdf",
        }
        storage_key = "tenant-2/agent-2/msg-2/report.pdf"

        attachment = build_attachment_from_slack_file(file_info, storage_key)

        assert attachment.media_type == MediaType.DOCUMENT
        assert attachment.filename == "report.pdf"
        assert attachment.mime_type == "application/pdf"
        assert attachment.size_bytes == 102400


# ---------------------------------------------------------------------------
# Tests: MinIO storage key format
# ---------------------------------------------------------------------------

class TestMinIOStorageKey:
    """Tests that MinIO storage keys follow the correct format."""

    def test_storage_key_format(self):
        """Storage key follows {tenant_id}/{agent_id}/{message_id}/{filename} format."""
        from gateway.channels.slack_media import build_slack_storage_key

        key = build_slack_storage_key(
            tenant_id="tenant-abc",
            agent_id="agent-xyz",
            message_id="msg-123",
            filename="photo.jpg",
        )

        assert key == "tenant-abc/agent-xyz/msg-123/photo.jpg"

    def test_storage_key_with_all_parts(self):
        """Storage key includes all four parts."""
        from gateway.channels.slack_media import build_slack_storage_key

        key = build_slack_storage_key(
            tenant_id="t1",
            agent_id="a1",
            message_id="m1",
            filename="document.pdf",
        )

        parts = key.split("/")
        assert len(parts) == 4
        assert parts[0] == "t1"
        assert parts[1] == "a1"
        assert parts[2] == "m1"
        assert parts[3] == "document.pdf"


# ---------------------------------------------------------------------------
# Tests: Download and upload to MinIO (mocked)
# ---------------------------------------------------------------------------

class TestSlackFileDownloadAndUpload:
    """Tests for Slack file download and MinIO upload flow."""

    @pytest.mark.asyncio
    async def test_download_and_store_slack_file_calls_minio(self):
        """download_and_store_slack_file downloads from Slack and uploads to MinIO."""
        from gateway.channels.slack_media import download_and_store_slack_file

        file_info = {
            "id": "F12345",
            "name": "image.png",
            "mimetype": "image/png",
            "size": 2048,
            "url_private_download": "https://files.slack.com/files-pri/T-xxx/image.png",
        }

        mock_response = MagicMock()
        mock_response.content = b"fake-image-bytes"
        mock_response.raise_for_status = MagicMock()

        mock_s3 = MagicMock()
        mock_s3.put_object = MagicMock()
        mock_s3.generate_presigned_url = MagicMock(
            return_value="http://minio:9000/konstruct-media/tenant-1/agent-1/msg-1/image.png?X-Amz-Signature=abc"
        )

        with patch("boto3.client") as mock_boto3_client:
            mock_boto3_client.return_value = mock_s3
            with patch("httpx.AsyncClient") as mock_httpx:
                mock_client = AsyncMock()
                mock_client.__aenter__ = AsyncMock(return_value=mock_client)
                mock_client.__aexit__ = AsyncMock(return_value=None)
                mock_client.get = AsyncMock(return_value=mock_response)
                mock_httpx.return_value = mock_client

                storage_key, presigned_url = await download_and_store_slack_file(
                    tenant_id="tenant-1",
                    agent_id="agent-1",
                    message_id="msg-1",
                    file_info=file_info,
                    bot_token="xoxb-test-token",
                )

        assert storage_key == "tenant-1/agent-1/msg-1/image.png"
        assert "minio:9000" in presigned_url
        mock_s3.put_object.assert_called_once()
        mock_s3.generate_presigned_url.assert_called_once()

    @pytest.mark.asyncio
    async def test_download_uses_bot_token_for_authorization(self):
        """Download request includes Slack bot token in Authorization header."""
        from gateway.channels.slack_media import download_and_store_slack_file

        file_info = {
            "id": "F99999",
            "name": "doc.pdf",
            "mimetype": "application/pdf",
            "size": 5000,
            "url_private_download": "https://files.slack.com/files-pri/T-xxx/doc.pdf",
        }

        mock_response = MagicMock()
        mock_response.content = b"pdf-content"
        mock_response.raise_for_status = MagicMock()

        captured_headers: list[dict] = []

        async def fake_get(url: str, **kwargs: object) -> MagicMock:
            captured_headers.append(dict(kwargs.get("headers", {})))
            return mock_response

        mock_s3 = MagicMock()
        mock_s3.put_object = MagicMock()
        mock_s3.generate_presigned_url = MagicMock(
            return_value="http://minio:9000/key?sig=xyz"
        )

        with patch("boto3.client") as mock_boto3_client:
            mock_boto3_client.return_value = mock_s3
            with patch("httpx.AsyncClient") as mock_httpx:
                mock_client = AsyncMock()
                mock_client.__aenter__ = AsyncMock(return_value=mock_client)
                mock_client.__aexit__ = AsyncMock(return_value=None)
                mock_client.get = AsyncMock(side_effect=fake_get)
                mock_httpx.return_value = mock_client

                await download_and_store_slack_file(
                    tenant_id="tenant-1",
                    agent_id="agent-1",
                    message_id="msg-1",
                    file_info=file_info,
                    bot_token="xoxb-my-bot-token",
                )

        # At least one request should include the Authorization header
        auth_headers = [h.get("Authorization", "") for h in captured_headers]
        assert any("xoxb-my-bot-token" in h for h in auth_headers), (
            f"Expected bot token in headers, got: {auth_headers}"
        )


# ---------------------------------------------------------------------------
# Tests: Channel-aware outbound routing helper
# ---------------------------------------------------------------------------

class TestChannelAwareOutbound:
    """Tests for the send_response helper that routes to Slack or WhatsApp."""

    @pytest.mark.asyncio
    async def test_slack_channel_calls_chat_update(self):
        """Slack channel routes to _update_slack_placeholder."""
        from orchestrator.tasks import _send_response

        with patch("orchestrator.tasks._update_slack_placeholder") as mock_update:
            mock_update.return_value = None
            await _send_response(
                channel="slack",
                text="Hello from agent",
                extras={
                    "bot_token": "xoxb-test",
                    "channel_id": "C-123",
                    "placeholder_ts": "1711234567.000001",
                },
            )
            mock_update.assert_called_once_with(
                bot_token="xoxb-test",
                channel_id="C-123",
                placeholder_ts="1711234567.000001",
                text="Hello from agent",
            )

    @pytest.mark.asyncio
    async def test_whatsapp_channel_calls_send_whatsapp_message(self):
        """WhatsApp channel routes to send_whatsapp_message."""
        from orchestrator.tasks import _send_response

        with patch("orchestrator.tasks.send_whatsapp_message") as mock_wa:
            mock_wa.return_value = None
            await _send_response(
                channel="whatsapp",
                text="Reply via WhatsApp",
                extras={
                    "phone_number_id": "12345678901",
                    "bot_token": "EAAx...",
                    "wa_id": "5511987654321",
                },
            )
            mock_wa.assert_called_once_with(
                phone_number_id="12345678901",
                access_token="EAAx...",
                recipient_wa_id="5511987654321",
                text="Reply via WhatsApp",
            )

    @pytest.mark.asyncio
    async def test_unknown_channel_logs_warning_no_exception(self):
        """Unknown channel type logs a warning but does not raise."""
        from orchestrator.tasks import _send_response

        # Should not raise, just log and return
        await _send_response(
            channel="mattermost",
            text="Reply via Mattermost",
            extras={},
        )

    @pytest.mark.asyncio
    async def test_slack_channel_missing_extras_does_not_raise(self):
        """Slack routing with empty extras logs warning but does not crash."""
        from orchestrator.tasks import _send_response

        # Should fall back gracefully when extras are missing
        await _send_response(
            channel="slack",
            text="Hello",
            extras={},
        )

    @pytest.mark.asyncio
    async def test_whatsapp_channel_missing_extras_does_not_raise(self):
        """WhatsApp routing with empty extras logs warning but does not crash."""
        from orchestrator.tasks import _send_response

        # Should fall back gracefully when wa_id is missing
        await _send_response(
            channel="whatsapp",
            text="Hello",
            extras={},
        )
