#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2025 New Vector, Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
from http import HTTPStatus
from urllib.parse import urlencode

from parameterized import parameterized

from twisted.internet.testing import MemoryReactor

from synapse.api.errors import StoreError
from synapse.appservice import ApplicationService
from synapse.server import HomeServer
from synapse.types import JsonDict, UserID, create_requester
from synapse.util.clock import Clock

from tests.unittest import skip_unless
from tests.utils import HAS_AUTHLIB

from ._base import BaseTestCase


@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasQueryUserResource(BaseTestCase):
    def test_other_token(self) -> None:
        channel = self.make_request(
            "GET",
            "/_synapse/mas/query_user?localpart=alice",
            shorthand=False,
            access_token="other_token",
        )

        self.assertEqual(channel.code, 403, channel.json_body)
        self.assertEqual(
            channel.json_body["error"], "This endpoint must only be called by MAS"
        )

    def test_query_user(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
                default_display_name="Alice",
            )
        )
        self.get_success(
            store.set_profile_avatar_url(
                user_id=alice,
                new_avatar_url="mxc://example.com/avatar",
            )
        )

        channel = self.make_request(
            "GET",
            "/_synapse/mas/query_user?localpart=alice",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(
            channel.json_body,
            {
                "user_id": "@alice:test",
                "display_name": "Alice",
                "avatar_url": "mxc://example.com/avatar",
                "is_suspended": False,
                "is_deactivated": False,
            },
        )

        self.get_success(
            store.set_user_suspended_status(user_id=str(alice), suspended=True)
        )

        channel = self.make_request(
            "GET",
            "/_synapse/mas/query_user?localpart=alice",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(
            channel.json_body,
            {
                "user_id": "@alice:test",
                "display_name": "Alice",
                "avatar_url": "mxc://example.com/avatar",
                "is_suspended": True,
                "is_deactivated": False,
            },
        )

        # Deactivate the account, it should clear the display name and avatar
        # and mark the user as deactivated
        self.get_success(
            self.hs.get_deactivate_account_handler().deactivate_account(
                user_id=str(alice),
                erase_data=True,
                requester=create_requester(alice),
            )
        )

        channel = self.make_request(
            "GET",
            "/_synapse/mas/query_user?localpart=alice",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(
            channel.json_body,
            {
                "user_id": "@alice:test",
                "display_name": None,
                "avatar_url": None,
                "is_suspended": True,
                "is_deactivated": True,
            },
        )

    def test_query_unknown_user(self) -> None:
        channel = self.make_request(
            "GET",
            "/_synapse/mas/query_user?localpart=alice",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 404, channel.json_body)

    def test_query_user_missing_localpart(self) -> None:
        channel = self.make_request(
            "GET",
            "/_synapse/mas/query_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 400, channel.json_body)


@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasProvisionUserResource(BaseTestCase):
    def test_other_token(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token="other_token",
            content={"localpart": "alice"},
        )

        self.assertEqual(channel.code, 403, channel.json_body)
        self.assertEqual(
            channel.json_body["error"], "This endpoint must only be called by MAS"
        )

    def test_provision_user(self) -> None:
        store = self.hs.get_datastores().main

        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": "alice",
                "set_displayname": "Alice",
                "set_emails": ["alice@example.com"],
                "set_avatar_url": "mxc://example.com/avatar",
            },
        )

        # This created the user, hence the 201 status code
        self.assertEqual(channel.code, 201, channel.json_body)
        self.assertEqual(channel.json_body, {})

        alice = UserID("alice", "test")
        profile = self.get_success(store.get_profileinfo(alice))
        self.assertEqual(profile.display_name, "Alice")
        self.assertEqual(profile.avatar_url, "mxc://example.com/avatar")
        threepids = self.get_success(store.user_get_threepids(str(alice)))
        self.assertEqual(len(threepids), 1)
        self.assertEqual(threepids[0].medium, "email")
        self.assertEqual(threepids[0].address, "alice@example.com")

        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": "alice",
                "unset_displayname": True,
                "unset_avatar_url": True,
                "unset_emails": True,
            },
        )

        # This updated the user, hence the 200 status code
        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

        # Check that the profile and threepids were deleted
        profile = self.get_success(store.get_profileinfo(alice))
        self.assertEqual(profile.display_name, None)
        self.assertEqual(profile.avatar_url, None)
        threepids = self.get_success(store.user_get_threepids(str(alice)))
        self.assertEqual(threepids, [])

    def test_provision_user_missing_localpart(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "set_displayname": "Alice",
            },
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_provision_user_empty_localpart(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": "",
                "set_displayname": "Alice",
            },
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_provision_user_invalid_localpart(self) -> None:
        # Test with characters that are invalid in localparts
        invalid_localparts = [
            "@alice:test",  # That's a MXID
            "alice@domain.com",
            "alice:test",
            "alice space",
            "alice#hash",
            "a" * 1000,  # Very long localpart
        ]

        for localpart in invalid_localparts:
            channel = self.make_request(
                "POST",
                "/_synapse/mas/provision_user",
                shorthand=False,
                access_token=self.SHARED_SECRET,
                content={
                    "localpart": localpart,
                    "set_displayname": "Alice",
                },
            )
            # Should be a validation error
            self.assertEqual(
                channel.code, 400, f"Should fail for localpart: {localpart}"
            )

    def test_provision_user_multiple_emails(self) -> None:
        store = self.hs.get_datastores().main

        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": "alice",
                "set_emails": ["alice@example.com", "alice.alt@example.com"],
            },
        )

        self.assertEqual(channel.code, 201, channel.json_body)

        alice = UserID("alice", "test")
        threepids = self.get_success(store.user_get_threepids(str(alice)))
        self.assertEqual(len(threepids), 2)
        email_addresses = {tp.address for tp in threepids}
        self.assertEqual(
            email_addresses, {"alice@example.com", "alice.alt@example.com"}
        )

    def test_provision_user_duplicate_emails(self) -> None:
        store = self.hs.get_datastores().main

        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": "alice",
                "set_emails": ["alice@example.com", "alice@example.com"],
            },
        )

        self.assertEqual(channel.code, 201, channel.json_body)

        alice = UserID("alice", "test")
        threepids = self.get_success(store.user_get_threepids(str(alice)))
        # Should deduplicate
        self.assertEqual(len(threepids), 1)
        self.assertEqual(threepids[0].address, "alice@example.com")

    def test_provision_user_conflicting_operations(self) -> None:
        # Test setting and unsetting the same field
        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": "alice",
                "set_displayname": "Alice",
                "unset_displayname": True,
            },
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_provision_user_invalid_json_types(self) -> None:
        # Test with wrong data types
        invalid_contents: list[JsonDict] = [
            {"localpart": "alice", "set_displayname": 123},  # Number instead of string
            {
                "localpart": "alice",
                "set_emails": "not-an-array",
            },  # String instead of array
            {
                "localpart": "alice",
                "unset_displayname": "not-a-bool",
            },  # String instead of bool
            {"localpart": 123},  # Number instead of string for localpart
        ]

        for content in invalid_contents:
            channel = self.make_request(
                "POST",
                "/_synapse/mas/provision_user",
                shorthand=False,
                access_token=self.SHARED_SECRET,
                content=content,
            )
            self.assertEqual(channel.code, 400, f"Should fail for content: {content}")

    def test_lock_and_unlock(self) -> None:
        store = self.hs.get_datastores().main

        # Create a user in the locked state
        alice = UserID("alice", "test")
        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": alice.localpart,
                "locked": True,
            },
        )
        # This created the user, hence the 201 status code
        self.assertEqual(channel.code, 201, channel.json_body)
        self.assertEqual(channel.json_body, {})
        self.assertTrue(
            self.get_success(store.get_user_locked_status(alice.to_string()))
        )

        # Then transition from locked to unlocked
        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": alice.localpart,
                "locked": False,
            },
        )
        # This updated the user, hence the 200 status code
        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})
        self.assertFalse(
            self.get_success(store.get_user_locked_status(alice.to_string()))
        )

        # And back from unlocked to locked
        channel = self.make_request(
            "POST",
            "/_synapse/mas/provision_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": alice.localpart,
                "locked": True,
            },
        )
        # This updated the user, hence the 200 status code
        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})
        self.assertTrue(
            self.get_success(store.get_user_locked_status(alice.to_string()))
        )


@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasIsLocalpartAvailableResource(BaseTestCase):
    def prepare(
        self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
    ) -> None:
        # Provision a user
        store = homeserver.get_datastores().main
        self.get_success(store.register_user("@alice:test"))

    def test_other_token(self) -> None:
        channel = self.make_request(
            "GET",
            "/_synapse/mas/is_localpart_available?localpart=alice",
            shorthand=False,
            access_token="other_token",
        )

        self.assertEqual(channel.code, 403, channel.json_body)
        self.assertEqual(
            channel.json_body["error"], "This endpoint must only be called by MAS"
        )

    def test_is_localpart_available(self) -> None:
        # "alice" is not available
        channel = self.make_request(
            "GET",
            "/_synapse/mas/is_localpart_available?localpart=alice",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 400, channel.json_body)
        self.assertEqual(channel.json_body["errcode"], "M_USER_IN_USE")

        # "bob" is available
        channel = self.make_request(
            "GET",
            "/_synapse/mas/is_localpart_available?localpart=bob",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

    def test_is_localpart_available_invalid_localparts(self) -> None:
        # Numeric-only localparts are not allowed
        channel = self.make_request(
            "GET",
            "/_synapse/mas/is_localpart_available?localpart=0",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 400, channel.json_body)
        self.assertEqual(channel.json_body["errcode"], "M_INVALID_USERNAME")

        # A super-long MXID is not allowed by the spec
        super_long = "a" * 1000
        channel = self.make_request(
            "GET",
            f"/_synapse/mas/is_localpart_available?localpart={super_long}",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 400, channel.json_body)
        self.assertEqual(channel.json_body["errcode"], "M_INVALID_USERNAME")

    def test_is_localpart_available_appservice_exclusive(self) -> None:
        # Insert an appservice which has exclusive namespaces
        appservice = ApplicationService(
            token="i_am_an_app_service",
            id="1234",
            namespaces={"users": [{"regex": r"@as_user_.*:.+", "exclusive": True}]},
            sender=UserID.from_string("@as_main:test"),
        )
        self.hs.get_datastores().main.services_cache = [appservice]

        channel = self.make_request(
            "GET",
            "/_synapse/mas/is_localpart_available?localpart=as_main",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 400, channel.json_body)
        self.assertEqual(channel.json_body["errcode"], "M_EXCLUSIVE")

        channel = self.make_request(
            "GET",
            "/_synapse/mas/is_localpart_available?localpart=as_user_alice",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 400, channel.json_body)
        self.assertEqual(channel.json_body["errcode"], "M_EXCLUSIVE")

        # Sanity-check that "bob" is available
        channel = self.make_request(
            "GET",
            "/_synapse/mas/is_localpart_available?localpart=bob",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

    def test_is_localpart_available_missing_localpart(self) -> None:
        channel = self.make_request(
            "GET",
            "/_synapse/mas/is_localpart_available",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_is_localpart_available_empty_localpart(self) -> None:
        channel = self.make_request(
            "GET",
            "/_synapse/mas/is_localpart_available?localpart=",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_is_localpart_available_invalid_characters(self) -> None:
        # Test with characters that are invalid in localparts
        invalid_localparts = [
            "alice@domain.com",  # Contains @
            "alice:test",  # Contains :
            "alice space",  # Contains space
            "alice\\backslash",  # Contains backslash
            "alice#hash",  # Contains hash
            "alice$dollar",  # Contains $
            "alice%percent",  # Contains %
            "alice&amp",  # Contains &
            "alice?question",  # Contains ?
            "alice[bracket",  # Contains [
            "alice]bracket",  # Contains ]
            "alice{brace",  # Contains {
            "alice}brace",  # Contains }
            "alice|pipe",  # Contains |
            'alice"quote',  # Contains "
            "alice'apostrophe",  # Contains '
            "alice<less",  # Contains <
            "alice>greater",  # Contains >
            "alice\ttab",  # Contains tab
            "alice\nnewline",  # Contains newline
        ]

        for localpart in invalid_localparts:
            channel = self.make_request(
                "GET",
                f"/_synapse/mas/is_localpart_available?{urlencode({'localpart': localpart})}",
                shorthand=False,
                access_token=self.SHARED_SECRET,
            )
            # Should return 400 for invalid characters
            self.assertEqual(
                channel.code,
                400,
                f"Should reject localpart with invalid chars: {localpart}",
            )
            self.assertEqual(
                channel.json_body["errcode"], "M_INVALID_USERNAME", localpart
            )

    def test_is_localpart_available_case_sensitivity(self) -> None:
        # Register a user with an uppercase localpart
        self.get_success(self.hs.get_datastores().main.register_user("@BOB:test"))

        # It should report as not available, the search should be case-insensitive
        channel = self.make_request(
            "GET",
            "/_synapse/mas/is_localpart_available?localpart=bob",
            shorthand=False,
            access_token=self.SHARED_SECRET,
        )

        self.assertEqual(channel.code, 400, channel.json_body)
        self.assertEqual(channel.json_body["errcode"], "M_USER_IN_USE")


@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasDeleteUserResource(BaseTestCase):
    def prepare(
        self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
    ) -> None:
        # Provision a user with a display name
        self.get_success(
            homeserver.get_registration_handler().register_user(
                localpart="alice",
                default_display_name="Alice",
            )
        )

    def test_other_token(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token="other_token",
            content={"localpart": "alice", "erase": False},
        )

        self.assertEqual(channel.code, 403, channel.json_body)
        self.assertEqual(
            channel.json_body["error"], "This endpoint must only be called by MAS"
        )

    def test_delete_user_no_erase(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main

        # Delete the user
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice", "erase": False},
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

        # Check that the user was deleted
        self.assertTrue(
            self.get_success(store.get_user_deactivated_status(user_id=str(alice)))
        )
        # But not erased
        self.assertFalse(self.get_success(store.is_user_erased(user_id=str(alice))))

    def test_delete_user_erase(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main

        # Delete the user
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice", "erase": True},
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

        # Check that the user was deleted
        self.assertTrue(
            self.get_success(store.get_user_deactivated_status(user_id=str(alice)))
        )
        # And erased
        self.assertTrue(self.get_success(store.is_user_erased(user_id=str(alice))))

    @parameterized.expand([(False,), (True,)])
    def test_user_deactivation_removes_user_from_user_directory(
        self, erase: bool
    ) -> None:
        """
        Test that when `/delete_user` deactivates a user, they get removed from the user directory.

        This is applicable regardless of whether the `erase` flag is set.

        Regression test for:
            'Users are not removed from user directory upon deactivation if erase flag is true'
            https://github.com/element-hq/synapse/issues/19540
        """
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main

        # Ensure we're testing what we think we are:
        # check the user is IN the directory at the start of the test
        self.assertEqual(
            self.get_success(store._get_user_in_directory(alice.to_string())),
            ("Alice", None),
        )

        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice", "erase": erase},
        )
        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

        self.assertIsNone(
            self.get_success(store._get_user_in_directory(alice.to_string())),
            "User still present in user directory after deactivation!",
        )

    def test_user_deactivation_removes_custom_profile_fields(self) -> None:
        """
        Test that when `/delete_user` deactivates a user with the `erase` flag,
        their custom profile fields get removed.
        """
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main

        # Add custom profile field
        self.get_success(store.set_profile_field(alice, "io.element.example", "hello"))

        # Ensure we're testing what we think we are:
        # check the user has profile data at the start of the test
        self.assertEqual(
            self.get_success(store.get_profile_fields(alice)),
            {"io.element.example": "hello"},
        )

        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice", "erase": True},
        )
        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

        # With no profile, we expect a 404 (Not Found) StoreError
        with self.assertRaises(StoreError) as raised:
            self.get_success_or_raise(store.get_profile_fields(alice))

        self.assertEqual(raised.exception.code, HTTPStatus.NOT_FOUND)

    def test_delete_user_missing_localpart(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"erase": False},
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_delete_user_missing_erase(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_delete_user_invalid_erase_type(self) -> None:
        invalid_erase_values = [
            "true",  # String instead of bool
            1,  # Number instead of bool
            "false",  # String instead of bool
            0,  # Number instead of bool
            {},  # Object instead of bool
            [],  # Array instead of bool
        ]

        for erase_value in invalid_erase_values:
            channel = self.make_request(
                "POST",
                "/_synapse/mas/delete_user",
                shorthand=False,
                access_token=self.SHARED_SECRET,
                content={"localpart": "alice", "erase": erase_value},
            )
            self.assertEqual(
                channel.code, 400, f"Should fail for erase value: {erase_value}"
            )

    def test_delete_nonexistent_user(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "nonexistent", "erase": False},
        )

        self.assertEqual(channel.code, 404)

    def test_delete_already_deleted_user(self) -> None:
        # First deletion
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice", "erase": False},
        )
        self.assertEqual(channel.code, 200)

        # Second deletion should be idempotent
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice", "erase": False},
        )
        self.assertEqual(channel.code, 200)

    def test_delete_user_erase_already_deleted_user(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main

        # First delete without erase
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice", "erase": False},
        )
        self.assertEqual(channel.code, 200)

        # Verify not erased initially
        self.assertFalse(self.get_success(store.is_user_erased(user_id=str(alice))))

        # Now delete with erase
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice", "erase": True},
        )
        self.assertEqual(channel.code, 200)

        # Should now be erased
        self.assertTrue(self.get_success(store.is_user_erased(user_id=str(alice))))

    def test_delete_user_empty_json(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={},
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_delete_user_extra_fields(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/delete_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": "alice",
                "erase": False,
                "extra_field": "should_be_ignored",
                "another_field": 123,
            },
        )

        # Should succeed and ignore extra fields
        self.assertEqual(channel.code, 200, channel.json_body)


@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasReactivateUserResource(BaseTestCase):
    def test_other_token(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/reactivate_user",
            shorthand=False,
            access_token="other_token",
            content={"localpart": "alice"},
        )

        self.assertEqual(channel.code, 403, channel.json_body)
        self.assertEqual(
            channel.json_body["error"], "This endpoint must only be called by MAS"
        )

    def test_reactivate_user(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
                default_display_name="Alice",
            )
        )
        self.get_success(
            self.hs.get_deactivate_account_handler().deactivate_account(
                user_id=str(alice),
                erase_data=True,
                requester=create_requester(alice),
            )
        )

        channel = self.make_request(
            "POST",
            "/_synapse/mas/reactivate_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

        # Check that the user was reactivated
        self.assertFalse(
            self.get_success(store.get_user_deactivated_status(user_id=str(alice)))
        )

    def test_reactivate_user_missing_localpart(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/reactivate_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={},
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_reactivate_nonexistent_user(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/reactivate_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "nonexistent"},
        )

        self.assertEqual(channel.code, 404, channel.json_body)

    def test_reactivate_active_user(self) -> None:
        # Create an active user
        alice = UserID("alice", "test")
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
                default_display_name="Alice",
            )
        )

        channel = self.make_request(
            "POST",
            "/_synapse/mas/reactivate_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )

        # Should be idempotent
        self.assertEqual(channel.code, 200, channel.json_body)

    def test_reactivate_erased_user(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
                default_display_name="Alice",
            )
        )

        # Deactivate with erase
        self.get_success(
            self.hs.get_deactivate_account_handler().deactivate_account(
                user_id=str(alice),
                erase_data=True,
                requester=create_requester(alice),
            )
        )

        # Verify user is erased
        self.assertTrue(self.get_success(store.is_user_erased(user_id=str(alice))))

        channel = self.make_request(
            "POST",
            "/_synapse/mas/reactivate_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )

        # Should succeed even for erased users
        self.assertEqual(channel.code, 200, channel.json_body)
        # Shouldn't be erased anymore
        self.assertFalse(self.get_success(store.is_user_erased(user_id=str(alice))))

    def test_reactivate_user_extra_fields(self) -> None:
        alice = UserID("alice", "test")
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
            )
        )
        self.get_success(
            self.hs.get_deactivate_account_handler().deactivate_account(
                user_id=str(alice),
                erase_data=False,
                requester=create_requester(alice),
            )
        )

        channel = self.make_request(
            "POST",
            "/_synapse/mas/reactivate_user",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": "alice",
                "extra_field": "should_be_ignored",
                "another_field": 123,
            },
        )

        # Should succeed and ignore extra fields
        self.assertEqual(channel.code, 200, channel.json_body)


@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasSetDisplayNameResource(BaseTestCase):
    def test_other_token(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/set_displayname",
            shorthand=False,
            access_token="other_token",
            content={"localpart": "alice", "displayname": "Bob"},
        )

        self.assertEqual(channel.code, 403, channel.json_body)
        self.assertEqual(
            channel.json_body["error"], "This endpoint must only be called by MAS"
        )

    def test_set_display_name(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
                default_display_name="Alice",
            )
        )
        profile = self.get_success(store.get_profileinfo(alice))
        self.assertEqual(profile.display_name, "Alice")

        channel = self.make_request(
            "POST",
            "/_synapse/mas/set_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice", "displayname": "Bob"},
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

        # Check that the profile was updated
        profile = self.get_success(store.get_profileinfo(alice))
        self.assertEqual(profile.display_name, "Bob")

    def test_set_display_name_missing_localpart(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/set_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"displayname": "Bob"},
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_set_display_name_missing_displayname(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/set_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_set_display_name_very_long(self) -> None:
        alice = UserID("alice", "test")
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
            )
        )

        long_name = "A" * 1000
        channel = self.make_request(
            "POST",
            "/_synapse/mas/set_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice", "displayname": long_name},
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_set_display_name_special_characters(self) -> None:
        alice = UserID("alice", "test")
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
            )
        )

        special_names = [
            "Alice 👋",  # Emoji
            "Alice & Bob",  # HTML entities
            "Alice\nNewline",  # Newline
            "Alice\tTab",  # Tab
            'Alice"Quote',  # Quote
            "Alice'Apostrophe",  # Apostrophe
            "Alice<script>",  # HTML tags
        ]

        for name in special_names:
            channel = self.make_request(
                "POST",
                "/_synapse/mas/set_displayname",
                shorthand=False,
                access_token=self.SHARED_SECRET,
                content={"localpart": "alice", "displayname": name},
            )

            # Should handle special characters gracefully
            self.assertEqual(channel.code, 200, f"Failed for name: {repr(name)}")

    def test_set_display_name_nonexistent_user(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/set_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "nonexistent", "displayname": "Bob"},
        )

        # Should fail for non-existent user
        self.assertEqual(channel.code, 404, channel.json_body)

    def test_set_display_name_invalid_json_type(self) -> None:
        alice = UserID("alice", "test")
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
            )
        )

        invalid_types = [
            123,  # Number
            True,  # Boolean
            [],  # Array
            {},  # Object
            None,  # Null
        ]

        for invalid_displayname in invalid_types:
            channel = self.make_request(
                "POST",
                "/_synapse/mas/set_displayname",
                shorthand=False,
                access_token=self.SHARED_SECRET,
                content={"localpart": "alice", "displayname": invalid_displayname},
            )
            self.assertEqual(
                channel.code, 400, f"Should fail for type: {type(invalid_displayname)}"
            )


@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasUnsetDisplayNameResource(BaseTestCase):
    def test_other_token(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/unset_displayname",
            shorthand=False,
            access_token="other_token",
            content={"localpart": "alice"},
        )

        self.assertEqual(channel.code, 403, channel.json_body)
        self.assertEqual(
            channel.json_body["error"], "This endpoint must only be called by MAS"
        )

    def test_unset_display_name(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
                default_display_name="Alice",
            )
        )

        channel = self.make_request(
            "POST",
            "/_synapse/mas/unset_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

        # Check that the profile was updated
        profile = self.get_success(store.get_profileinfo(alice))
        self.assertEqual(profile.display_name, None)

    def test_unset_display_name_missing_localpart(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/unset_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={},
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_unset_display_name_idempotent(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
            )
        )

        channel = self.make_request(
            "POST",
            "/_synapse/mas/unset_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )

        self.assertEqual(channel.code, 200, channel.json_body)

        channel = self.make_request(
            "POST",
            "/_synapse/mas/unset_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )

        # Should be idempotent
        self.assertEqual(channel.code, 200, channel.json_body)

        # Still should be None
        profile = self.get_success(store.get_profileinfo(alice))
        self.assertEqual(profile.display_name, None)

    def test_unset_display_name_nonexistent_user(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/unset_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "nonexistent"},
        )

        # Should fail for non-existent user
        self.assertEqual(channel.code, 404, channel.json_body)

    def test_unset_display_name_extra_fields(self) -> None:
        alice = UserID("alice", "test")
        self.get_success(
            self.hs.get_registration_handler().register_user(
                localpart=alice.localpart,
                default_display_name="Alice",
            )
        )

        channel = self.make_request(
            "POST",
            "/_synapse/mas/unset_displayname",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": "alice",
                "extra_field": "should_be_ignored",
                "another_field": 123,
            },
        )

        # Should succeed and ignore extra fields
        self.assertEqual(channel.code, 200, channel.json_body)

    def test_unset_display_name_invalid_localpart_type(self) -> None:
        invalid_types = [
            123,  # Number
            True,  # Boolean
            [],  # Array
            {},  # Object
            None,  # Null
        ]

        for invalid_localpart in invalid_types:
            channel = self.make_request(
                "POST",
                "/_synapse/mas/unset_displayname",
                shorthand=False,
                access_token=self.SHARED_SECRET,
                content={"localpart": invalid_localpart},
            )
            self.assertEqual(
                channel.code, 400, f"Should fail for type: {type(invalid_localpart)}"
            )


@skip_unless(HAS_AUTHLIB, "requires authlib")
class MasAllowCrossSigningResetResource(BaseTestCase):
    def test_other_token(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/allow_cross_signing_reset",
            shorthand=False,
            access_token="other_token",
            content={"localpart": "alice"},
        )

        self.assertEqual(channel.code, 403, channel.json_body)
        self.assertEqual(
            channel.json_body["error"], "This endpoint must only be called by MAS"
        )

    def test_allow_cross_signing_reset(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main
        handler = self.hs.get_e2e_keys_handler()
        self.get_success(
            self.hs.get_registration_handler().register_user(localpart=alice.localpart)
        )

        # Upload a master key
        dummy_key = {"keys": {"a": "b"}}
        self.get_success(
            store.set_e2e_cross_signing_key(str(alice), "master", dummy_key)
        )

        # The key exists but is not replaceable without UIA
        exists, replaceable_without_uia = self.get_success(
            handler.check_cross_signing_setup(user_id=str(alice))
        )
        self.assertIs(exists, True)
        self.assertIs(replaceable_without_uia, False)

        channel = self.make_request(
            "POST",
            "/_synapse/mas/allow_cross_signing_reset",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )

        self.assertEqual(channel.code, 200, channel.json_body)
        self.assertEqual(channel.json_body, {})

        # Now it should be replaceable without UIA
        exists, replaceable_without_uia = self.get_success(
            handler.check_cross_signing_setup(user_id=str(alice))
        )
        self.assertIs(exists, True)
        self.assertIs(replaceable_without_uia, True)

    def test_allow_cross_signing_reset_missing_localpart(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/allow_cross_signing_reset",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={},
        )

        self.assertEqual(channel.code, 400, channel.json_body)

    def test_allow_cross_signing_reset_nonexistent_user(self) -> None:
        channel = self.make_request(
            "POST",
            "/_synapse/mas/allow_cross_signing_reset",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "nonexistent"},
        )

        self.assertEqual(channel.code, 404, channel.json_body)

    def test_allow_cross_signing_reset_no_keys(self) -> None:
        # Test with a user that has no cross-signing keys
        alice = UserID("alice", "test")
        handler = self.hs.get_e2e_keys_handler()
        self.get_success(
            self.hs.get_registration_handler().register_user(localpart=alice.localpart)
        )

        # Verify no keys exist
        exists, replaceable_without_uia = self.get_success(
            handler.check_cross_signing_setup(user_id=str(alice))
        )
        self.assertIs(exists, False)

        channel = self.make_request(
            "POST",
            "/_synapse/mas/allow_cross_signing_reset",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )

        # Should succeed even with no existing keys
        self.assertEqual(channel.code, 200, channel.json_body)

    def test_allow_cross_signing_reset_already_replaceable(self) -> None:
        alice = UserID("alice", "test")
        store = self.hs.get_datastores().main
        handler = self.hs.get_e2e_keys_handler()
        self.get_success(
            self.hs.get_registration_handler().register_user(localpart=alice.localpart)
        )

        # Upload a master key
        dummy_key = {"keys": {"a": "b"}}
        self.get_success(
            store.set_e2e_cross_signing_key(str(alice), "master", dummy_key)
        )

        # First call to make it replaceable
        channel = self.make_request(
            "POST",
            "/_synapse/mas/allow_cross_signing_reset",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )
        self.assertEqual(channel.code, 200)

        # Verify it's now replaceable
        exists, replaceable_without_uia = self.get_success(
            handler.check_cross_signing_setup(user_id=str(alice))
        )
        self.assertIs(exists, True)
        self.assertIs(replaceable_without_uia, True)

        # Second call should be idempotent
        channel = self.make_request(
            "POST",
            "/_synapse/mas/allow_cross_signing_reset",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={"localpart": "alice"},
        )
        self.assertEqual(channel.code, 200, channel.json_body)

    def test_allow_cross_signing_reset_extra_fields(self) -> None:
        alice = UserID("alice", "test")
        self.get_success(
            self.hs.get_registration_handler().register_user(localpart=alice.localpart)
        )

        channel = self.make_request(
            "POST",
            "/_synapse/mas/allow_cross_signing_reset",
            shorthand=False,
            access_token=self.SHARED_SECRET,
            content={
                "localpart": "alice",
                "extra_field": "should_be_ignored",
                "another_field": 123,
            },
        )

        # Should succeed and ignore extra fields
        self.assertEqual(channel.code, 200, channel.json_body)

    def test_allow_cross_signing_reset_invalid_localpart_type(self) -> None:
        invalid_types = [
            123,  # Number
            True,  # Boolean
            [],  # Array
            {},  # Object
            None,  # Null
        ]

        for invalid_localpart in invalid_types:
            channel = self.make_request(
                "POST",
                "/_synapse/mas/allow_cross_signing_reset",
                shorthand=False,
                access_token=self.SHARED_SECRET,
                content={"localpart": invalid_localpart},
            )
            self.assertEqual(
                channel.code, 400, f"Should fail for type: {type(invalid_localpart)}"
            )
