import os
import re
import shutil
from collections.abc import Iterator
from io import BytesIO
from typing import TYPE_CHECKING, Any
from unittest import mock
from unittest.mock import ANY
from urllib.parse import parse_qs, urlsplit

import orjson
import responses
from django.conf import settings
from django.http import HttpResponse
from django.test import override_settings
from django.utils.timezone import now as timezone_now
from requests.models import PreparedRequest

from confirmation import settings as confirmation_settings
from confirmation.models import Confirmation, get_object_from_key
from zerver.actions.create_realm import do_create_realm, get_email_address_visibility_default
from zerver.actions.create_user import do_create_user
from zerver.actions.data_import import import_slack_data
from zerver.data_import.import_util import (
    UploadFileRequest,
    UploadRecordData,
    ZerverFieldsT,
    build_defaultstream,
    build_recipient,
    build_subscription,
    build_usermessages,
    build_zerver_realm,
    download_and_export_upload_file,
)
from zerver.data_import.sequencer import NEXT_ID
from zerver.data_import.slack import (
    SLACK_IMPORT_TOKEN_SCOPES,
    AddedChannelsT,
    AddedDMsT,
    AddedMPIMsT,
    DMMembersT,
    MessageConversionResult,
    SlackBotEmail,
    SlackBotNotFoundError,
    channel_message_to_zerver_message,
    channels_to_zerver_stream,
    check_slack_token_access,
    convert_slack_workspace_messages,
    do_convert_zipfile,
    fetch_shared_channel_users,
    get_admin,
    get_guest,
    get_message_sending_user,
    get_owner,
    get_slack_api_data,
    get_subscription,
    get_user_timezone,
    process_message_files,
    slack_emoji_name_to_codepoint,
    slack_workspace_to_realm,
    users_to_zerver_userprofile,
)
from zerver.lib.exceptions import SlackImportInvalidFileError
from zerver.lib.import_realm import do_import_realm
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import find_key_by_email, read_test_image_file
from zerver.lib.topic import EXPORT_TOPIC_NAME
from zerver.models import (
    Message,
    PreregistrationRealm,
    Realm,
    RealmAuditLog,
    Recipient,
    UserProfile,
)
from zerver.models.realm_audit_logs import AuditLogEventType
from zerver.models.realms import get_realm

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


def remove_folder(path: str) -> None:
    if os.path.exists(path):
        shutil.rmtree(path)


def request_callback(request: PreparedRequest) -> tuple[int, dict[str, str], bytes]:
    valid_endpoint = False
    endpoints = [
        "https://slack.com/api/users.list",
        "https://slack.com/api/users.info",
        "https://slack.com/api/team.info",
        "https://slack.com/api/bots.info",
        "https://slack.com/api/api.test",
    ]
    for endpoint in endpoints:
        if request.url and endpoint in request.url:
            valid_endpoint = True
            break
    if not valid_endpoint:
        return (404, {}, b"")

    if request.headers.get("Authorization") != "Bearer xoxb-valid-token":
        return (200, {}, orjson.dumps({"ok": False, "error": "invalid_auth"}))

    if request.url == "https://slack.com/api/users.list":
        return (200, {}, orjson.dumps({"ok": True, "members": "user_data"}))

    query_from_url = str(urlsplit(request.url).query)
    qs = parse_qs(query_from_url)
    if request.url and "https://slack.com/api/users.info" in request.url:
        user2team_dict = {
            "U061A3E0G": "T6LARQE2Z",
            "U061A8H1G": "T7KJRQE8Y",
            "U8X25EBAB": "T5YFFM2QY",
            "U11111111": "T7KJRQE8Y",
            "U22222222": "T7KJRQE8Y",
            "U33333333": "T7KJRQE8Y",
        }
        try:
            user_id = qs["user"][0]
            team_id = user2team_dict[user_id]
        except KeyError:
            return (200, {}, orjson.dumps({"ok": False, "error": "user_not_found"}))
        return (200, {}, orjson.dumps({"ok": True, "user": {"id": user_id, "team_id": team_id}}))

    if request.url and "https://slack.com/api/bots.info" in request.url:
        bot_info_dict = {
            "B06NWMNUQ3W": {
                "id": "B06NWMNUQ3W",
                "deleted": False,
                "name": "ClickUp",
                "updated": 1714669546,
                "app_id": "A3G4A68V9",
                "icons": {
                    "image_36": "https://avatars.slack-edge.com/2024-05-01/7057208497908_a4351f6deb91094eac4c_36.png",
                    "image_48": "https://avatars.slack-edge.com/2024-05-01/7057208497908_a4351f6deb91094eac4c_48.png",
                    "image_72": "https://avatars.slack-edge.com/2024-05-01/7057208497908_a4351f6deb91094eac4c_72.png",
                },
            }
        }
        try:
            bot_id = qs["bot"][0]
            bot_info = bot_info_dict[bot_id]
        except KeyError:
            return (200, {}, orjson.dumps({"ok": False, "error": "bot_not_found"}))
        return (200, {}, orjson.dumps({"ok": True, "bot": bot_info}))

    if request.url and "https://slack.com/api/api.test" in request.url:
        return (200, {}, orjson.dumps({"ok": True}))
    # Else, https://slack.com/api/team.info
    team_not_found: tuple[int, dict[str, str], bytes] = (
        200,
        {},
        orjson.dumps({"ok": False, "error": "team_not_found"}),
    )
    try:
        team_id = qs["team"][0]
    except KeyError:
        return team_not_found

    team_dict = {
        "T6LARQE2Z": "foreignteam1",
        "T7KJRQE8Y": "foreignteam2",
    }
    try:
        team_domain = team_dict[team_id]
    except KeyError:
        return team_not_found
    return (200, {}, orjson.dumps({"ok": True, "team": {"id": team_id, "domain": team_domain}}))


class SlackImporter(ZulipTestCase):
    def run_channel_message_to_zerver_message_with_fixtures(
        self, fixture_names: list[str], **kwargs: dict[str, Any]
    ) -> MessageConversionResult:
        """
        This is a wrapper for `channel_message_to_zerver_message`, it
        comes with default values for most of the method's parameters.

        The `fixture_names` parameter specifies which Slack exported
        messages fixture(s) in the `slack_fixtures/exported_messages_fixtures`
        directory will be used as input for conversion.
        """
        slack_message_fixture_directory = "slack_fixtures/exported_messages_fixtures"

        user_data = kwargs.get(
            "user_data",
            orjson.loads(self.fixture_data("users.json", type=slack_message_fixture_directory)),
        )

        slack_user_id_to_zulip_user_id = kwargs.get(
            "slack_user_id_to_zulip_user_id", {"U066MTL5U": 5, "U061A5N1G": 24, "U061A1R2R": 43}
        )

        slack_recipient_name_to_zulip_recipient_id = kwargs.get(
            "slack_recipient_name_to_zulip_recipient_id",
            {
                "random": 2,
                "general": 1,
            },
        )

        all_messages = []
        for filename in fixture_names:
            all_messages.extend(
                orjson.loads(
                    self.fixture_data(f"{filename}.json", type=slack_message_fixture_directory)
                )
            )

        added_channels: dict[str, tuple[str, int]] = kwargs.get(
            "added_channels", {"random": ("c5", 1), "general": ("c6", 2)}
        )

        convert_slack_threads = kwargs.get("convert_slack_threads", True)
        assert isinstance(convert_slack_threads, bool)

        with mock.patch("zerver.data_import.slack.build_usermessages", return_value=(2, 4)):
            return channel_message_to_zerver_message(
                realm_id=1,
                users=user_data,
                slack_user_id_to_zulip_user_id=slack_user_id_to_zulip_user_id,
                slack_recipient_name_to_zulip_recipient_id=slack_recipient_name_to_zulip_recipient_id,
                all_messages=all_messages,
                zerver_realmemoji=[],
                subscriber_map={},
                added_channels=added_channels,
                dm_members={},
                domain_name="domain",
                long_term_idle=set(),
                convert_slack_threads=convert_slack_threads,
                do_download_and_export_upload_file=lambda request: None,
            )

    @responses.activate
    def test_get_slack_api_data_with_pagination(self) -> None:
        token = "xoxb-valid-token"
        pagination_limit = 40

        api_users_list = [f"user{i}" for i in range(100)]
        count = 0

        def paginated_request_callback(
            request: PreparedRequest,
        ) -> tuple[int, dict[str, str], bytes]:
            """
            A callback that in a very simple way simulates Slack's /users.list API
            with support for Pagination and some rate-limiting behavior.
            """
            assert request.url is not None
            assert request.url.startswith("https://slack.com/api/users.list")
            # Otherwise mypy complains about PreparedRequest not having params attribute:
            assert hasattr(request, "params")

            nonlocal count
            count += 1

            self.assertEqual(request.params["limit"], str(pagination_limit))
            cursor = int(request.params.get("cursor", 0))
            next_cursor = cursor + pagination_limit

            if count % 3 == 0:
                # Simulate a rate limit hit on every third request.
                return (
                    429,
                    {"retry-after": "30"},
                    orjson.dumps({"ok": False, "error": "rate_limit_hit"}),
                )

            result_user_data = api_users_list[cursor:next_cursor]

            if next_cursor >= len(api_users_list):
                # The fetch is completed.
                response_metadata = {}
            else:
                response_metadata = {"next_cursor": str(next_cursor)}
            return (
                200,
                {},
                orjson.dumps(
                    {
                        "ok": True,
                        "members": result_user_data,
                        "response_metadata": response_metadata,
                    }
                ),
            )

        responses.add_callback(
            responses.GET, "https://slack.com/api/users.list", callback=paginated_request_callback
        )
        with (
            mock.patch("zerver.data_import.slack.time.sleep", return_value=None) as mock_sleep,
            self.assertLogs(level="INFO") as mock_log,
        ):
            result = get_slack_api_data(
                "https://slack.com/api/users.list",
                "members",
                token=token,
                pagination_limit=pagination_limit,
            )
        self.assertEqual(result, api_users_list)
        self.assertEqual(mock_sleep.call_count, 1)
        self.assertIn("INFO:root:Rate limit exceeded. Retrying in 30 seconds...", mock_log.output)

    @responses.activate
    def test_get_slack_api_data(self) -> None:
        token = "xoxb-valid-token"

        # Users list
        slack_user_list_url = "https://slack.com/api/users.list"
        responses.add_callback(responses.GET, slack_user_list_url, callback=request_callback)
        self.assertEqual(
            get_slack_api_data(slack_user_list_url, "members", token=token), "user_data"
        )

        # Users info
        slack_users_info_url = "https://slack.com/api/users.info"
        user_id = "U8X25EBAB"
        responses.add_callback(responses.GET, slack_users_info_url, callback=request_callback)
        self.assertEqual(
            get_slack_api_data(slack_users_info_url, "user", token=token, user=user_id),
            {"id": user_id, "team_id": "T5YFFM2QY"},
        )
        # Should error if the required user argument is not specified
        with self.assertRaises(Exception) as invalid:
            get_slack_api_data(slack_users_info_url, "user", token=token)
        self.assertEqual(invalid.exception.args, ("Error accessing Slack API: user_not_found",))
        # Should error if the required user is not found
        with self.assertRaises(Exception) as invalid:
            get_slack_api_data(slack_users_info_url, "user", token=token, user="idontexist")
        self.assertEqual(invalid.exception.args, ("Error accessing Slack API: user_not_found",))

        # Bot info
        slack_bots_info_url = "https://slack.com/api/bots.info"
        responses.add_callback(responses.GET, slack_bots_info_url, callback=request_callback)
        with self.assertRaises(SlackBotNotFoundError):
            get_slack_api_data(slack_bots_info_url, "XXXYYYZZZ", token=token)

        # Api test
        api_test_info_url = "https://slack.com/api/api.test"
        responses.add_callback(responses.GET, api_test_info_url, callback=request_callback)
        self.assertTrue(get_slack_api_data(api_test_info_url, "ok", token=token))

        # Team info
        slack_team_info_url = "https://slack.com/api/team.info"
        responses.add_callback(responses.GET, slack_team_info_url, callback=request_callback)
        with self.assertRaises(Exception) as invalid:
            get_slack_api_data(slack_team_info_url, "team", token=token, team="wedontexist")
        self.assertEqual(invalid.exception.args, ("Error accessing Slack API: team_not_found",))
        # Should error if the required user argument is not specified
        with self.assertRaises(Exception) as invalid:
            get_slack_api_data(slack_team_info_url, "team", token=token)
        self.assertEqual(invalid.exception.args, ("Error accessing Slack API: team_not_found",))

        token = "xoxb-invalid-token"
        with self.assertRaises(Exception) as invalid:
            get_slack_api_data(slack_user_list_url, "members", token=token)
        self.assertEqual(invalid.exception.args, ("Error accessing Slack API: invalid_auth",))

        with self.assertRaises(Exception) as invalid:
            get_slack_api_data(slack_user_list_url, "members")
        self.assertEqual(invalid.exception.args, ("Slack token missing in kwargs",))

        token = "xoxb-status404"
        wrong_url = "https://slack.com/api/wrong"
        responses.add_callback(responses.GET, wrong_url, callback=request_callback)
        with self.assertRaises(Exception) as invalid, self.assertLogs(level="INFO") as mock_log:
            get_slack_api_data(wrong_url, "members", token=token)
        self.assertEqual(invalid.exception.args, ("HTTP error accessing the Slack API.",))
        self.assertEqual(mock_log.output, ["INFO:root:HTTP error: 404, Response: "])

    def test_build_zerver_realm(self) -> None:
        realm_id = 2
        realm_subdomain = "test-realm"
        time = float(timezone_now().timestamp())
        test_realm: list[dict[str, Any]] = build_zerver_realm(
            realm_id, realm_subdomain, time, "Slack"
        )
        test_zerver_realm_dict = test_realm[0]

        self.assertEqual(test_zerver_realm_dict["id"], realm_id)
        self.assertEqual(test_zerver_realm_dict["string_id"], realm_subdomain)
        self.assertEqual(test_zerver_realm_dict["name"], realm_subdomain)
        self.assertEqual(test_zerver_realm_dict["date_created"], time)
        self.assertNotIn("uuid", test_zerver_realm_dict)
        self.assertNotIn("uuid_owner_secret", test_zerver_realm_dict)

    @responses.activate
    def test_check_slack_token_access(self) -> None:
        def token_request_callback(request: PreparedRequest) -> tuple[int, dict[str, str], bytes]:
            auth = request.headers.get("Authorization")
            if auth == "Bearer xoxb-broken-request":
                return (400, {}, orjson.dumps({"ok": False, "error": "invalid_auth"}))

            if auth == "Bearer xoxb-invalid-token":
                return (200, {}, orjson.dumps({"ok": False, "error": "invalid_auth"}))

            if auth == "Bearer xoxb-very-limited-scopes":
                return (
                    200,
                    {"x-oauth-scopes": "emoji:read,bogus:scope"},
                    orjson.dumps(
                        {
                            "ok": False,
                            "error": "missing_scope",
                            "needed": "team:read",
                            "provided": "emoji:read,bogus:scope",
                        }
                    ),
                )
            if auth == "Bearer xoxb-limited-scopes":
                return (
                    200,
                    {"x-oauth-scopes": "team:read,bogus:scope"},
                    orjson.dumps({"ok": True}),
                )
            if auth == "Bearer xoxb-valid-token":
                return (
                    200,
                    {"x-oauth-scopes": "emoji:read,users:read,users:read.email,team:read"},
                    orjson.dumps({"ok": True}),
                )
            else:  # nocoverage
                raise Exception("Unknown token mock")

        responses.add_callback(
            responses.GET, "https://slack.com/api/api.test", callback=token_request_callback
        )

        def exception_for(token: str, required_scopes: set[str] = SLACK_IMPORT_TOKEN_SCOPES) -> str:
            with self.assertRaises(Exception) as invalid:
                check_slack_token_access(token, required_scopes)
            return invalid.exception.args[0]

        self.assertEqual(
            exception_for("xoxq-unknown"),
            "Invalid token. Valid tokens start with xoxb-.",
        )

        with self.assertLogs(level="ERROR"):
            self.assertEqual(
                exception_for("xoxb-invalid-token"),
                "Invalid token: xoxb-invalid-token",
            )

        self.assertEqual(
            exception_for("xoxb-broken-request"),
            "Failed to fetch data (HTTP status 400) for Slack token: xoxb-broken-request",
        )

        self.assertEqual(
            exception_for("xoxb-limited-scopes"),
            "Slack token is missing the following required scopes: ['emoji:read', 'users:read', 'users:read.email']",
        )
        self.assertEqual(
            exception_for("xoxb-very-limited-scopes"),
            "Slack token is missing the following required scopes: ['team:read', 'users:read', 'users:read.email']",
        )

        self.assertEqual(
            exception_for("xoxb-valid-token", set()),
            "required_scopes shouldn't be empty!",
        )

        check_slack_token_access("xoxb-valid-token", required_scopes=SLACK_IMPORT_TOKEN_SCOPES)

    def test_get_owner(self) -> None:
        user_data = [
            {"is_owner": False, "is_primary_owner": False},
            {"is_owner": True, "is_primary_owner": False},
            {"is_owner": False, "is_primary_owner": True},
            {"is_owner": True, "is_primary_owner": True},
        ]
        self.assertEqual(get_owner(user_data[0]), False)
        self.assertEqual(get_owner(user_data[1]), True)
        self.assertEqual(get_owner(user_data[2]), True)
        self.assertEqual(get_owner(user_data[3]), True)

    def test_get_admin(self) -> None:
        user_data = [{"is_admin": True}, {"is_admin": False}]
        self.assertEqual(get_admin(user_data[0]), True)
        self.assertEqual(get_admin(user_data[1]), False)

    def test_get_guest(self) -> None:
        user_data = [
            {"is_restricted": False, "is_ultra_restricted": False},
            {"is_restricted": True, "is_ultra_restricted": False},
            {"is_restricted": False, "is_ultra_restricted": True},
            {"is_restricted": True, "is_ultra_restricted": True},
        ]
        self.assertEqual(get_guest(user_data[0]), False)
        self.assertEqual(get_guest(user_data[1]), True)
        self.assertEqual(get_guest(user_data[2]), True)
        self.assertEqual(get_guest(user_data[3]), True)

    def test_get_timezone(self) -> None:
        user_chicago_timezone = {"tz": "America/Chicago"}
        user_timezone_none = {"tz": None}
        user_no_timezone: dict[str, Any] = {}

        self.assertEqual(get_user_timezone(user_chicago_timezone), "America/Chicago")
        self.assertEqual(get_user_timezone(user_timezone_none), "America/New_York")
        self.assertEqual(get_user_timezone(user_no_timezone), "America/New_York")

    @mock.patch("zerver.data_import.slack.get_data_file")
    @mock.patch("zerver.data_import.slack.get_messages_iterator")
    @responses.activate
    def test_fetch_shared_channel_users(
        self, messages_mock: mock.Mock, data_file_mock: mock.Mock
    ) -> None:
        users = [{"id": "U061A1R2R"}, {"id": "U061A5N1G"}, {"id": "U064KUGRJ"}]
        data_file_mock.side_effect = [
            [  # Channels
                {"name": "general", "members": ["U061A1R2R", "U061A5N1G"]},
                {"name": "sharedchannel", "members": ["U061A1R2R", "U061A3E0G"]},
            ],
            [  # Private channels ("groups")
                {"name": "private", "members": ["U061A1R2R", "U11111111"]},
            ],
            [  # Direct message groups ("mpims")
                {
                    "name": "mpdm-foo--bar--baz-1",
                    "members": ["U061A1R2R", "U061A5N1G", "U22222222"],
                },
            ],
            [  # DMs
                {"id": "D123456", "members": ["U064KUGRJ", "U33333333"]},
            ],
        ]
        messages_mock.return_value = [
            {"user": "U061A1R2R", "team": "T6LARQE2Z"},
            {"user": "U061A5N1G", "team": "T6LARQE2Z"},
            {"user": "U061A8H1G", "team": "T6LARQE2Z"},
            {"subtype": "bot_message", "text": "", "username": "ClickUp", "bot_id": "B06NWMNUQ3W"},
        ]
        # Users info
        slack_users_info_url = "https://slack.com/api/users.info"
        responses.add_callback(responses.GET, slack_users_info_url, callback=request_callback)
        # Team info
        slack_team_info_url = "https://slack.com/api/team.info"
        responses.add_callback(responses.GET, slack_team_info_url, callback=request_callback)
        # Bot info
        slack_bot_info_url = "https://slack.com/api/bots.info"
        responses.add_callback(responses.GET, slack_bot_info_url, callback=request_callback)

        slack_data_dir = self.fixture_file_name("", type="slack_fixtures")
        fetch_shared_channel_users(users, slack_data_dir, "xoxb-valid-token")

        # Normal users
        self.assert_length(users, 9)
        self.assertEqual(users[0]["id"], "U061A1R2R")
        self.assertEqual(users[0]["is_mirror_dummy"], False)
        self.assertFalse("team_domain" in users[0])
        self.assertEqual(users[1]["id"], "U061A5N1G")
        self.assertEqual(users[2]["id"], "U064KUGRJ")

        # Shared channel users
        # We need to do this because the outcome order of `users` list is
        # not deterministic.
        later_users = sorted(users[3:], key=lambda x: x["id"])
        expected_users = [
            ("B06NWMNUQ3W", "ClickUp"),
            ("U061A3E0G", "foreignteam1"),
            ("U061A8H1G", "foreignteam2"),
            ("U11111111", "foreignteam2"),
            ("U22222222", "foreignteam2"),
            ("U33333333", "foreignteam2"),
        ]
        for expected, found in zip(expected_users, later_users, strict=False):
            self.assertEqual(found["id"], expected[0])
            if "bot_id" in found.get("profile", {}):
                self.assertEqual(found["real_name"], expected[1])
                self.assertEqual(found["is_mirror_dummy"], False)
            else:
                self.assertEqual(found["team_domain"], expected[1])
                self.assertEqual(found["is_mirror_dummy"], True)

    @mock.patch("zerver.data_import.slack.get_data_file")
    def test_users_to_zerver_userprofile(self, mock_get_data_file: mock.Mock) -> None:
        custom_profile_field_user1 = {
            "Xf06054BBB": {"value": "random1"},
            "Xf023DSCdd": {"value": "employee"},
        }
        custom_profile_field_user2 = {
            "Xf06054BBB": {"value": "random2"},
            "Xf023DSCdd": {"value": "employer"},
        }
        user_data: list[dict[str, Any]] = [
            {
                "id": "U08RGD1RD",
                "team_id": "T5YFFM2QY",
                "name": "john",
                "deleted": False,
                "is_mirror_dummy": False,
                "real_name": "John Doe",
                "profile": {
                    "image_32": "",
                    "email": "jon@gmail.com",
                    "avatar_hash": "hash",
                    "phone": "+1-123-456-77-868",
                    "fields": custom_profile_field_user1,
                },
            },
            {
                "id": "U0CBK5KAT",
                "team_id": "T5YFFM2QY",
                "is_admin": True,
                "is_bot": False,
                "is_owner": True,
                "is_primary_owner": True,
                "name": "Jane",
                "real_name": "Jane Doe",
                "deleted": False,
                "is_mirror_dummy": False,
                "profile": {
                    "image_32": "https://secure.gravatar.com/avatar/random.png",
                    "fields": custom_profile_field_user2,
                    "email": "jane@foo.com",
                    "avatar_hash": "hash",
                },
            },
            {
                "id": "U09TYF5Sk",
                "team_id": "T5YFFM2QY",
                "name": "Bot",
                "real_name": "Bot",
                "is_bot": True,
                "deleted": False,
                "is_mirror_dummy": False,
                "profile": {
                    "image_32": "https://secure.gravatar.com/avatar/random1.png",
                    "skype": "test_skype_name",
                    "email": "bot1@zulipchat.com",
                    "avatar_hash": "hash",
                },
            },
            {
                "id": "UHSG7OPQN",
                "team_id": "T6LARQE2Z",
                "name": "matt.perry",
                "color": "9d8eee",
                "is_bot": False,
                "is_app_user": False,
                "is_mirror_dummy": True,
                "team_domain": "foreignteam",
                "profile": {
                    "image_32": "https://secure.gravatar.com/avatar/random6.png",
                    "avatar_hash": "hash",
                    "first_name": "Matt",
                    "last_name": "Perry",
                    "real_name": "Matt Perry",
                    "display_name": "matt.perry",
                    "team": "T6LARQE2Z",
                },
            },
            {
                "id": "U8VAHEVUY",
                "team_id": "T5YFFM2QY",
                "name": "steviejacob34",
                "real_name": "Steve Jacob",
                "is_admin": False,
                "is_owner": False,
                "is_primary_owner": False,
                "is_restricted": True,
                "is_ultra_restricted": False,
                "is_bot": False,
                "is_mirror_dummy": False,
                "profile": {
                    "email": "steviejacob34@yahoo.com",
                    "avatar_hash": "hash",
                    "image_32": "https://secure.gravatar.com/avatar/random6.png",
                },
            },
            {
                "id": "U8X25EBAB",
                "team_id": "T5YFFM2QY",
                "name": "pratikweb_0",
                "real_name": "Pratik",
                "is_admin": False,
                "is_owner": False,
                "is_primary_owner": False,
                "is_restricted": True,
                "is_ultra_restricted": True,
                "is_bot": False,
                "is_mirror_dummy": False,
                "profile": {
                    "email": "pratik@mit.edu",
                    "avatar_hash": "hash",
                    "image_32": "https://secure.gravatar.com/avatar/random.png",
                },
            },
            {
                "id": "U015J7JSE",
                "team_id": "T5YFFM2QY",
                "name": "georgesm27",
                "real_name": "George",
                "is_admin": True,
                "is_owner": True,
                "is_primary_owner": False,
                "is_restricted": False,
                "is_ultra_restricted": False,
                "is_bot": False,
                "is_mirror_dummy": False,
                "profile": {
                    "email": "george@yahoo.com",
                    "avatar_hash": "hash",
                    "image_32": "https://secure.gravatar.com/avatar/random5.png",
                },
            },
            {
                "id": "U1RDFEC80",
                "team_id": "T5YFFM2QY",
                "name": "daniel.smith",
                "real_name": "Daniel Smith",
                "is_admin": True,
                "is_owner": False,
                "is_primary_owner": False,
                "is_restricted": False,
                "is_ultra_restricted": False,
                "is_bot": False,
                "is_mirror_dummy": False,
                "profile": {
                    "email": "daniel@gmail.com",
                    "avatar_hash": "hash",
                    "image_32": "https://secure.gravatar.com/avatar/random7.png",
                },
            },
            # Unknown user data format.
            {
                "id": "U1ZYFEC91",
                "team_id": "T5YFFM2QZ",
                "name": "daniel.who",
                "real_name": "Daniel Who",
                "is_admin": True,
                "is_owner": False,
                "is_primary_owner": False,
                "is_restricted": False,
                "is_ultra_restricted": False,
                "is_bot": False,
                "is_mirror_dummy": False,
                "profile": {
                    "email": "daniel.who@gmail.com",
                },
                # Missing the `avatar_hash` value.
            },
            {
                "id": "U1MBOTC81",
                "name": "Integration Bot",
                "deleted": False,
                "is_mirror_dummy": False,
                "real_name": "Integration Bot",
                "is_integration_bot": True,
                "profile": {
                    "image_72": "https://avatars.slack-edge.com/2024-05-01/7057208497908_a4351f6deb91094eac4c_512.png",
                    "bot_id": "B06NWMNUQ3W",
                    "first_name": "Integration Bot",
                },
            },
            # Test error-handling for a bot user with an unknown avatar URL format.
            # In reality, we expect the file to have a file extension type to be
            # `.png` or within the range of `THUMBNAIL_ACCEPT_IMAGE_TYPES`.
            {
                "id": "U1RDFEC90",
                "name": "Unknown Bot",
                "deleted": False,
                "is_mirror_dummy": False,
                "real_name": "Unknown Bot",
                "is_integration_bot": True,
                "profile": {
                    "image_72": "https://avatars.slack-edge.com/2024-05-01/dasdasdasdasdXXXXXX",
                    "bot_id": "B0DSAMNUQ3W",
                    "first_name": "Unknown Bot",
                },
            },
        ]

        mock_get_data_file.return_value = user_data
        # As user with slack_id 'U0CBK5KAT' is the primary owner, that user should be imported first
        # and hence has zulip_id = 1
        test_slack_user_id_to_zulip_user_id = {
            "U08RGD1RD": 1,
            "U0CBK5KAT": 0,
            "U09TYF5Sk": 2,
            "UHSG7OPQN": 3,
            "U8VAHEVUY": 4,
            "U8X25EBAB": 5,
            "U015J7JSE": 6,
            "U1RDFEC80": 7,
            "U1ZYFEC91": 8,
            "U1MBOTC81": 9,
            "U1RDFEC90": 10,
        }
        slack_data_dir = "./random_path"
        timestamp = int(timezone_now().timestamp())
        mock_get_data_file.return_value = user_data

        with self.assertLogs(level="INFO"):
            (
                zerver_userprofile,
                avatar_list,
                slack_user_id_to_zulip_user_id,
                customprofilefield,
                customprofilefield_value,
            ) = users_to_zerver_userprofile(
                slack_data_dir, user_data, 1, timestamp, "testdomain.com"
            )

        # Test custom profile fields
        self.assertEqual(customprofilefield[0]["field_type"], 1)
        self.assertEqual(customprofilefield[3]["name"], "skype")
        cpf_name = {cpf["name"] for cpf in customprofilefield}
        self.assertIn("phone", cpf_name)
        self.assertIn("skype", cpf_name)
