"""Direct tests for send_file_message and _upload_file_as_mxc."""

from __future__ import annotations

import asyncio
from types import SimpleNamespace
from typing import TYPE_CHECKING, cast
from unittest.mock import AsyncMock, MagicMock, Mock, patch

import nio
import pytest
from nio.exceptions import OlmUnverifiedDeviceError

from mindroom.matrix.client import DeliveredMatrixEvent, join_room
from mindroom.matrix.client_delivery import (
    _msgtype_for_mimetype,
    _upload_file_as_mxc,
    edit_message_result,
    send_audio_message,
    send_file_message,
    send_message_result,
    send_runtime_encrypted_media_message,
)
from mindroom.matrix.client_room_admin import RoomJoinOutcome
from mindroom.matrix.media import extract_media_caption
from mindroom.matrix.runtime_media import RuntimeEncryptedMediaAttachment

if TYPE_CHECKING:
    from collections.abc import Callable
    from pathlib import Path
    from typing import BinaryIO


def _mock_client(*, encrypted: bool = False) -> AsyncMock:
    """Create a mock nio.AsyncClient with room state."""
    client = AsyncMock(spec=nio.AsyncClient)
    room = MagicMock()
    room.encrypted = encrypted
    client.rooms = {"!room:localhost": room}
    client.olm = MagicMock() if encrypted else None
    return client


def _upload_response(content_uri: str = "mxc://localhost/abc123") -> nio.UploadResponse:
    resp = MagicMock(spec=nio.UploadResponse)
    resp.content_uri = content_uri
    return resp


class TestUploadFileAsMxc:
    """Tests for _upload_file_as_mxc."""

    @pytest.mark.asyncio
    async def test_unencrypted_upload_returns_mxc_and_info(self, tmp_path: Path) -> None:
        """Unencrypted upload should return MXC URI and info payload without file key."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/plain"), {})

        file = tmp_path / "doc.txt"
        file.write_text("hello", encoding="utf-8")

        mxc_uri, payload = await _upload_file_as_mxc(
            client,
            "!room:localhost",
            file,
            mimetype="text/plain",
        )

        assert mxc_uri == "mxc://localhost/plain"
        assert payload is not None
        assert "info" in payload
        assert payload["info"]["mimetype"] == "text/plain"
        assert payload["info"]["size"] == 5
        assert "file" not in payload

    @pytest.mark.asyncio
    async def test_encrypted_upload_returns_file_payload(self, tmp_path: Path) -> None:
        """Encrypted upload should include encryption keys in the file payload."""
        client = _mock_client(encrypted=True)
        client.upload.return_value = (_upload_response("mxc://localhost/enc"), {})

        file = tmp_path / "secret.bin"
        file.write_bytes(b"\x00" * 16)

        with patch(
            "mindroom.matrix.client_delivery.crypto.attachments.encrypt_attachment",
            return_value=(
                b"encrypted_bytes",
                {
                    "key": {"k": "test_key"},
                    "iv": "test_iv",
                    "hashes": {"sha256": "test_hash"},
                },
            ),
        ):
            mxc_uri, payload = await _upload_file_as_mxc(
                client,
                "!room:localhost",
                file,
                mimetype="application/octet-stream",
            )

        assert mxc_uri == "mxc://localhost/enc"
        assert payload is not None
        assert "file" in payload
        file_payload = payload["file"]
        assert file_payload["url"] == "mxc://localhost/enc"
        assert file_payload["key"] == {"k": "test_key"}
        assert file_payload["iv"] == "test_iv"
        assert file_payload["hashes"] == {"sha256": "test_hash"}
        assert file_payload["v"] == "v2"
        assert file_payload["mimetype"] == "application/octet-stream"

        # Upload should use octet-stream content type and .enc suffix
        upload_call = client.upload.call_args
        assert upload_call.kwargs["content_type"] == "application/octet-stream"
        assert upload_call.kwargs["filename"] == "secret.bin.enc"

    @pytest.mark.asyncio
    async def test_upload_returns_none_on_read_failure(self, tmp_path: Path) -> None:
        """Should return (None, None) when the file cannot be read."""
        client = _mock_client()
        missing = tmp_path / "nonexistent.txt"

        mxc_uri, payload = await _upload_file_as_mxc(
            client,
            "!room:localhost",
            missing,
            mimetype="text/plain",
        )

        assert mxc_uri is None
        assert payload is None

    @pytest.mark.asyncio
    async def test_upload_returns_none_on_upload_error(self, tmp_path: Path) -> None:
        """Should return (None, None) when the Matrix upload fails."""
        client = _mock_client()
        error = MagicMock(spec=nio.UploadError)
        client.upload.return_value = (error, {})

        file = tmp_path / "doc.txt"
        file.write_text("content", encoding="utf-8")

        mxc_uri, payload = await _upload_file_as_mxc(
            client,
            "!room:localhost",
            file,
            mimetype="text/plain",
        )

        assert mxc_uri is None
        assert payload is None


class TestSendRuntimeEncryptedMediaMessage:
    """Tests for resending turn-scoped encrypted MXC media."""

    @staticmethod
    def _attachment() -> RuntimeEncryptedMediaAttachment:
        return RuntimeEncryptedMediaAttachment(
            attachment_id="att_screenshot",
            filename="desktop-screenshot.png",
            url="mxc://localhost/existing",
            key="key",
            iv="iv",
            sha256="hash",
            mime_type="image/png",
            size=321,
        )

    @pytest.mark.asyncio
    async def test_reuses_encrypted_media_without_upload(self) -> None:
        """A runtime image sends the existing MXC object and keys inside the encrypted room event."""
        client = _mock_client(encrypted=True)
        attachment = self._attachment()

        with patch(
            "mindroom.matrix.client_delivery.send_message_result",
            new=AsyncMock(
                side_effect=lambda _client, _room, content, **_kwargs: DeliveredMatrixEvent(
                    event_id="$image:localhost",
                    content_sent=content,
                ),
            ),
        ) as send_message:
            event_id = await send_runtime_encrypted_media_message(
                client,
                "!room:localhost",
                attachment,
                thread_id="$thread:localhost",
                latest_thread_event_id="$latest:localhost",
            )

        assert event_id == "$image:localhost"
        content = send_message.await_args.args[2]
        assert content["msgtype"] == "m.image"
        assert content["body"] == attachment.filename
        assert content["info"] == {"size": 321, "mimetype": "image/png"}
        assert content["file"] == attachment.encrypted_file_content()
        assert content["m.relates_to"] == {
            "rel_type": "m.thread",
            "event_id": "$thread:localhost",
            "is_falling_back": True,
            "m.in_reply_to": {"event_id": "$latest:localhost"},
        }
        client.upload.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_reuses_encrypted_media_in_unencrypted_room(self) -> None:
        """An unencrypted room can receive the existing encrypted MXC object without another upload."""
        client = _mock_client(encrypted=False)

        with patch(
            "mindroom.matrix.client_delivery.send_message_result",
            new=AsyncMock(
                side_effect=lambda _client, _room, content, **_kwargs: DeliveredMatrixEvent(
                    event_id="$image:localhost",
                    content_sent=content,
                ),
            ),
        ) as send_message:
            event_id = await send_runtime_encrypted_media_message(
                client,
                "!room:localhost",
                self._attachment(),
            )

        assert event_id == "$image:localhost"
        assert send_message.await_args.args[2]["file"] == self._attachment().encrypted_file_content()
        client.upload.assert_not_awaited()


class TestSendFileMessage:
    """Tests for send_file_message."""

    @pytest.mark.asyncio
    async def test_sends_unencrypted_file_with_url(self, tmp_path: Path) -> None:
        """Unencrypted file should produce content with 'url' and no 'file' key."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/f1"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$evt:localhost", content_sent=content)

        file = tmp_path / "report.pdf"
        file.write_bytes(b"%PDF")

        with patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send):
            event_id = await send_file_message(
                client,
                "!room:localhost",
                file,
            )

        assert event_id == "$evt:localhost"
        assert sent_content is not None
        assert sent_content["msgtype"] == "m.file"
        assert sent_content["body"] == "report.pdf"
        assert sent_content["filename"] == "report.pdf"
        assert sent_content["url"] == "mxc://localhost/f1"
        assert "file" not in sent_content
        assert "m.relates_to" not in sent_content

    @pytest.mark.asyncio
    async def test_sends_encrypted_file_with_file_key(self, tmp_path: Path) -> None:
        """Encrypted file should produce content with 'file' key and no 'url'."""
        client = _mock_client(encrypted=True)
        client.upload.return_value = (_upload_response("mxc://localhost/enc1"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$evt:localhost", content_sent=content)

        file = tmp_path / "secret.bin"
        file.write_bytes(b"\x00" * 8)

        with (
            patch("mindroom.matrix.client_delivery.crypto.ENCRYPTION_ENABLED", True),
            patch(
                "mindroom.matrix.client_delivery.crypto.attachments.encrypt_attachment",
                return_value=(
                    b"encrypted",
                    {
                        "key": {"k": "k1"},
                        "iv": "iv1",
                        "hashes": {"sha256": "h1"},
                    },
                ),
            ),
            patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send),
        ):
            event_id = await send_file_message(
                client,
                "!room:localhost",
                file,
            )

        assert event_id == "$evt:localhost"
        assert sent_content is not None
        assert "file" in sent_content
        assert sent_content["file"]["url"] == "mxc://localhost/enc1"
        assert "url" not in sent_content

    @pytest.mark.asyncio
    async def test_thread_relation_is_set(self, tmp_path: Path) -> None:
        """When thread_id is provided, m.relates_to should be set."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/t1"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$evt:localhost", content_sent=content)

        file = tmp_path / "data.csv"
        file.write_text("a,b,c", encoding="utf-8")

        with patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send):
            event_id = await send_file_message(
                client,
                "!room:localhost",
                file,
                thread_id="$root:localhost",
                latest_thread_event_id="$latest:localhost",
            )

        assert event_id == "$evt:localhost"
        assert sent_content is not None
        relates_to = sent_content["m.relates_to"]
        assert relates_to["rel_type"] == "m.thread"
        assert relates_to["event_id"] == "$root:localhost"
        assert relates_to["is_falling_back"] is True
        assert relates_to["m.in_reply_to"]["event_id"] == "$latest:localhost"

    @pytest.mark.asyncio
    async def test_uses_precomputed_latest_thread_event_id_when_provided(self, tmp_path: Path) -> None:
        """Threaded sends should skip lookup when the caller already resolved the latest event."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/t1"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$evt:localhost", content_sent=content)

        file = tmp_path / "data.csv"
        file.write_text("a,b,c", encoding="utf-8")

        with patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send):
            event_id = await send_file_message(
                client,
                "!room:localhost",
                file,
                thread_id="$root:localhost",
                latest_thread_event_id="$precomputed:localhost",
            )

        assert event_id == "$evt:localhost"
        assert sent_content is not None
        assert sent_content["m.relates_to"]["m.in_reply_to"]["event_id"] == "$precomputed:localhost"

    @pytest.mark.asyncio
    async def test_threaded_send_requires_precomputed_latest_thread_event_id(self, tmp_path: Path) -> None:
        """Threaded file sends should require fallback resolution from the conversation-cache seam."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/t1"), {})
        file = tmp_path / "data.csv"
        file.write_text("a,b,c", encoding="utf-8")

        with pytest.raises(ValueError, match="latest_thread_event_id is required for thread fallback"):
            await send_file_message(
                client,
                "!room:localhost",
                file,
                thread_id="$root:localhost",
            )

    @pytest.mark.asyncio
    async def test_threaded_send_records_outbound_message_when_cache_available(self, tmp_path: Path) -> None:
        """Threaded file sends should write through to the conversation cache immediately."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/t1"), {})
        conversation_cache = AsyncMock()
        conversation_cache.notify_outbound_message = Mock()
        file = tmp_path / "data.csv"
        file.write_text("a,b,c", encoding="utf-8")

        with patch(
            "mindroom.matrix.client_delivery.send_message_result",
            new=AsyncMock(
                return_value=DeliveredMatrixEvent(
                    event_id="$evt:localhost",
                    content_sent={
                        "msgtype": "m.file",
                        "body": "data.csv",
                        "url": "mxc://localhost/t1",
                        "m.relates_to": {
                            "rel_type": "m.thread",
                            "event_id": "$root:localhost",
                            "is_falling_back": True,
                            "m.in_reply_to": {"event_id": "$precomputed:localhost"},
                        },
                    },
                ),
            ),
        ):
            event_id = await send_file_message(
                client,
                "!room:localhost",
                file,
                thread_id="$root:localhost",
                latest_thread_event_id="$precomputed:localhost",
                conversation_cache=conversation_cache,
            )

        assert event_id == "$evt:localhost"
        conversation_cache.notify_outbound_message.assert_called_once()
        record_args = conversation_cache.notify_outbound_message.call_args.args
        assert record_args[0] == "!room:localhost"
        assert record_args[1] == "$evt:localhost"
        assert record_args[2]["m.relates_to"]["event_id"] == "$root:localhost"
        assert record_args[2]["m.relates_to"]["m.in_reply_to"]["event_id"] == "$precomputed:localhost"

    @pytest.mark.asyncio
    async def test_returns_none_for_missing_file(self, tmp_path: Path) -> None:
        """Should return None when the file doesn't exist."""
        client = _mock_client()
        result = await send_file_message(
            client,
            "!room:localhost",
            tmp_path / "gone.txt",
        )
        assert result is None

    @pytest.mark.asyncio
    async def test_returns_none_for_encrypted_room_when_e2ee_support_is_unavailable(self, tmp_path: Path) -> None:
        """Encrypted-room file sends should fail early when nio E2EE support is disabled."""
        client = _mock_client(encrypted=True)

        file = tmp_path / "secret.bin"
        file.write_bytes(b"\x00" * 8)

        with (
            patch("mindroom.matrix.client_delivery.crypto.ENCRYPTION_ENABLED", False),
            patch("mindroom.matrix.client_delivery._upload_file_as_mxc", new_callable=AsyncMock) as mock_upload,
        ):
            result = await send_file_message(
                client,
                "!room:localhost",
                file,
            )

        assert result is None
        mock_upload.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_encrypted_file_waits_for_classic_room_cache_rebuild(self, tmp_path: Path) -> None:
        """Encrypted file delivery should survive nio rebuilding its Classic room cache."""
        client = _mock_client(encrypted=True)
        room = client.rooms.pop("!room:localhost")
        client.room_get_state_event.return_value = nio.RoomGetStateEventResponse(
            {"algorithm": "m.megolm.v1.aes-sha2"},
            "m.room.encryption",
            "",
            "!room:localhost",
        )
        client.upload.return_value = (_upload_response("mxc://localhost/recovered-file"), {})
        client.room_send.side_effect = [
            nio.SendRetryError("Classic Sync room state is being rebuilt."),
            nio.RoomSendResponse("$file:localhost", "!room:localhost"),
        ]
        file = tmp_path / "secret.bin"
        file.write_bytes(b"secret")

        async def restore_room_cache(_delay: float) -> None:
            client.rooms["!room:localhost"] = room

        with (
            patch("mindroom.matrix.client_delivery.crypto.ENCRYPTION_ENABLED", True),
            patch(
                "mindroom.matrix.client_delivery.crypto.attachments.encrypt_attachment",
                return_value=(
                    b"encrypted",
                    {
                        "key": {"k": "key"},
                        "iv": "iv",
                        "hashes": {"sha256": "hash"},
                    },
                ),
            ),
            patch("mindroom.matrix.client_delivery.asyncio.sleep", new=restore_room_cache),
        ):
            event_id = await send_file_message(client, "!room:localhost", file)

        assert event_id == "$file:localhost"
        client.upload.assert_awaited_once()
        assert client.room_send.await_count == 2

    @pytest.mark.asyncio
    async def test_caption_overrides_body(self, tmp_path: Path) -> None:
        """When caption is set, body should use it instead of filename."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/c1"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$evt:localhost", content_sent=content)

        file = tmp_path / "report.pdf"
        file.write_bytes(b"%PDF")

        with patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send):
            await send_file_message(
                client,
                "!room:localhost",
                file,
                caption="Q4 Report",
            )

        assert sent_content is not None
        assert sent_content["body"] == "Q4 Report"
        assert sent_content["filename"] == "report.pdf"


class TestSendAudioMessage:
    """Tests for direct Matrix audio voice sends."""

    @pytest.mark.asyncio
    async def test_sends_unencrypted_voice_audio_with_url(self) -> None:
        """Unencrypted voice audio should produce m.audio content with voice metadata."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/voice"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$voice:localhost", content_sent=content)

        with patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send):
            event_id = await send_audio_message(
                client,
                "!room:localhost",
                b"audio-bytes",
                mimetype="audio/mpeg",
                filename="reply.mp3",
                caption="Voice reply",
                duration_ms=1234,
                waveform=[0, 512, 1024],
            )

        assert event_id == "$voice:localhost"
        assert sent_content is not None
        assert sent_content["msgtype"] == "m.audio"
        assert sent_content["body"] == "Voice reply"
        assert sent_content["filename"] == "reply.mp3"
        assert sent_content["url"] == "mxc://localhost/voice"
        assert sent_content["info"] == {"size": 11, "mimetype": "audio/mpeg", "duration": 1234}
        assert sent_content["org.matrix.msc3245.voice"] == {}
        assert sent_content["org.matrix.msc1767.audio"] == {
            "duration": 1234,
            "waveform": [0, 512, 1024],
        }
        assert "file" not in sent_content

        received_event = MagicMock(spec=nio.RoomMessageAudio)
        received_event.body = sent_content["body"]
        received_event.source = {"content": sent_content}
        assert extract_media_caption(received_event, default="[Attached voice message]") == "Voice reply"

    @pytest.mark.asyncio
    async def test_sends_encrypted_voice_audio_with_file_payload(self) -> None:
        """Encrypted voice audio should produce an encrypted file payload."""
        client = _mock_client(encrypted=True)
        client.upload.return_value = (_upload_response("mxc://localhost/voice-enc"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$voice:localhost", content_sent=content)

        with (
            patch("mindroom.matrix.client_delivery.crypto.ENCRYPTION_ENABLED", True),
            patch(
                "mindroom.matrix.client_delivery.crypto.attachments.encrypt_attachment",
                return_value=(
                    b"encrypted",
                    {
                        "key": {"k": "k1"},
                        "iv": "iv1",
                        "hashes": {"sha256": "h1"},
                    },
                ),
            ),
            patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send),
        ):
            event_id = await send_audio_message(
                client,
                "!room:localhost",
                b"audio-bytes",
                mimetype="audio/mpeg",
                filename="reply.mp3",
                duration_ms=1234,
                waveform=[1024],
            )

        assert event_id == "$voice:localhost"
        assert sent_content is not None
        assert sent_content["msgtype"] == "m.audio"
        assert sent_content["body"] == "reply.mp3"
        assert sent_content["org.matrix.msc3245.voice"] == {}
        assert sent_content["org.matrix.msc1767.audio"] == {
            "duration": 1234,
            "waveform": [1024],
        }
        assert sent_content["file"]["url"] == "mxc://localhost/voice-enc"
        assert sent_content["file"]["mimetype"] == "audio/mpeg"
        assert "url" not in sent_content

    @pytest.mark.asyncio
    async def test_returns_none_for_encrypted_room_when_e2ee_support_is_unavailable(self) -> None:
        """Encrypted-room audio sends should fail early when nio E2EE support is disabled."""
        client = _mock_client(encrypted=True)

        with (
            patch("mindroom.matrix.client_delivery.crypto.ENCRYPTION_ENABLED", False),
            patch("mindroom.matrix.client_delivery._upload_media_bytes_as_mxc", new_callable=AsyncMock) as mock_upload,
        ):
            result = await send_audio_message(
                client,
                "!room:localhost",
                b"audio-bytes",
                mimetype="audio/mpeg",
            )

        assert result is None
        mock_upload.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_thread_relation_is_set(self) -> None:
        """Voice audio should preserve Matrix thread fallback metadata."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/thread-voice"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$voice:localhost", content_sent=content)

        with patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send):
            await send_audio_message(
                client,
                "!room:localhost",
                b"audio-bytes",
                mimetype="audio/ogg",
                duration_ms=1000,
                thread_id="$thread-root",
                latest_thread_event_id="$latest",
            )

        assert sent_content is not None
        assert sent_content["m.relates_to"] == {
            "rel_type": "m.thread",
            "event_id": "$thread-root",
            "is_falling_back": True,
            "m.in_reply_to": {"event_id": "$latest"},
        }

    @pytest.mark.asyncio
    async def test_audio_without_duration_omits_voice_marker(self) -> None:
        """Audio without voice details should not claim Matrix voice-note metadata."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/audio"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$audio:localhost", content_sent=content)

        with patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send):
            await send_audio_message(
                client,
                "!room:localhost",
                b"audio-bytes",
                mimetype="audio/mpeg",
                filename="reply.mp3",
            )

        assert sent_content is not None
        assert "org.matrix.msc3245.voice" not in sent_content
        assert "org.matrix.msc1767.audio" not in sent_content

    @pytest.mark.asyncio
    async def test_uncached_unencrypted_room_uses_raw_send_fallback(self) -> None:
        """Voice audio sends should match text delivery's uncached-room fallback."""
        client = AsyncMock(spec=nio.AsyncClient)
        client.rooms = {}
        client.olm = None
        client.access_token = "token"  # noqa: S105
        client.upload.return_value = (_upload_response("mxc://localhost/voice"), {})
        client.room_get_state_event.return_value = nio.RoomGetStateEventError(
            "not found",
            status_code="M_NOT_FOUND",
        )
        client._send.return_value = nio.RoomSendResponse("$voice:localhost", "!room:localhost")

        event_id = await send_audio_message(
            client,
            "!room:localhost",
            b"audio-bytes",
            mimetype="audio/ogg",
            filename="reply.opus",
            duration_ms=1000,
            waveform=[0],
        )

        assert event_id == "$voice:localhost"
        assert client.room_get_state_event.await_count == 2
        client.room_send.assert_not_awaited()
        client._send.assert_awaited_once()


class TestMsgtypeForMimetype:
    """Tests for _msgtype_for_mimetype."""

    @pytest.mark.parametrize(
        ("mimetype", "expected"),
        [
            ("image/png", "m.image"),
            ("image/jpeg", "m.image"),
            ("video/mp4", "m.video"),
            ("audio/ogg", "m.audio"),
            ("application/pdf", "m.file"),
            ("text/plain", "m.file"),
        ],
    )
    def test_mimetype_mapping(self, mimetype: str, expected: str) -> None:
        """Verify MIME type to Matrix msgtype mapping."""
        assert _msgtype_for_mimetype(mimetype) == expected


class TestSendMessageResult:
    """Tests for send_message_result."""

    @pytest.mark.asyncio
    async def test_returns_none_for_encrypted_room_when_e2ee_support_is_unavailable(self) -> None:
        """Encrypted-room text sends should fail before sidecar prep when nio E2EE support is disabled."""
        client = _mock_client(encrypted=True)

        with (
            patch("mindroom.matrix.client_delivery.crypto.ENCRYPTION_ENABLED", False),
            patch("mindroom.matrix.client_delivery.prepare_large_message", new_callable=AsyncMock) as mock_prepare,
        ):
            result = await send_message_result(
                client,
                "!room:localhost",
                {"body": "hello", "msgtype": "m.text"},
            )

        assert result is None
        mock_prepare.assert_not_awaited()
        client.room_send.assert_not_called()

    @pytest.mark.asyncio
    async def test_falls_back_to_raw_send_when_room_missing_from_cache_but_room_is_unencrypted(self) -> None:
        """Unencrypted sends should not depend on nio's room cache."""
        client = AsyncMock(spec=nio.AsyncClient)
        client.rooms = {}
        client.olm = None
        client.access_token = "token"  # noqa: S105
        client.room_send.return_value = nio.RoomSendResponse("$wrong:localhost", "!room:localhost")
        client.room_get_state_event.return_value = nio.RoomGetStateEventError(
            "not found",
            status_code="M_NOT_FOUND",
        )
        client._send.return_value = nio.RoomSendResponse("$evt:localhost", "!room:localhost")

        prepared_content = {"body": "hello", "msgtype": "m.text"}
        with patch("mindroom.matrix.client_delivery.prepare_large_message", new_callable=AsyncMock) as mock_prepare:
            mock_prepare.return_value = prepared_content
            result = await send_message_result(
                client,
                "!room:localhost",
                {"body": "hello", "msgtype": "m.text"},
            )

        assert result is not None
        assert result.event_id == "$evt:localhost"
        assert result.content_sent == prepared_content
        mock_prepare.assert_awaited_once_with(
            client,
            "!room:localhost",
            {"body": "hello", "msgtype": "m.text"},
            room_encrypted=False,
        )
        client.room_get_state_event.assert_awaited_once_with("!room:localhost", "m.room.encryption")
        client.room_send.assert_not_awaited()
        client._send.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_returns_none_for_uncached_encrypted_room_without_olm(self) -> None:
        """Encrypted sends should fail closed until nio has synced the room cache."""
        client = AsyncMock(spec=nio.AsyncClient)
        client.rooms = {}
        client.olm = None
        client.access_token = "token"  # noqa: S105
        client.room_send.return_value = nio.RoomSendResponse("$wrong:localhost", "!room:localhost")
        client.room_get_state_event.return_value = nio.RoomGetStateEventResponse(
            {"algorithm": "m.megolm.v1.aes-sha2"},
            "m.room.encryption",
            "",
            "!room:localhost",
        )

        with patch(
            "mindroom.matrix.client_delivery.prepare_large_message",
            new=AsyncMock(side_effect=lambda *_, **__: {"body": "hello", "msgtype": "m.text"}),
        ):
            result = await send_message_result(
                client,
                "!room:localhost",
                {"body": "hello", "msgtype": "m.text"},
            )

        assert result is None
        client.room_get_state_event.assert_awaited_once_with("!room:localhost", "m.room.encryption")
        client.room_send.assert_not_awaited()
        client._send.assert_not_awaited()

    @pytest.mark.asyncio
    async def test_treats_non_dict_room_cache_as_unknown_room(self) -> None:
        """Non-dict room caches should be treated as empty for plain sends."""
        client = AsyncMock(spec=nio.AsyncClient)
        client.rooms = AsyncMock()
        client.room_send.return_value = nio.RoomSendResponse("$evt:localhost", "!room:localhost")

        with patch(
            "mindroom.matrix.client_delivery.prepare_large_message",
            new=AsyncMock(side_effect=lambda *_, **__: {"body": "hello", "msgtype": "m.text"}),
        ):
            result = await send_message_result(
                client,
                "!room:localhost",
                {"body": "hello", "msgtype": "m.text"},
            )

        assert result is not None
        assert result.event_id == "$evt:localhost"
        client.room_send.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_returns_none_when_room_send_raises_unverified_device_error(self) -> None:
        """Local E2EE trust failures should not escape text send delivery."""
        client = _mock_client()
        client.room_send.side_effect = OlmUnverifiedDeviceError(
            SimpleNamespace(user_id="@private:localhost", device_id="SECRETDEVICE"),
            "unverified device @private:localhost SECRETDEVICE",
        )

        with (
            patch(
                "mindroom.matrix.client_delivery.prepare_large_message",
                new=AsyncMock(side_effect=lambda *_, **__: {"body": "hello", "msgtype": "m.text"}),
            ),
            patch("mindroom.matrix.client_delivery.logger.error") as mock_error,
        ):
            result = await send_message_result(
                client,
                "!room:localhost",
                {"body": "hello", "msgtype": "m.text"},
            )

        assert result is None
        mock_error.assert_called_once()
        assert mock_error.call_args.args == ("matrix_message_delivery_exception",)
        assert mock_error.call_args.kwargs["exception_type"] == "OlmUnverifiedDeviceError"
        assert mock_error.call_args.kwargs["error_message"] == (
            "Matrix encrypted delivery rejected by local device trust policy."
        )

    @pytest.mark.asyncio
    async def test_edit_returns_none_when_room_send_raises_unverified_device_error(self) -> None:
        """Local E2EE trust failures should not escape edit delivery."""
        client = _mock_client()
        client.room_send.side_effect = OlmUnverifiedDeviceError(
            SimpleNamespace(user_id="@private:localhost", device_id="SECRETDEVICE"),
            "unverified device @private:localhost SECRETDEVICE",
        )

        with (
            patch(
                "mindroom.matrix.client_delivery.prepare_large_message",
                new=AsyncMock(side_effect=lambda *_, **__: {"body": "hello", "msgtype": "m.text"}),
            ),
            patch("mindroom.matrix.client_delivery.logger.error") as mock_error,
        ):
            result = await edit_message_result(
                client,
                "!room:localhost",
                "$placeholder",
                {"body": "hello", "msgtype": "m.text"},
                "hello",
            )

        assert result is None
        assert mock_error.call_args.args == ("matrix_message_delivery_exception",)
        assert mock_error.call_args.kwargs["operation"] == "edit_message"
        assert mock_error.call_args.kwargs["exception_type"] == "OlmUnverifiedDeviceError"

    @pytest.mark.asyncio
    async def test_sync_recovery_retry_reuses_one_prepared_payload(self) -> None:
        """Opt-in recovery retries should reuse the exact prepared wire payload."""
        client = _mock_client()
        error = nio.SendRetryError("Room timeline recovery is still pending.")
        response = nio.RoomSendResponse("$evt:localhost", "!room:localhost")
        client.room_send.side_effect = [error, error, error, response]
        prepared = {"body": "prepared", "msgtype": "m.text"}
        prepare = AsyncMock(return_value=prepared)
        sleep_mock = AsyncMock()

        with (
            patch("mindroom.matrix.client_delivery.prepare_large_message", new=prepare),
            patch("mindroom.matrix.client_delivery.asyncio.sleep", new=sleep_mock),
            patch("mindroom.matrix.client_delivery.logger.warning") as warning_mock,
        ):
            result = await send_message_result(
                client,
                "!room:localhost",
                {"body": "hello", "msgtype": "m.text"},
                retry_sync_recovery=True,
            )

        assert result is not None
        assert result.event_id == "$evt:localhost"
        prepare.assert_awaited_once()
        assert client.room_send.await_count == 4
        assert all(call.kwargs["content"] is prepared for call in client.room_send.await_args_list)
        assert sleep_mock.await_count == 3
        warning_mock.assert_called_once()

    @pytest.mark.asyncio
    async def test_encrypted_send_waits_for_room_cache_rebuild_by_default(self) -> None:
        """Every encrypted delivery waits while a rejected sync rebuilds room state."""
        client = _mock_client(encrypted=True)
        room = client.rooms.pop("!room:localhost")
        client.room_get_state_event.return_value = nio.RoomGetStateEventResponse(
            {"algorithm": "m.megolm.v1.aes-sha2"},
            "m.room.encryption",
            "",
            "!room:localhost",
        )
        client.room_send.side_effect = [
            nio.SendRetryError("Classic Sync room state is being rebuilt."),
            nio.RoomSendResponse("$evt:localhost", "!room:localhost"),
        ]

        async def restore_room_cache(_delay: float) -> None:
            client.rooms["!room:localhost"] = room

        with (
            patch(
                "mindroom.matrix.client_delivery.prepare_large_message",
                new=AsyncMock(return_value={"body": "prepared", "msgtype": "m.text"}),
            ),
            patch("mindroom.matrix.client_delivery.asyncio.sleep", new=restore_room_cache),
        ):
            result = await send_message_result(
                client,
                "!room:localhost",
                {"body": "hello", "msgtype": "m.text"},
            )

        assert result is not None
        assert result.event_id == "$evt:localhost"
        assert client.room_send.await_count == 2

    @pytest.mark.asyncio
    async def test_encrypted_sidecar_keeps_cached_state_across_reset_race(self) -> None:
        """A cache reset after inspection cannot downgrade sidecar encryption."""
        client = _mock_client(encrypted=True)
        room = client.rooms["!room:localhost"]
        client.room_send.side_effect = [
            nio.SendRetryError("Classic Sync room state is being rebuilt."),
            nio.RoomSendResponse("$evt:localhost", "!room:localhost"),
        ]

        async def prepare(
            _client: nio.AsyncClient,
            _room_id: str,
            _content: dict[str, object],
            *,
            room_encrypted: bool | None,
        ) -> dict[str, str]:
            assert room_encrypted is True
            client.rooms.clear()
            return {"body": "prepared", "msgtype": "m.text"}

        async def restore_room_cache(_delay: float) -> None:
            client.rooms["!room:localhost"] = room

        with (
            patch("mindroom.matrix.client_delivery.prepare_large_message", new=prepare),
            patch("mindroom.matrix.client_delivery.asyncio.sleep", new=restore_room_cache),
        ):
            result = await send_message_result(
                client,
                "!room:localhost",
                {"body": "hello", "msgtype": "m.text"},
            )

        assert result is not None
        assert result.event_id == "$evt:localhost"

    @pytest.mark.parametrize("is_edit", [False, True], ids=["send", "edit"])
    @pytest.mark.asyncio
    async def test_sync_recovery_encrypts_oversized_sidecar_before_room_cache_rebuild(
        self,
        is_edit: bool,
    ) -> None:
        """Remote encryption state must protect a sidecar while nio rebuilds its room cache."""
        client = _mock_client(encrypted=True)
        room = client.rooms.pop("!room:localhost")
        client.room_get_state_event.return_value = nio.RoomGetStateEventResponse(
            {"algorithm": "m.megolm.v1.aes-sha2"},
            "m.room.encryption",
            "",
            "!room:localhost",
        )
        client.room_send.side_effect = [
            nio.SendRetryError("Classic Sync room state is being rebuilt."),
            nio.RoomSendResponse("$evt:localhost", "!room:localhost"),
        ]
        uploaded: list[tuple[bytes, str, str]] = []

        async def upload(**kwargs: object) -> tuple[nio.UploadResponse, None]:
            data_provider = cast(
                "Callable[[int | None, int | None], BinaryIO]",
                kwargs["data_provider"],
            )
            uploaded.append(
                (
                    data_provider(None, None).read(),
                    str(kwargs["content_type"]),
                    str(kwargs["filename"]),
                ),
            )
            return nio.UploadResponse.from_dict(
                {"content_uri": "mxc://localhost/encrypted-sidecar"},
            ), None

        async def restore_room_cache(_delay: float) -> None:
            client.rooms["!room:localhost"] = room

        client.upload.side_effect = upload
        secret_body = "sensitive response " * 10_000
        encrypted_sidecar = b"encrypted-sidecar-bytes"
        encryption_keys = {
            "key": {"k": "sidecar-key"},
            "iv": "sidecar-iv",
            "hashes": {"sha256": "sidecar-hash"},
        }
        with (
            patch(
                "mindroom.matrix.large_messages.crypto.attachments.encrypt_attachment",
                return_value=(encrypted_sidecar, encryption_keys),
            ) as encrypt_attachment,
            patch("mindroom.matrix.client_delivery.asyncio.sleep", new=restore_room_cache),
        ):
            if is_edit:
                result = await edit_message_result(
                    client,
                    "!room:localhost",
                    "$original:localhost",
                    {"body": secret_body, "msgtype": "m.text"},
                    secret_body,
                    retry_sync_recovery=True,
                )
            else:
                result = await send_message_result(
                    client,
                    "!room:localhost",
                    {"body": secret_body, "msgtype": "m.text"},
                    retry_sync_recovery=True,
                )

        assert result is not None
        assert result.event_id == "$evt:localhost"
        assert uploaded == [(encrypted_sidecar, "application/octet-stream", "message-content.json.enc")]
        encrypt_attachment.assert_called_once()
        inner = result.content_sent["m.new_content"] if is_edit else result.content_sent
        file_info = inner["file"]
        assert file_info["url"] == "mxc://localhost/encrypted-sidecar"
        assert file_info["key"] == encryption_keys["key"]
        assert file_info["iv"] == encryption_keys["iv"]
        assert file_info["hashes"] == encryption_keys["hashes"]
        assert file_info["v"] == "v2"
        assert file_info["mimetype"] == "application/json"
        assert isinstance(file_info["size"], int)
        assert file_info["size"] > 0
        assert "url" not in inner

    @pytest.mark.asyncio
    async def test_sync_recovery_retry_bounds_a_stuck_second_send(self) -> None:
        """The recovery deadline should also bound a retry stuck in transport."""
        client = _mock_client()
        original_error = nio.SendRetryError("Room timeline recovery is still pending.")
        send_count = 0
        prepared = {"body": "prepared", "msgtype": "m.text"}
        prepare = AsyncMock(return_value=prepared)

        async def send(**_kwargs: object) -> nio.RoomSendResponse:
            nonlocal send_count
            send_count += 1
            if send_count == 1:
                raise original_error
            await asyncio.Event().wait()
            raise AssertionError

        client.room_send.side_effect = send
        with (
            patch("mindroom.matrix.client_delivery.prepare_large_message", new=prepare),
            patch("mindroom.matrix.client_delivery.asyncio.sleep", new=AsyncMock()),
            patch("mindroom.matrix.client_delivery._SYNC_RECOVERY_RETRY_TIMEOUT_SECONDS", new=0.01),
            pytest.raises(nio.SendRetryError) as raised,
        ):
            await asyncio.wait_for(
                send_message_result(
                    client,
                    "!room:localhost",
                    {"body": "hello", "msgtype": "m.text"},
                    retry_sync_recovery=True,
                ),
                0.5,
            )

        assert raised.value is original_error
        prepare.assert_awaited_once()
        assert send_count == 2

    @pytest.mark.asyncio
    async def test_sync_recovery_retry_sleep_is_cancellation_aware(self) -> None:
        """Cancellation should interrupt recovery backoff immediately."""
        client = _mock_client()
        client.room_send.side_effect = nio.SendRetryError("Room timeline recovery is still pending.")
        sleep_started = asyncio.Event()

        async def wait_forever(_delay: float) -> None:
            sleep_started.set()
            await asyncio.Event().wait()

        with (
            patch(
                "mindroom.matrix.client_delivery.prepare_large_message",
                new=AsyncMock(return_value={"body": "prepared", "msgtype": "m.text"}),
            ),
            patch("mindroom.matrix.client_delivery.asyncio.sleep", new=wait_forever),
        ):
            task = asyncio.create_task(
                send_message_result(
                    client,
                    "!room:localhost",
                    {"body": "hello", "msgtype": "m.text"},
                    retry_sync_recovery=True,
                ),
            )
            await sleep_started.wait()
            task.cancel()
            with pytest.raises(asyncio.CancelledError):
                await task

    @pytest.mark.asyncio
    async def test_sync_recovery_error_keeps_default_send_contract(self) -> None:
        """Ordinary sends should normalize a recovery window that expires."""
        client = _mock_client()
        recovery_error = nio.SendRetryError("Room timeline recovery is still pending.")
        client.room_send.side_effect = recovery_error

        with (
            patch(
                "mindroom.matrix.client_delivery.prepare_large_message",
                new=AsyncMock(side_effect=lambda *_, **__: {"body": "hello", "msgtype": "m.text"}),
            ),
            patch(
                "mindroom.matrix.client_delivery._retry_prepared_room_message_after_sync_recovery",
                new=AsyncMock(side_effect=recovery_error),
            ) as retry,
            patch("mindroom.matrix.client_delivery.logger.error") as mock_error,
        ):
            result = await send_message_result(
                client,
                "!room:localhost",
                {"body": "hello", "msgtype": "m.text"},
            )

        assert result is None
        retry.assert_awaited_once()
        mock_error.assert_called_once()
        assert mock_error.call_args.args == ("matrix_message_delivery_exception",)
        assert mock_error.call_args.kwargs["exception_type"] == "SendRetryError"

    @pytest.mark.asyncio
    async def test_sync_recovery_error_keeps_default_edit_contract(self) -> None:
        """Ordinary edits should normalize a recovery window that expires."""
        client = _mock_client()
        recovery_error = nio.SendRetryError("Room timeline recovery is still pending.")
        client.room_send.side_effect = recovery_error

        with (
            patch(
                "mindroom.matrix.client_delivery.prepare_large_message",
                new=AsyncMock(side_effect=lambda *_, **__: {"body": "hello", "msgtype": "m.text"}),
            ),
            patch(
                "mindroom.matrix.client_delivery._retry_prepared_room_message_after_sync_recovery",
                new=AsyncMock(side_effect=recovery_error),
            ) as retry,
            patch("mindroom.matrix.client_delivery.logger.error") as mock_error,
        ):
            result = await edit_message_result(
                client,
                "!room:localhost",
                "$placeholder",
                {"body": "hello", "msgtype": "m.text"},
                "hello",
            )

        assert result is None
        retry.assert_awaited_once()
        mock_error.assert_called_once()
        assert mock_error.call_args.args == ("matrix_message_delivery_exception",)
        assert mock_error.call_args.kwargs["operation"] == "edit_message"
        assert mock_error.call_args.kwargs["exception_type"] == "SendRetryError"

    @pytest.mark.asyncio
    async def test_unexpected_room_send_exception_logs_generic_sanitized_message(self) -> None:
        """Unexpected local delivery exceptions should log type plus a safe generic message."""
        client = _mock_client()
        client.room_send.side_effect = RuntimeError(
            "failed token=supersecret for @private:localhost via https://matrix.example/private",
        )

        with (
            patch(
                "mindroom.matrix.client_delivery.prepare_large_message",
                new=AsyncMock(side_effect=lambda *_, **__: {"body": "hello", "msgtype": "m.text"}),
            ),
            patch("mindroom.matrix.client_delivery.logger.error") as mock_error,
        ):
            result = await send_message_result(
                client,
                "!room:localhost",
                {"body": "hello", "msgtype": "m.text"},
            )

        assert result is None
        assert mock_error.call_args.args == ("matrix_message_delivery_exception",)
        assert mock_error.call_args.kwargs["exception_type"] == "RuntimeError"
        assert mock_error.call_args.kwargs["error_message"] == "Matrix delivery raised an unexpected local exception."


class TestJoinRoom:
    """Tests for join_room."""

    @pytest.mark.asyncio
    async def test_treats_non_dict_room_cache_as_uninitialized(self) -> None:
        """Join should succeed without mutating when the room cache is not a real dict."""
        client = AsyncMock(spec=nio.AsyncClient)
        client.rooms = AsyncMock()
        client.user_id = "@mindroom_test:localhost"
        client.join.return_value = nio.JoinResponse("!room:localhost")

        joined = await join_room(client, "!room:localhost")

        assert joined is RoomJoinOutcome.JOINED
        client.join.assert_awaited_once_with("!room:localhost")

    @pytest.mark.asyncio
    @pytest.mark.parametrize(
        ("response", "expected"),
        [
            (
                nio.JoinError("forbidden", "M_FORBIDDEN"),
                RoomJoinOutcome.RETRYABLE_FAILURE,
            ),
            (
                nio.JoinError("not found", "M_NOT_FOUND"),
                RoomJoinOutcome.RETRYABLE_FAILURE,
            ),
            (
                nio.JoinError("bad state", "M_BAD_STATE"),
                RoomJoinOutcome.TERMINAL_FAILURE,
            ),
            (
                nio.JoinError("busy", "M_LIMIT_EXCEEDED"),
                RoomJoinOutcome.RETRYABLE_FAILURE,
            ),
            (
                nio.RoomInviteError("forbidden", "M_FORBIDDEN"),
                RoomJoinOutcome.RETRYABLE_FAILURE,
            ),
        ],
        ids=[
            "ambiguous-forbidden-join-error",
            "ambiguous-not-found-join-error",
            "terminal-join-error",
            "retryable-join-error",
            "non-join-error",
        ],
    )
    async def test_classifies_join_failures(
        self,
        response: nio.Response,
        expected: RoomJoinOutcome,
    ) -> None:
        """Only explicit terminal JoinError codes may suppress later retries."""
        client = AsyncMock(spec=nio.AsyncClient)
        client.join.return_value = response

        outcome = await join_room(client, "!room:localhost")

        assert outcome is expected


class TestSendFileMessageMsgtype:
    """Tests for send_file_message msgtype selection."""

    @pytest.mark.asyncio
    async def test_image_uses_m_image_msgtype(self, tmp_path: Path) -> None:
        """Image files should be sent as m.image without filename field."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/img1"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$evt:localhost", content_sent=content)

        file = tmp_path / "photo.png"
        file.write_bytes(b"\x89PNG\r\n\x1a\n")

        with patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send):
            event_id = await send_file_message(
                client,
                "!room:localhost",
                file,
            )

        assert event_id == "$evt:localhost"
        assert sent_content is not None
        assert sent_content["msgtype"] == "m.image"
        assert sent_content["body"] == "photo.png"
        assert "filename" not in sent_content
        assert sent_content["url"] == "mxc://localhost/img1"

    @pytest.mark.asyncio
    async def test_video_uses_m_video_msgtype(self, tmp_path: Path) -> None:
        """Video files should be sent as m.video."""
        client = _mock_client(encrypted=False)
        client.upload.return_value = (_upload_response("mxc://localhost/vid1"), {})

        sent_content: dict | None = None

        async def capture_send(
            _client: object,
            _room: str,
            content: dict,
        ) -> DeliveredMatrixEvent:
            nonlocal sent_content
            sent_content = content
            return DeliveredMatrixEvent(event_id="$evt:localhost", content_sent=content)

        file = tmp_path / "clip.mp4"
        file.write_bytes(b"\x00\x00\x00\x1cftyp")

        with patch("mindroom.matrix.client_delivery.send_message_result", side_effect=capture_send):
            event_id = await send_file_message(
                client,
                "!room:localhost",
                file,
            )

        assert event_id == "$evt:localhost"
        assert sent_content is not None
        assert sent_content["msgtype"] == "m.video"
