import re
import time
from datetime import timedelta
from io import StringIO
from typing import TYPE_CHECKING, Any
from unittest import mock

import orjson
import time_machine
from django.test import override_settings
from django.utils.timezone import now as timezone_now

from zerver.actions.scheduled_messages import (
    SCHEDULED_MESSAGE_LATE_CUTOFF_MINUTES,
    try_deliver_one_scheduled_message,
)
from zerver.actions.users import change_user_is_active
from zerver.lib.message import is_message_to_self
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import most_recent_message
from zerver.lib.timestamp import timestamp_to_datetime
from zerver.models import Attachment, Message, Recipient, ScheduledMessage, UserMessage
from zerver.models.recipients import get_or_create_direct_message_group

if TYPE_CHECKING:
    from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse


class ScheduledMessageTest(ZulipTestCase):
    def last_scheduled_message(self) -> ScheduledMessage:
        return ScheduledMessage.objects.all().order_by("-id")[0]

    def get_scheduled_message(self, id: str) -> ScheduledMessage:
        return ScheduledMessage.objects.get(id=id)

    def do_schedule_message(
        self,
        msg_type: str,
        to: int | list[str] | list[int],
        msg: str,
        scheduled_delivery_timestamp: int,
    ) -> "TestHttpResponse":
        self.login("hamlet")

        topic_name = ""
        if msg_type in ["stream", "channel"]:
            topic_name = "Test topic"

        payload = {
            "type": msg_type,
            "to": orjson.dumps(to).decode(),
            "content": msg,
            "topic": topic_name,
            "scheduled_delivery_timestamp": scheduled_delivery_timestamp,
        }

        result = self.client_post("/json/scheduled_messages", payload)
        return result

    def test_schedule_message(self) -> None:
        content = "Test message"
        scheduled_delivery_timestamp = int(time.time() + 86400)
        verona_stream_id = self.get_stream_id("Verona")

        # Scheduling a message to a stream you are subscribed is successful.
        result = self.do_schedule_message(
            "channel", verona_stream_id, f"{content} 1", scheduled_delivery_timestamp
        )
        scheduled_message = self.last_scheduled_message()
        self.assert_json_success(result)
        self.assertEqual(scheduled_message.content, "Test message 1")
        self.assertEqual(scheduled_message.rendered_content, "<p>Test message 1</p>")
        self.assertEqual(scheduled_message.topic_name(), "Test topic")
        self.assertEqual(
            scheduled_message.scheduled_timestamp,
            timestamp_to_datetime(scheduled_delivery_timestamp),
        )

        # Scheduling a direct message with user IDs is successful.
        othello = self.example_user("othello")
        result = self.do_schedule_message(
            "direct", [othello.id], f"{content} 3", scheduled_delivery_timestamp
        )
        scheduled_message = self.last_scheduled_message()
        self.assert_json_success(result)
        self.assertEqual(scheduled_message.content, "Test message 3")
        self.assertEqual(scheduled_message.rendered_content, "<p>Test message 3</p>")
        self.assertEqual(
            scheduled_message.scheduled_timestamp,
            timestamp_to_datetime(scheduled_delivery_timestamp),
        )
        self.assertEqual(scheduled_message.topic_name(), Message.DM_TOPIC)

        # Cannot schedule a direct message with user emails.
        result = self.do_schedule_message(
            "direct", [othello.email], f"{content} 4", scheduled_delivery_timestamp
        )
        self.assert_json_error(result, 'to["int"] is not an integer')

    def create_scheduled_message(self) -> None:
        content = "Test message"
        scheduled_delivery_datetime = timezone_now() + timedelta(minutes=5)
        scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
        verona_stream_id = self.get_stream_id("Verona")
        result = self.do_schedule_message(
            "channel", verona_stream_id, f"{content} 1", scheduled_delivery_timestamp
        )
        self.assert_json_success(result)

    def assert_scheduled_message_delivered(
        self, scheduled_message: ScheduledMessage, recipient: Recipient | None
    ) -> None:
        sender = self.example_user("hamlet")

        # mock current time to be greater than the scheduled time, so that the `scheduled_message` can be sent.
        more_than_scheduled_delivery_datetime = scheduled_message.scheduled_timestamp + timedelta(
            minutes=1
        )

        with (
            time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
            self.assertLogs(level="INFO") as logs,
        ):
            result = try_deliver_one_scheduled_message()
            self.assertTrue(result)
            self.assertEqual(
                logs.output,
                [
                    f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})"
                ],
            )
            scheduled_message.refresh_from_db()

            assert isinstance(scheduled_message.delivered_message_id, int)
            self.assertEqual(scheduled_message.delivered, True)
            self.assertEqual(scheduled_message.failed, False)
            self.assertEqual(scheduled_message.recipient, recipient)
            self.assertEqual(scheduled_message.sender, sender)

            delivered_message = Message.objects.get(id=scheduled_message.delivered_message_id)
            self.assertEqual(delivered_message.content, scheduled_message.content)
            self.assertEqual(delivered_message.rendered_content, scheduled_message.rendered_content)
            self.assertEqual(delivered_message.topic_name(), scheduled_message.topic_name())
            self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
            self.assertEqual(delivered_message.recipient, scheduled_message.recipient)
            self.assertEqual(delivered_message.sender, scheduled_message.sender)

            sender_user_message = UserMessage.objects.get(
                message_id=scheduled_message.delivered_message_id, user_profile_id=sender.id
            )
            self.assertEqual(
                sender_user_message.flags.read, not is_message_to_self(delivered_message)
            )

    def test_successful_deliver_stream_scheduled_message(self) -> None:
        # No scheduled message
        result = try_deliver_one_scheduled_message()
        self.assertFalse(result)

        self.create_scheduled_message()
        scheduled_message = self.last_scheduled_message()

        stream_id = self.get_stream_id("Verona")
        recipient = Recipient.objects.get(type=Recipient.STREAM, type_id=stream_id)
        self.assert_scheduled_message_delivered(scheduled_message, recipient)

    def test_successful_deliver_direct_scheduled_message_to_other(self) -> None:
        # No scheduled message
        self.assertFalse(try_deliver_one_scheduled_message())

        content = "Test message"
        scheduled_delivery_datetime = timezone_now() + timedelta(minutes=5)
        scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
        othello = self.example_user("othello")
        response = self.do_schedule_message(
            "direct", [othello.id], f"{content} 3", scheduled_delivery_timestamp
        )
        self.assert_json_success(response)
        scheduled_message = self.last_scheduled_message()

        self.assert_scheduled_message_delivered(scheduled_message, recipient=othello.recipient)

        # Check error is sent if an edit happens after the scheduled
        # message is successfully sent.
        new_delivery_datetime = timezone_now() + timedelta(minutes=7)
        new_delivery_timestamp = int(new_delivery_datetime.timestamp())
        content = "New message content"
        payload = {
            "content": content,
            "scheduled_delivery_timestamp": new_delivery_timestamp,
        }
        updated_response = self.client_patch(
            f"/json/scheduled_messages/{scheduled_message.id}", payload
        )
        self.assert_json_error(updated_response, "Scheduled message was already sent")

    @override_settings(PREFER_DIRECT_MESSAGE_GROUP=True)
    def test_successful_deliver_personal_scheduled_message_using_direct_message_group(self) -> None:
        # No scheduled message
        self.assertFalse(try_deliver_one_scheduled_message())

        content = "Test message"
        scheduled_delivery_datetime = timezone_now() + timedelta(minutes=5)
        scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
        sender = self.example_user("hamlet")
        othello = self.example_user("othello")

        # Create a direct message group for the sender and othello.
        direct_message_group = get_or_create_direct_message_group(id_list=[sender.id, othello.id])

        response = self.do_schedule_message(
            "direct", [othello.id], f"{content} 4", scheduled_delivery_timestamp
        )
        self.assert_json_success(response)
        scheduled_message = self.last_scheduled_message()

        # mock current time to be greater than the scheduled time.
        more_than_scheduled_delivery_datetime = scheduled_delivery_datetime + timedelta(minutes=1)

        with (
            time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
            self.assertLogs(level="INFO") as logs,
        ):
            result = try_deliver_one_scheduled_message()
            self.assertTrue(result)
            self.assertEqual(
                logs.output,
                [
                    f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})"
                ],
            )
            scheduled_message.refresh_from_db()
            assert isinstance(scheduled_message.delivered_message_id, int)
            self.assertEqual(scheduled_message.delivered, True)
            self.assertEqual(scheduled_message.failed, False)
            delivered_message = Message.objects.get(id=scheduled_message.delivered_message_id)
            self.assertEqual(delivered_message.content, scheduled_message.content)
            self.assertEqual(delivered_message.recipient, direct_message_group.recipient)
            self.assertEqual(delivered_message.rendered_content, scheduled_message.rendered_content)
            self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
            sender_user_message = UserMessage.objects.get(
                message_id=scheduled_message.delivered_message_id, user_profile_id=sender.id
            )
            self.assertTrue(sender_user_message.flags.read)

    def test_successful_deliver_direct_scheduled_message_to_self(self) -> None:
        # No scheduled message
        self.assertFalse(try_deliver_one_scheduled_message())

        content = "Test message to self"
        scheduled_delivery_datetime = timezone_now() + timedelta(minutes=5)
        scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
        sender = self.example_user("hamlet")
        response = self.do_schedule_message(
            "direct", [sender.id], content, scheduled_delivery_timestamp
        )
        self.assert_json_success(response)
        scheduled_message = self.last_scheduled_message()

        self.assert_scheduled_message_delivered(scheduled_message, recipient=sender.recipient)

    @override_settings(PREFER_DIRECT_MESSAGE_GROUP=True)
    def test_successful_deliver_direct_scheduled_message_to_self_using_direct_message_group(
        self,
    ) -> None:
        # No scheduled message
        self.assertFalse(try_deliver_one_scheduled_message())

        content = "Test message to self"
        scheduled_delivery_datetime = timezone_now() + timedelta(minutes=5)
        scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
        sender = self.example_user("hamlet")

        # Create a direct message group for the sender.
        direct_message_group = get_or_create_direct_message_group(id_list=[sender.id])

        response = self.do_schedule_message(
            "direct", [sender.id], content, scheduled_delivery_timestamp
        )
        self.assert_json_success(response)
        scheduled_message = self.last_scheduled_message()

        self.assert_scheduled_message_delivered(
            scheduled_message, recipient=direct_message_group.recipient
        )

    def verify_deliver_scheduled_message_failure(
        self, scheduled_message: ScheduledMessage, expected_failure_message: str
    ) -> None:
        with self.assertLogs(level="INFO") as logs:
            result = try_deliver_one_scheduled_message()
        self.assertTrue(result)
        scheduled_message.refresh_from_db()
        self.assertEqual(scheduled_message.failure_message, expected_failure_message)
        self.assertEqual(
            logs.output,
            [
                f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})",
                f"INFO:root:Failed with message: {scheduled_message.failure_message}",
            ],
        )
        self.assertTrue(scheduled_message.failed)

    def test_too_late_to_deliver_scheduled_message(self) -> None:
        expected_failure_message = "Message could not be sent at the scheduled time."
        self.create_scheduled_message()
        scheduled_message = self.last_scheduled_message()

        too_late_to_send_message_datetime = scheduled_message.scheduled_timestamp + timedelta(
            minutes=SCHEDULED_MESSAGE_LATE_CUTOFF_MINUTES + 1
        )

        with time_machine.travel(too_late_to_send_message_datetime, tick=False):
            self.verify_deliver_scheduled_message_failure(
                scheduled_message, expected_failure_message
            )

        # Verify that the user was sent a message informing them about
        # the failed scheduled message.
        realm = scheduled_message.realm
        msg = most_recent_message(scheduled_message.sender)
        self.assertEqual(msg.recipient.type, msg.recipient.PERSONAL)
        self.assertEqual(msg.sender_id, self.notification_bot(realm).id)
        self.assertIn(expected_failure_message, msg.content)

    def test_realm_deactivated_failed_to_deliver_scheduled_message(self) -> None:
        expected_failure_message = "This organization has been deactivated"
        self.create_scheduled_message()
        scheduled_message = self.last_scheduled_message()

        # Verify realm isn't deactivated and get user's most recent
        # message.
        self.assertFalse(scheduled_message.realm.deactivated)
        message_before_deactivation = most_recent_message(scheduled_message.sender)

        more_than_scheduled_delivery_datetime = scheduled_message.scheduled_timestamp + timedelta(
            minutes=1
        )

        with time_machine.travel(more_than_scheduled_delivery_datetime, tick=False):
            scheduled_message = self.last_scheduled_message()
            scheduled_message.realm.deactivated = True
            scheduled_message.realm.save()
            self.verify_deliver_scheduled_message_failure(
                scheduled_message, expected_failure_message
            )

        # Verify that no failed scheduled message notification was sent.
        self.assertTrue(scheduled_message.realm.deactivated)
        message_after_deactivation = most_recent_message(scheduled_message.sender)
        self.assertEqual(message_after_deactivation.content, message_before_deactivation.content)
        self.assertNotIn(expected_failure_message, message_after_deactivation.content)

    def test_sender_deactivated_failed_to_deliver_scheduled_message(self) -> None:
        expected_failure_message = "Account is deactivated"
        self.create_scheduled_message()
        scheduled_message = self.last_scheduled_message()

        # Verify user isn't deactivated and get user's most recent
        # message.
        self.assertTrue(scheduled_message.sender.is_active)
        message_before_deactivation = most_recent_message(scheduled_message.sender)

        more_than_scheduled_delivery_datetime = scheduled_message.scheduled_timestamp + timedelta(
            minutes=1
        )

        with time_machine.travel(more_than_scheduled_delivery_datetime, tick=False):
            scheduled_message = self.last_scheduled_message()
            change_user_is_active(scheduled_message.sender, False)
            self.verify_deliver_scheduled_message_failure(
                scheduled_message, expected_failure_message
            )

        # Verify that no failed scheduled message notification was sent.
        self.assertFalse(scheduled_message.sender.is_active)
        message_after_deactivation = most_recent_message(scheduled_message.sender)
        self.assertEqual(message_after_deactivation.content, message_before_deactivation.content)
        self.assertNotIn(expected_failure_message, message_after_deactivation.content)

    def test_failed_to_deliver_scheduled_message_unknown_exception(
        self,
    ) -> None:
        self.create_scheduled_message()
        scheduled_message = self.last_scheduled_message()

        more_than_scheduled_delivery_datetime = scheduled_message.scheduled_timestamp + timedelta(
            minutes=1
        )
        with (
            mock.patch(
                "zerver.actions.scheduled_messages.send_scheduled_message",
                side_effect=Exception(),
            ),
            time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
        ):
            scheduled_message = self.last_scheduled_message()
            with self.assertLogs(level="INFO") as logs:
                result = try_deliver_one_scheduled_message()
            self.assertTrue(result)
            scheduled_message.refresh_from_db()
            self.assert_length(logs.output, 2)
            self.assertEqual(
                logs.output[0],
                f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})",
            )
            self.assertTrue(
                logs.output[1].startswith(
                    f"ERROR:root:Unexpected error sending scheduled message {scheduled_message.id} (sent: {scheduled_message.delivered})\nTraceback (most recent call last)"
                )
            )
            self.assertTrue(scheduled_message.failed)

        # Verify that the user was sent a message informing them about
        # the failed scheduled message.
        realm = scheduled_message.realm
        msg = most_recent_message(scheduled_message.sender)
        self.assertEqual(msg.recipient.type, msg.recipient.PERSONAL)
        self.assertEqual(msg.sender_id, self.notification_bot(realm).id)
        self.assertIn("Internal server error", msg.content)

    def test_editing_failed_send_scheduled_message(self) -> None:
        expected_failure_message = "Message could not be sent at the scheduled time."
        self.create_scheduled_message()
        scheduled_message = self.last_scheduled_message()

        too_late_to_send_message_datetime = scheduled_message.scheduled_timestamp + timedelta(
            minutes=SCHEDULED_MESSAGE_LATE_CUTOFF_MINUTES + 1
        )

        with time_machine.travel(too_late_to_send_message_datetime, tick=False):
            self.verify_deliver_scheduled_message_failure(
                scheduled_message, expected_failure_message
            )

            # After verifying the scheduled message failed to be sent:
            # Confirm not updating the scheduled delivery timestamp for
            # the scheduled message with that ID returns an error.
            payload_without_timestamp = {"topic": "Failed to send"}
            response = self.client_patch(
                f"/json/scheduled_messages/{scheduled_message.id}", payload_without_timestamp
            )
            self.assert_json_error(response, "Scheduled delivery time must be in the future.")

        # Editing the scheduled message with that ID for a future time is
        # successful and resets the `failed` and `failure_message` fields.
        new_delivery_datetime = timezone_now() + timedelta(minutes=60)
        new_delivery_timestamp = int(new_delivery_datetime.timestamp())
        scheduled_message_id = scheduled_message.id
        payload_with_timestamp = {
            "scheduled_delivery_timestamp": new_delivery_timestamp,
        }
        response = self.client_patch(
            f"/json/scheduled_messages/{scheduled_message.id}", payload_with_timestamp
        )
        self.assert_json_success(response)

        scheduled_message = self.last_scheduled_message()
        self.assertEqual(scheduled_message.id, scheduled_message_id)
        self.assertFalse(scheduled_message.failed)
        self.assertIsNone(scheduled_message.failure_message)

    def test_scheduling_in_past(self) -> None:
        # Scheduling a message in past should fail.
        content = "Test message"
        verona_stream_id = self.get_stream_id("Verona")
        scheduled_delivery_timestamp = int(time.time() - 86400)

        result = self.do_schedule_message(
            "channel", verona_stream_id, f"{content} 1", scheduled_delivery_timestamp
        )
        self.assert_json_error(result, "Scheduled delivery time must be in the future.")

    def test_edit_schedule_message(self) -> None:
        content = "Original test message"
        scheduled_delivery_timestamp = int(time.time() + 86400)
        verona_stream_id = self.get_stream_id("Verona")

        # Scheduling a message to a stream you are subscribed is successful.
        result = self.do_schedule_message(
            "channel", verona_stream_id, content, scheduled_delivery_timestamp
        )
        scheduled_message = self.last_scheduled_message()
        self.assert_json_success(result)
        self.assertEqual(scheduled_message.recipient.type, Recipient.STREAM)
        self.assertEqual(scheduled_message.content, "Original test message")
        self.assertEqual(scheduled_message.topic_name(), "Test topic")
        self.assertEqual(
            scheduled_message.scheduled_timestamp,
            timestamp_to_datetime(scheduled_delivery_timestamp),
        )
        scheduled_message_id = scheduled_message.id
        payload: dict[str, Any]

        # Edit message with other stream message type ("stream") and no other changes
        # results in no changes to the scheduled message.
        payload = {
            "type": "stream",
            "to": orjson.dumps(verona_stream_id).decode(),
            "topic": "Test topic",
        }
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
        self.assert_json_success(result)

        scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
        self.assertEqual(scheduled_message.recipient.type, Recipient.STREAM)
        self.assertEqual(scheduled_message.stream_id, verona_stream_id)
        self.assertEqual(scheduled_message.content, "Original test message")
        self.assertEqual(scheduled_message.topic_name(), "Test topic")
        self.assertEqual(
            scheduled_message.scheduled_timestamp,
            timestamp_to_datetime(scheduled_delivery_timestamp),
        )

        # Sending request with only scheduled message ID returns an error
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}")
        self.assert_json_error(result, "Nothing to change")

        # Trying to edit only message `type` returns an error
        payload = {
            "type": "direct",
        }
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
        self.assert_json_error(
            result, "Recipient required when updating type of scheduled message."
        )

        # Edit message `type` with valid `to` parameter succeeds
        othello = self.example_user("othello")
        to = [othello.id]
        payload = {"type": "direct", "to": orjson.dumps(to).decode()}
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
        self.assert_json_success(result)

        scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
        self.assertNotEqual(scheduled_message.recipient.type, Recipient.STREAM)
        self.assertEqual(scheduled_message.recipient.type, Recipient.PERSONAL)
        scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
        self.assertEqual(scheduled_message.topic_name(), Message.DM_TOPIC)

        # Trying to edit `topic` for direct message is ignored
        payload = {
            "topic": "Direct message topic",
        }
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
        self.assert_json_success(result)

        scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
        self.assertEqual(scheduled_message.topic_name(), Message.DM_TOPIC)

        # Trying to edit `type` to stream message type without a `topic` returns an error
        payload = {
            "type": "channel",
            "to": orjson.dumps(verona_stream_id).decode(),
        }
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
        self.assert_json_error(
            result, "Topic required when updating scheduled message type to channel."
        )

        # Edit message `type` to stream with valid `to` and `topic` succeeds
        payload = {
            "type": "channel",
            "to": orjson.dumps(verona_stream_id).decode(),
            "topic": "New test topic",
        }
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
        self.assert_json_success(result)

        scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
        self.assertEqual(scheduled_message.recipient.type, Recipient.STREAM)
        self.assertEqual(scheduled_message.topic_name(), "New test topic")

        # Trying to edit with timestamp in the past returns an error
        new_scheduled_delivery_timestamp = int(time.time() - 86400)
        payload = {
            "scheduled_delivery_timestamp": new_scheduled_delivery_timestamp,
        }
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
        self.assert_json_error(result, "Scheduled delivery time must be in the future.")

        # Edit content and time of scheduled message succeeds
        edited_content = "Edited test message"
        new_scheduled_delivery_timestamp = scheduled_delivery_timestamp + int(
            time.time() + (3 * 86400)
        )
        payload = {
            "content": edited_content,
            "scheduled_delivery_timestamp": new_scheduled_delivery_timestamp,
        }
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
        self.assert_json_success(result)

        scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
        self.assertEqual(scheduled_message.content, edited_content)
        self.assertEqual(scheduled_message.topic_name(), "New test topic")
        self.assertEqual(
            scheduled_message.scheduled_timestamp,
            timestamp_to_datetime(new_scheduled_delivery_timestamp),
        )

        # Edit topic and content of scheduled stream message
        edited_content = "Final content edit for test"
        payload = {
            "topic": "Another topic for test",
            "content": edited_content,
        }
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message.id}", payload)
        self.assert_json_success(result)

        scheduled_message = self.get_scheduled_message(str(scheduled_message.id))
        self.assertEqual(scheduled_message.content, edited_content)
        self.assertEqual(scheduled_message.topic_name(), "Another topic for test")

        # Edit only topic of scheduled stream message
        payload = {
            "topic": "Final topic for test",
        }
        result = self.client_patch(f"/json/scheduled_messages/{scheduled_message.id}", payload)
        self.assert_json_success(result)

        scheduled_message = self.get_scheduled_message(str(scheduled_message.id))
        self.assertEqual(scheduled_message.recipient.type, Recipient.STREAM)
        self.assertEqual(scheduled_message.content, edited_content)
        self.assertEqual(scheduled_message.topic_name(), "Final topic for test")
        self.assertEqual(
            scheduled_message.scheduled_timestamp,
            timestamp_to_datetime(new_scheduled_delivery_timestamp),
        )

    def test_fetch_scheduled_messages(self) -> None:
        self.login("hamlet")
        # No scheduled message
        result = self.client_get("/json/scheduled_messages")
        self.assert_json_success(result)
        self.assert_length(orjson.loads(result.content)["scheduled_messages"], 0)

        verona_stream_id = self.get_stream_id("Verona")
        content = "Test message"
        scheduled_delivery_timestamp = int(time.time() + 86400)
        self.do_schedule_message("channel", verona_stream_id, content, scheduled_delivery_timestamp)

        # Single scheduled message
        result = self.client_get("/json/scheduled_messages")
        self.assert_json_success(result)
        scheduled_messages = orjson.loads(result.content)["scheduled_messages"]

        self.assert_length(scheduled_messages, 1)
        self.assertEqual(
            scheduled_messages[0]["scheduled_message_id"], self.last_scheduled_message().id
        )
        self.assertEqual(scheduled_messages[0]["content"], content)
        self.assertEqual(scheduled_messages[0]["to"], verona_stream_id)
        self.assertEqual(scheduled_messages[0]["type"], "stream")
        self.assertEqual(scheduled_messages[0]["topic"], "Test topic")
        self.assertEqual(
            scheduled_messages[0]["scheduled_delivery_timestamp"], scheduled_delivery_timestamp
        )

        othello = self.example_user("othello")
        result = self.do_schedule_message(
            "direct", [othello.id], f"{content} 3", scheduled_delivery_timestamp
        )

        # Multiple scheduled messages
        result = self.client_get("/json/scheduled_messages")
        self.assert_json_success(result)
        self.assert_length(orjson.loads(result.content)["scheduled_messages"], 2)

        # Check if another user can access these scheduled messages.
        self.logout()
        self.login("othello")
        result = self.client_get("/json/scheduled_messages")
        self.assert_json_success(result)
        self.assert_length(orjson.loads(result.content)["scheduled_messages"], 0)

    def test_delete_scheduled_messages(self) -> None:
        self.login("hamlet")

        content = "Test message"
        verona_stream_id = self.get_stream_id("Verona")
        scheduled_delivery_timestamp = int(time.time() + 86400)

        self.do_schedule_message("channel", verona_stream_id, content, scheduled_delivery_timestamp)
        scheduled_message = self.last_scheduled_message()
        self.logout()

        # Other user cannot delete it.
        othello = self.example_user("othello")
        result = self.api_delete(othello, f"/api/v1/scheduled_messages/{scheduled_message.id}")
        self.assert_json_error(result, "Scheduled message does not exist", 404)

        self.login("hamlet")
        result = self.client_delete(f"/json/scheduled_messages/{scheduled_message.id}")
        self.assert_json_success(result)

        # Already deleted.
        result = self.client_delete(f"/json/scheduled_messages/{scheduled_message.id}")
        self.assert_json_error(result, "Scheduled message does not exist", 404)

    def test_attachment_handling(self) -> None:
        self.login("hamlet")
        hamlet = self.example_user("hamlet")
        verona_stream_id = self.get_stream_id("Verona")

        attachment_file1 = StringIO("zulip!")
        attachment_file1.name = "dummy_1.txt"
        result = self.client_post("/json/user_uploads", {"file": attachment_file1})
