import os
import re
from html import escape
from textwrap import dedent
from typing import Any
from unittest import mock

import orjson
import requests
import responses
from bs4 import BeautifulSoup
from django.conf import settings
from django.test import override_settings
from markdown import Markdown
from responses import matchers
from typing_extensions import override

from zerver.actions.alert_words import do_add_alert_words
from zerver.actions.create_realm import do_create_realm
from zerver.actions.realm_emoji import do_remove_realm_emoji
from zerver.actions.realm_settings import do_set_realm_property
from zerver.actions.streams import do_change_stream_group_based_setting
from zerver.actions.user_groups import (
    add_subgroups_to_user_group,
    check_add_user_group,
    do_deactivate_user_group,
)
from zerver.actions.user_settings import do_change_user_setting
from zerver.actions.users import change_user_is_active
from zerver.lib.alert_words import get_alert_word_automaton
from zerver.lib.camo import get_camo_url
from zerver.lib.create_user import create_user
from zerver.lib.emoji import codepoint_to_name, get_emoji_url
from zerver.lib.emoji_utils import hex_codepoint_to_emoji
from zerver.lib.exceptions import JsonableError, MarkdownRenderingError
from zerver.lib.markdown import (
    POSSIBLE_EMOJI_RE,
    InlineInterestingLinkProcessor,
    MarkdownListPreprocessor,
    MessageRenderingResult,
    clear_web_link_regex_for_testing,
    content_has_emoji_syntax,
    image_preview_enabled,
    markdown_convert,
    possible_linked_stream_names,
    render_message_markdown,
    topic_links,
    url_embed_preview_enabled,
    url_to_a,
)
from zerver.lib.markdown.fenced_code import FencedBlockPreprocessor
from zerver.lib.mdiff import diff_strings
from zerver.lib.mention import (
    FullNameInfo,
    MentionBackend,
    MentionData,
    PossibleMentions,
    get_possible_mentions_info,
    possible_mentions,
    possible_user_group_mentions,
    stream_wildcards,
    topic_wildcards,
)
from zerver.lib.per_request_cache import flush_per_request_caches
from zerver.lib.streams import user_has_content_access, user_has_metadata_access
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.tex import render_tex
from zerver.lib.types import UserGroupMembersData
from zerver.lib.upload import upload_message_attachment
from zerver.lib.user_groups import UserGroupMembershipDetails
from zerver.models import Message, NamedUserGroup, RealmEmoji, RealmFilter, UserMessage, UserProfile
from zerver.models.clients import get_client
from zerver.models.groups import SystemGroups
from zerver.models.linkifiers import linkifiers_for_realm
from zerver.models.realms import get_realm
from zerver.models.streams import get_stream
from zerver.models.users import get_system_bot


class SimulatedFencedBlockPreprocessor(FencedBlockPreprocessor):
    # Simulate code formatting.

    @override
    def format_code(self, lang: str | None, code: str) -> str:
        return (lang or "") + ":" + code

    @override
    def placeholder(self, s: str) -> str:
        return "**" + s.strip("\n") + "**"


class FencedBlockPreprocessorTest(ZulipTestCase):
    def test_simple_quoting(self) -> None:
        processor = FencedBlockPreprocessor(Markdown())
        markdown_input = [
            "~~~ quote",
            "hi",
            "bye",
            "",
            "",
        ]
        expected = [
            "",
            "> hi",
            "> bye",
            "> ",
            "> ",
            "",
            "",
        ]
        lines = processor.run(markdown_input)
        self.assertEqual(lines, expected)

    def test_serial_quoting(self) -> None:
        processor = FencedBlockPreprocessor(Markdown())
        markdown_input = [
            "~~~ quote",
            "hi",
            "~~~",
            "",
            "~~~ quote",
            "bye",
            "",
            "",
        ]
        expected = [
            "",
            "> hi",
            "",
            "",
            "",
            "> bye",
            "> ",
            "> ",
            "",
            "",
        ]
        lines = processor.run(markdown_input)
        self.assertEqual(lines, expected)

    def test_serial_code(self) -> None:
        processor = SimulatedFencedBlockPreprocessor(Markdown())

        markdown_input = [
            "``` .py",
            "hello()",
            "```",
            "",
            "```vb.net",
            "goodbye()",
            "```",
            "",
            "```c#",
            "weirdchar()",
            "```",
            "",
            "```",
            "no-highlight()",
            "```",
            "",
        ]
        expected = [
            "",
            "**py:hello()**",
            "",
            "",
            "",
            "**vb.net:goodbye()**",
            "",
            "",
            "",
            "**c#:weirdchar()**",
            "",
            "",
            "",
            "**:no-highlight()**",
            "",
            "",
        ]
        lines = processor.run(markdown_input)
        self.assertEqual(lines, expected)

    def test_nested_code(self) -> None:
        processor = SimulatedFencedBlockPreprocessor(Markdown())

        markdown_input = [
            "~~~ quote",
            "hi",
            "``` .py",
            "hello()",
            "```",
            "",
            "",
        ]
        expected = [
            "",
            "> hi",
            "> ",
            "> **py:hello()**",
            "> ",
            "> ",
            "> ",
            "",
            "",
        ]
        lines = processor.run(markdown_input)
        self.assertEqual(lines, expected)


def markdown_convert_wrapper(content: str) -> str:
    return markdown_convert(
        content=content,
        message_realm=get_realm("zulip"),
    ).rendered_content


class MarkdownMiscTest(ZulipTestCase):
    def test_diffs_work_as_expected(self) -> None:
        str1 = "<p>The quick brown fox jumps over the lazy dog.  Animal stories are fun, yeah</p>"
        str2 = "<p>The fast fox jumps over the lazy dogs and cats.  Animal stories are fun</p>"
        expected_diff = "\u001b[34m-\u001b[0m <p>The \u001b[33mquick brown\u001b[0m fox jumps over the lazy dog.  Animal stories are fun\u001b[31m, yeah\u001b[0m</p>\n\u001b[34m+\u001b[0m <p>The \u001b[33mfast\u001b[0m fox jumps over the lazy dog\u001b[32ms and cats\u001b[0m.  Animal stories are fun</p>\n"
        self.assertEqual(diff_strings(str1, str2), expected_diff)

    def test_get_possible_mentions_info(self) -> None:
        realm = get_realm("zulip")

        def make_user(email: str, full_name: str) -> UserProfile:
            return create_user(
                email=email,
                password="whatever",
                realm=realm,
                full_name=full_name,
            )

        fred1 = make_user("fred1@example.com", "Fred Flintstone")
        change_user_is_active(fred1, False)

        fred2 = make_user("fred2@example.com", "Fred Flintstone")

        fred3 = make_user("fred3@example.com", "Fred Flintstone")
        change_user_is_active(fred3, False)

        fred4 = make_user("fred4@example.com", "Fred Flintstone")

        mention_backend = MentionBackend(realm.id)
        lst = get_possible_mentions_info(
            mention_backend,
            {"Fred Flintstone", "Cordelia, LEAR's daughter", "Not A User"},
            message_sender=None,
        )
        set_of_names = {x.full_name.lower() for x in lst}
        self.assertEqual(set_of_names, {"fred flintstone", "cordelia, lear's daughter"})

        by_id = {row.id: row for row in lst}
        self.assertEqual(
            by_id.get(fred2.id),
            FullNameInfo(
                full_name="Fred Flintstone",
                id=fred2.id,
                is_active=True,
            ),
        )
        self.assertEqual(
            by_id.get(fred4.id),
            FullNameInfo(
                full_name="Fred Flintstone",
                id=fred4.id,
                is_active=True,
            ),
        )

    def test_mention_data(self) -> None:
        realm = get_realm("zulip")
        hamlet = self.example_user("hamlet")
        cordelia = self.example_user("cordelia")
        content = "@**King Hamlet** @**Cordelia, lear's daughter**"
        mention_backend = MentionBackend(realm.id)
        mention_data = MentionData(mention_backend, content, message_sender=None)
        self.assertEqual(mention_data.get_user_ids(), {hamlet.id, cordelia.id})
        self.assertEqual(
            mention_data.get_user_by_id(hamlet.id),
            FullNameInfo(
                full_name=hamlet.full_name,
                id=hamlet.id,
                is_active=True,
            ),
        )

        user = mention_data.get_user_by_name("king hamLET")
        assert user is not None
        self.assertEqual(user.full_name, hamlet.full_name)

        self.assertFalse(mention_data.message_has_stream_wildcards())
        content = "@**King Hamlet** @**Cordelia, lear's daughter** @**all**"
        mention_data = MentionData(mention_backend, content, message_sender=None)
        self.assertTrue(mention_data.message_has_stream_wildcards())

        self.assertFalse(mention_data.message_has_topic_wildcards())
        content = "@**King Hamlet** @**Cordelia, lear's daughter** @**topic**"
        mention_data = MentionData(mention_backend, content, message_sender=None)
        self.assertTrue(mention_data.message_has_topic_wildcards())

        content = "@*hamletcharacters*"
        group = NamedUserGroup.objects.get(realm_for_sharding=realm, name="hamletcharacters")
        mention_data = MentionData(mention_backend, content, message_sender=None)
        self.assertEqual(mention_data.get_group_members(group.id), {hamlet.id, cordelia.id})

        change_user_is_active(cordelia, False)
        mention_data = MentionData(mention_backend, content, message_sender=None)
        self.assertEqual(mention_data.get_group_members(group.id), {hamlet.id})

    def test_bulk_user_group_mentions(self) -> None:
        realm = get_realm("zulip")
        hamlet = self.example_user("hamlet")
        cordelia = self.example_user("cordelia")
        othello = self.example_user("othello")
        mention_backend = MentionBackend(realm.id)

        content = ""
        for i in range(5):
            group_name = f"group{i}"
            check_add_user_group(realm, group_name, [hamlet, cordelia], acting_user=othello)
            content += f" @*{group_name}*"

        CONSTANT_QUERY_COUNT = 2  # even if it increases in future, make sure it's constant.
        with self.assert_database_query_count(CONSTANT_QUERY_COUNT):
            MentionData(mention_backend, content, message_sender=None)

    def test_mention_user_groups_with_common_subgroup(self) -> None:
        # Mention multiple groups (class-A and class-B) with a common sub-group (good-students)
        # and make sure each mentioned group has the expected members
        # (i.e. direct and via sub-groups) in mention_data.

        realm = get_realm("zulip")
        aaron = self.example_user("aaron")
        hamlet = self.example_user("hamlet")
        cordelia = self.example_user("cordelia")
        iago = self.example_user("iago")
        othello = self.example_user("othello")

        good_students = check_add_user_group(
            realm, "good-students", [aaron, hamlet], acting_user=othello
        )
        class_A = check_add_user_group(realm, "class-A", [iago], acting_user=othello)
        class_B = check_add_user_group(realm, "class-B", [cordelia], acting_user=othello)

        add_subgroups_to_user_group(class_A, [good_students], acting_user=othello)
        add_subgroups_to_user_group(class_B, [good_students], acting_user=othello)

        content = "@*class-A*  @*class-B*"
        mention_backend = MentionBackend(realm.id)
        mention_data = MentionData(mention_backend, content, message_sender=None)

        # both groups should have their direct members and the sub-group's members.
        self.assertEqual(mention_data.get_group_members(class_A.id), {iago.id, aaron.id, hamlet.id})
        self.assertEqual(
            mention_data.get_group_members(class_B.id), {cordelia.id, aaron.id, hamlet.id}
        )

    def test_silent_mention_user_groups(self) -> None:
        # silent VS non-silent group mentions, in regard to fetching group membership.

        realm = get_realm("zulip")
        aaron = self.example_user("aaron")
        hamlet = self.example_user("hamlet")
        cordelia = self.example_user("cordelia")
        iago = self.example_user("iago")
        othello = self.example_user("othello")

        hamlet_group = NamedUserGroup.objects.get(realm_for_sharding=realm, name="hamletcharacters")
        zulip_group = check_add_user_group(realm, "zulip_group", [iago, aaron], acting_user=othello)
        mention_backend = MentionBackend(realm.id)

        # mention zulip_group, but silent mention hamlet_group.
        content = "@*zulip_group*, @_*hamletcharacters*"
        mention_data = MentionData(mention_backend, content, message_sender=None)

        # non-silent mention should fetch group membership.
        self.assertEqual(mention_data.get_group_members(zulip_group.id), {iago.id, aaron.id})

        # silent mention should NOT fetch group membership.
        self.assertEqual(mention_data.get_group_members(hamlet_group.id), set())

        # We should make sure non-silent mention always results in fetching group membership,
        # whether it precedes or follows a silent mention for the SAME group

        # non-silent before silent.
        content = "@*hamletcharacters*, @_*hamletcharacters*"
        mention_data = MentionData(mention_backend, content, message_sender=None)
        self.assertEqual(mention_data.get_group_members(hamlet_group.id), {hamlet.id, cordelia.id})

        # non-silent after silent.
        content = "@_*hamletcharacters*, @*hamletcharacters*"
        mention_data = MentionData(mention_backend, content, message_sender=None)
        self.assertEqual(mention_data.get_group_members(hamlet_group.id), {hamlet.id, cordelia.id})

    def test_invalid_katex_path(self) -> None:
        with self.settings(DEPLOY_ROOT="/nonexistent"):
            with self.assertLogs(level="ERROR") as m:
                render_tex("random text")
            self.assertEqual(m.output, ["ERROR:root:Cannot find KaTeX for latex rendering!"])

    @responses.activate
    @override_settings(KATEX_SERVER=True, SHARED_SECRET="foo")
    def test_katex_server(self) -> None:
        responses.post(
            "http://localhost:9700/",
            match=[
                matchers.urlencoded_params_matcher(
                    {"content": "foo", "is_display": "false", "shared_secret": "foo"}
                )
            ],
            content_type="text/html; charset=utf-8",
            body="<i>html</i>",
        )
        self.assertEqual(render_tex("foo"), "<i>html</i>")

        responses.post(
            "http://localhost:9700/?",
            match=[
                matchers.urlencoded_params_matcher(
                    {"content": "foo", "is_display": "true", "shared_secret": "foo"}
                )
            ],
            content_type="text/html; charset=utf-8",
            body="<i>other</i>",
        )
        self.assertEqual(render_tex("foo", is_inline=False), "<i>other</i>")

        responses.post(
            "http://localhost:9700/",
            content_type="text/html; charset=utf-8",
            status=400,
            body=r"KaTeX parse error: &#39;\&#39;",
        )
        self.assertEqual(render_tex("bad"), None)

        responses.post(
            "http://localhost:9700/",
            content_type="text/html; charset=utf-8",
            status=400,
            body=r"KaTeX parse error: &#39;\&#39;",
        )
        self.assertEqual(render_tex("bad"), None)

        responses.post("http://localhost:9700/", status=403, body="")
        with self.assertLogs(level="WARNING") as m:
            self.assertEqual(render_tex("bad"), None)
        self.assertEqual(m.output, ["WARNING:root:KaTeX rendering service failed: (403) "])

        responses.post("http://localhost:9700/", status=500, body="")
        with self.assertLogs(level="WARNING") as m:
            self.assertEqual(render_tex("bad"), None)
        self.assertEqual(m.output, ["WARNING:root:KaTeX rendering service failed: (500) "])

        responses.post("http://localhost:9700/", body=requests.exceptions.Timeout())
        with self.assertLogs(level="WARNING") as m:
            self.assertEqual(render_tex("bad"), None)
        self.assertEqual(
            m.output, ["WARNING:root:KaTeX rendering service timed out with 3 byte long input"]
        )

        responses.post("http://localhost:9700/", body=requests.exceptions.ConnectionError())
        with self.assertLogs(level="WARNING") as m:
            self.assertEqual(render_tex("bad"), None)
        self.assertEqual(m.output, ["WARNING:root:KaTeX rendering service failed: ConnectionError"])

        with override_settings(KATEX_SERVER_PORT=9701):
            responses.post(
                "http://localhost:9701/",
                body="<i>html</i>",
                content_type="text/html; charset=utf-8",
            )
            self.assertEqual(render_tex("foo"), "<i>html</i>")


class MarkdownListPreprocessorTest(ZulipTestCase):
    # We test that the preprocessor inserts blank lines at correct places.
    # We use <> to indicate that we need to insert a blank line here.
    def split_message(self, msg: str) -> tuple[list[str], list[str]]:
        original = msg.replace("<>", "").split("\n")
        expected = re.split(r"\n|<>", msg)
        return original, expected

    def test_basic_list(self) -> None:
        preprocessor = MarkdownListPreprocessor()
        original, expected = self.split_message("List without a gap\n<>* One\n* Two")
        self.assertEqual(preprocessor.run(original), expected)

    def test_list_after_quotes(self) -> None:
        preprocessor = MarkdownListPreprocessor()
        original, expected = self.split_message(
            "```quote\nSomething\n```\n\nList without a gap\n<>* One\n* Two"
        )
        self.assertEqual(preprocessor.run(original), expected)

    def test_list_in_code(self) -> None:
        preprocessor = MarkdownListPreprocessor()
        original, expected = self.split_message("```\nList without a gap\n* One\n* Two\n```")
        self.assertEqual(preprocessor.run(original), expected)

    def test_complex_nesting_with_different_fences(self) -> None:
        preprocessor = MarkdownListPreprocessor()
        msg = """```quote
In quote. We should convert a list here:<>
* one
* two

~~~
This is a nested code fence, do not make changes here:
* one
* two

````quote
Quote in code fence. Should not convert:
* one
* two
````

~~~

Back in the quote. We should convert:<>
* one
* two
```

Outside. Should convert:<>
* one
* two
        """
        original, expected = self.split_message(msg)
        self.assertEqual(preprocessor.run(original), expected)

    def test_complex_nesting_with_same_fence(self) -> None:
        preprocessor = MarkdownListPreprocessor()
        msg = """```quote
In quote. We should convert a list here:<>
* one
* two

```python
This is a nested code fence, do not make changes here:
* one
* two

```quote
Quote in code fence. Should not convert:
* one
* two
```

```

Back in the quote. We should convert:<>
* one
* two
```

Outside. Should convert:<>
* one
* two
        """
        original, expected = self.split_message(msg)
        self.assertEqual(preprocessor.run(original), expected)


class MarkdownFixtureTest(ZulipTestCase):
    def load_markdown_tests(self) -> tuple[dict[str, Any], list[list[str]]]:
        test_fixtures = {}
        with open(
            os.path.join(os.path.dirname(__file__), "fixtures/markdown_test_cases.json"), "rb"
        ) as f:
            data = orjson.loads(f.read())
        for test in data["regular_tests"]:
            test_fixtures[test["name"]] = test

        return test_fixtures, data["linkify_tests"]

    def test_markdown_no_ignores(self) -> None:
        # We do not want any ignored tests to be committed and merged.
        format_tests, _linkify_tests = self.load_markdown_tests()
        for name, test in format_tests.items():
            message = f'Test "{name}" shouldn\'t be ignored.'
            is_ignored = test.get("ignore", False)
            self.assertFalse(is_ignored, message)

    def test_markdown_fixtures_unique_names(self) -> None:
        # All markdown fixtures must have unique names.
        found_names: set[str] = set()
        with open(
            os.path.join(os.path.dirname(__file__), "fixtures/markdown_test_cases.json"), "rb"
        ) as f:
            data = orjson.loads(f.read())
        for test in data["regular_tests"]:
            test_name = test["name"]
            message = f'Test name: "{test_name}" must be unique.'
            is_unique = test_name not in found_names
            self.assertTrue(is_unique, message)
            found_names.add(test_name)

    def test_markdown_fixtures(self) -> None:
        format_tests, linkify_tests = self.load_markdown_tests()
        valid_keys = {
            "name",
            "input",
            "expected_output",
            "backend_only_rendering",
            "marked_expected_output",
            "text_content",
            "translate_emoticons",
            "ignore",
        }

        for name, test in format_tests.items():
            with self.subTest(markdown_test_case=name):
                # Check that there aren't any unexpected keys as those are often typos
                self.assert_length(set(test.keys()) - valid_keys, 0)
                # Ignore tests if specified
                if test.get("ignore", False):
                    continue  # nocoverage

                if test.get("translate_emoticons", False):
                    # Create a userprofile and send message with it.
                    user_profile = self.example_user("othello")
                    do_change_user_setting(
                        user_profile, "translate_emoticons", True, acting_user=None
                    )
                    msg = Message(
                        sender=user_profile,
                        sending_client=get_client("test"),
                        realm=user_profile.realm,
                    )
                    rendering_result = render_message_markdown(msg, test["input"])
                    converted = rendering_result.rendered_content
                else:
                    converted = markdown_convert_wrapper(test["input"])

                self.assertEqual(converted, test["expected_output"])

        def replaced(payload: str, url: str, phrase: str = "") -> str:
            if url[:4] == "http":
                href = url
            elif "@" in url:
                href = "mailto:" + url
            else:
                href = "http://" + url
            return payload % (f'<a href="{href}">{url}</a>',)

        for inline_url, reference, url in linkify_tests:
            try:
                match = replaced(reference, url, phrase=inline_url)
            except TypeError:
                match = reference
            converted = markdown_convert_wrapper(inline_url)
            self.assertEqual(match, converted)


class MarkdownLinkTest(ZulipTestCase):
    def test_url_to_a(self) -> None:
        url = "javascript://example.com/invalidURL"
        converted = url_to_a(db_data=None, url=url, text=url)
        self.assertEqual(
            converted,
            "javascript://example.com/invalidURL",
        )

    def test_normal_link(self) -> None:
        realm = get_realm("zulip")
        sender_user_profile = self.example_user("othello")
        message = Message(sender=sender_user_profile, sending_client=get_client("test"))
        msg = "http://example.com/#settings/"

        self.assertEqual(
            markdown_convert(msg, message_realm=realm, message=message).rendered_content,
            '<p><a href="http://example.com/#settings/">http://example.com/#settings/</a></p>',
        )

    def test_relative_link(self) -> None:
        realm = get_realm("zulip")
        sender_user_profile = self.example_user("othello")
        message = Message(sender=sender_user_profile, sending_client=get_client("test"))

        msg = "http://zulip.testserver/#narrow/channel/999-hello"
        self.assertEqual(
            markdown_convert(msg, message_realm=realm, message=message).rendered_content,
            '<p><a href="#narrow/channel/999-hello">http://zulip.testserver/#narrow/channel/999-hello</a></p>',
        )

        msg = f"http://zulip.testserver/user_uploads/{realm.id}/ff/file.txt"
        self.assertEqual(
            markdown_convert(msg, message_realm=realm, message=message).rendered_content,
            f'<p><a href="user_uploads/{realm.id}/ff/file.txt">http://zulip.testserver/user_uploads/{realm.id}/ff/file.txt</a></p>',
        )

        msg = "http://zulip.testserver/not:relative"
        self.assertEqual(
            markdown_convert(msg, message_realm=realm, message=message).rendered_content,
            '<p><a href="http://zulip.testserver/not:relative">http://zulip.testserver/not:relative</a></p>',
        )

    def test_relative_link_streams_page(self) -> None:
        realm = get_realm("zulip")
        sender_user_profile = self.example_user("othello")
        message = Message(sender=sender_user_profile, sending_client=get_client("test"))
        msg = "http://zulip.testserver/#channels/all"

        self.assertEqual(
            markdown_convert(msg, message_realm=realm, message=message).rendered_content,
            '<p><a href="#channels/all">http://zulip.testserver/#channels/all</a></p>',
        )

    def test_md_relative_link(self) -> None:
        realm = get_realm("zulip")
        sender_user_profile = self.example_user("othello")
        message = Message(sender=sender_user_profile, sending_client=get_client("test"))

        msg = "[hello](http://zulip.testserver/#narrow/channel/999-hello)"
        self.assertEqual(
            markdown_convert(msg, message_realm=realm, message=message).rendered_content,
            '<p><a href="#narrow/channel/999-hello">hello</a></p>',
        )

        msg = f"[hello](http://zulip.testserver/user_uploads/{realm.id}/ff/file.txt)"
        self.assertEqual(
            markdown_convert(msg, message_realm=realm, message=message).rendered_content,
            f'<p><a href="user_uploads/{realm.id}/ff/file.txt">hello</a></p>',
        )

        msg = "[hello](http://zulip.testserver/not:relative)"
        self.assertEqual(
            markdown_convert(msg, message_realm=realm, message=message).rendered_content,
            '<p><a href="http://zulip.testserver/not:relative">hello</a></p>',
        )

    def test_inline_file(self) -> None:
        msg = "Check out this file file:///Volumes/myserver/Users/Shared/pi.py"

        # Make separate realms because the markdown engines cache the
        # linkifiers on them, including if ENABLE_FILE_LINKS was used
        realm = do_create_realm(string_id="file_links_disabled", name="File links disabled")
        self.assertEqual(
            markdown_convert(msg, message_realm=realm).rendered_content,
            "<p>Check out this file file:///Volumes/myserver/Users/Shared/pi.py</p>",
        )
        clear_web_link_regex_for_testing()

        try:
            with self.settings(ENABLE_FILE_LINKS=True):
                realm = do_create_realm(string_id="file_links_enabled", name="File links enabled")
                self.assertEqual(
                    markdown_convert(msg, message_realm=realm).rendered_content,
                    '<p>Check out this file <a href="file:///Volumes/myserver/Users/Shared/pi.py">file:///Volumes/myserver/Users/Shared/pi.py</a></p>',
                )
        finally:
            clear_web_link_regex_for_testing()

    def test_inline_bitcoin(self) -> None:
        msg = "To bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa or not to bitcoin"
        converted = markdown_convert_wrapper(msg)
        self.assertEqual(
            converted,
            '<p>To <a href="bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa">bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa</a> or not to bitcoin</p>',
        )


class MarkdownEmbedsTest(ZulipTestCase):
    def assert_message_content_is(
        self, message_id: int, rendered_content: str, user_name: str = "othello"
    ) -> None:
        sender_user_profile = self.example_user(user_name)
        result = self.assert_json_success(
            self.api_get(sender_user_profile, f"/api/v1/messages/{message_id}")
        )
        self.assertEqual(result["message"]["content"], rendered_content)

    def send_message_content(self, content: str, user_name: str = "othello") -> int:
        sender_user_profile = self.example_user(user_name)
        return self.send_stream_message(
            sender=sender_user_profile,
            stream_name="Verona",
            content=content,
        )

    def test_inline_youtube(self) -> None:
        msg = "Check out the debate: http://www.youtube.com/watch?v=hx1mjT73xYE"
        converted = markdown_convert_wrapper(msg)

        self.assertEqual(
            converted,
            f"""<p>Check out the debate: <a href="http://www.youtube.com/watch?v=hx1mjT73xYE">http://www.youtube.com/watch?v=hx1mjT73xYE</a></p>\n<div class="youtube-video message_inline_image"><a data-id="hx1mjT73xYE" href="http://www.youtube.com/watch?v=hx1mjT73xYE"><img src="{get_camo_url("https://i.ytimg.com/vi/hx1mjT73xYE/mqdefault.jpg")}"></a></div>""",
        )

        msg = "http://www.youtube.com/watch?v=hx1mjT73xYE"
        converted = markdown_convert_wrapper(msg)

        self.assertEqual(
            converted,
            f"""<p><a href="http://www.youtube.com/watch?v=hx1mjT73xYE">http://www.youtube.com/watch?v=hx1mjT73xYE</a></p>\n<div class="youtube-video message_inline_image"><a data-id="hx1mjT73xYE" href="http://www.youtube.com/watch?v=hx1mjT73xYE"><img src="{get_camo_url("https://i.ytimg.com/vi/hx1mjT73xYE/mqdefault.jpg")}"></a></div>""",
        )

        msg = "https://youtu.be/hx1mjT73xYE"
        converted = markdown_convert_wrapper(msg)

        self.assertEqual(
            converted,
            f"""<p><a href="https://youtu.be/hx1mjT73xYE">https://youtu.be/hx1mjT73xYE</a></p>\n<div class="youtube-video message_inline_image"><a data-id="hx1mjT73xYE" href="https://youtu.be/hx1mjT73xYE"><img src="{get_camo_url("https://i.ytimg.com/vi/hx1mjT73xYE/mqdefault.jpg")}"></a></div>""",
        )

        msg = "https://www.youtube.com/playlist?list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo"
        not_converted = markdown_convert_wrapper(msg)

        self.assertEqual(
            not_converted,
            '<p><a href="https://www.youtube.com/playlist?list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo">https://www.youtube.com/playlist?list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo</a></p>',
        )

        msg = "https://www.youtube.com/watch?v=O5nskjZ_GoI&list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo"
        converted = markdown_convert_wrapper(msg)

        self.assertEqual(
            converted,
            f"""<p><a href="https://www.youtube.com/watch?v=O5nskjZ_GoI&amp;list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo">https://www.youtube.com/watch?v=O5nskjZ_GoI&amp;list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo</a></p>\n<div class="youtube-video message_inline_image"><a data-id="O5nskjZ_GoI" href="https://www.youtube.com/watch?v=O5nskjZ_GoI&amp;list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo"><img src="{get_camo_url("https://i.ytimg.com/vi/O5nskjZ_GoI/mqdefault.jpg")}"></a></div>""",
        )

        msg = "http://www.youtube.com/watch_videos?video_ids=nOJgD4fcZhI,i96UO8-GFvw"
        converted = markdown_convert_wrapper(msg)

        self.assertEqual(
            converted,
            f"""<p><a href="http://www.youtube.com/watch_videos?video_ids=nOJgD4fcZhI,i96UO8-GFvw">http://www.youtube.com/watch_videos?video_ids=nOJgD4fcZhI,i96UO8-GFvw</a></p>\n<div class="youtube-video message_inline_image"><a data-id="nOJgD4fcZhI" href="http://www.youtube.com/watch_videos?video_ids=nOJgD4fcZhI,i96UO8-GFvw"><img src="{get_camo_url("https://i.ytimg.com/vi/nOJgD4fcZhI/mqdefault.jpg")}"></a></div>""",
        )

    def test_inline_image(self) -> None:
        image_url = "https://www.google.com/images/srpr/logo4w.png"
        msg = f"The Google logo looks like this: ![Google logo]({image_url}) and..."
        converted = markdown_convert_wrapper(msg)
