from unittest import TestCase

import pytest
from parameterized import parameterized
from pydantic import ValidationError
from synapse.api.constants import CREATOR_POWER_LEVEL, EventTypes

from famedly_control_synapse.rest.types import (
    CreateManagedRoomRequest,
    CreationContent,
    PowerLevelEventContent,
)


class TestCreationContent:
    def test_valid_creator_for_v10(self) -> None:
        content = CreationContent(creator="@alice:example.com")
        assert content.creator == "@alice:example.com"

    def test_m_federate_always_false(self) -> None:
        content = CreationContent()
        assert content.m_federate is False

        with pytest.raises(ValidationError):
            content = CreationContent(m_federate=True)


class TestPowerLevelEventContent:
    def test_valid_power_level_event_content(self) -> None:
        json_dict: dict = {
            "users": {"@alice:example.com": 100, "@bob:example.com": 50},
            "users_default": 0,
        }
        plc = PowerLevelEventContent.model_validate(
            json_dict, context={"room_creator": "@room_creator:example.com"}
        )
        assert plc.users["@alice:example.com"] == 100
        assert plc.users["@bob:example.com"] == 50
        assert plc.users_default == 0

    def test_invalid_users_type(self) -> None:
        json_dict: dict = {
            "users": ["@alice:example.com", "@bob:example.com"],
            "users_default": 0,
        }

        with pytest.raises(ValidationError):
            PowerLevelEventContent.model_validate(
                json_dict, context={"room_creator": "@room_creator:example.com"}
            )

    def test_matrix_default_values_are_not_serialized(self) -> None:
        """
        Test verify that serializing the power level event does not contain matrix spec
        defaulted data
        """
        # First test that an empty payload is as expected
        json_dict: dict = {}
        plc = PowerLevelEventContent.model_validate(json_dict)
        assert plc.users_default == 0
        assert plc.ban == CREATOR_POWER_LEVEL - 1
        dumped_dict = plc.model_dump()
        # users_default should be None as 0 is its default
        assert dumped_dict.get("users_default") is None
        # events_default should be None as 0 is its default
        assert dumped_dict.get("events_default") is None
        # redact should be None as 50 is its default
        assert dumped_dict.get("redact") is None
        # state_default should be None as 50 is its default
        assert dumped_dict.get("state_default") is None
        assert dumped_dict.get("ban") == CREATOR_POWER_LEVEL - 1

        # Then test that explicitly giving a value that is the default still is as expected
        json_dict = {
            "users_default": 0,
            "events_default": 0,
            "redact": 50,
            "state_default": 50,
        }
        plc = PowerLevelEventContent.model_validate(json_dict)
        assert plc.users_default == 0
        assert plc.ban == CREATOR_POWER_LEVEL - 1
        dumped_dict = plc.model_dump()
        # users_default should be None as 0 is its default
        assert dumped_dict.get("users_default") is None
        # events_default should be None as 0 is its default
        assert dumped_dict.get("events_default") is None
        # redact should be None as 50 is its default
        assert dumped_dict.get("redact") is None
        # state_default should be None as 50 is its default
        assert dumped_dict.get("state_default") is None
        assert dumped_dict.get("ban") == CREATOR_POWER_LEVEL - 1

        # Then test that making the value a non-default is being produced in the result
        json_dict = {
            "users_default": 1,
            "events_default": 1,
            "redact": 51,
            "state_default": 51,
        }
        plc = PowerLevelEventContent.model_validate(json_dict)
        assert plc.users_default == 1
        assert plc.ban == CREATOR_POWER_LEVEL - 1
        dumped_dict = plc.model_dump()
        # Should all be present as they are explicitly defined as a non-default value
        assert dumped_dict.get("users_default") == 1
        assert dumped_dict.get("events_default") == 1
        assert dumped_dict.get("redact") == 51
        assert dumped_dict.get("state_default") == 51
        assert dumped_dict.get("ban") == CREATOR_POWER_LEVEL - 1


class TestCreateManagedRoomRequest(TestCase):
    """
    Exercise the various options in the shape of the CreateManagedRoomRequest, to make
    sure it behaves as expected.

    As notes:
        * It is not expected here that the creation_content.creator field is filled in, as
        this is overridden by Synapse at the time of room creation.
        * While testing that a room version is passed in and in the correct places, the
        default behavior is to use the default room version defined by the server, so it
        is None here and filled in during the request
    """

    @parameterized.expand([("10",), ("12",)])
    def test_default(self, room_version) -> None:
        """
        Test that a basic object is created with expected parameters
        """
        json_dict = {
            "room_alias_name": "testroom",
            "name": "Test Room",
            "room_version": room_version,
            "groups": ["testgroup"],
        }

        req = CreateManagedRoomRequest.model_validate(
            json_dict, context={"room_creator": "@room_creator:example.com"}
        )
        assert req.room_alias_name == "testroom"
        assert req.name == "Test Room"
        assert req.room_version == room_version
        # This does not need to exist here. It will be overridden during the room
        # creation, so is not needed
        assert req.creation_content.room_version is None
        assert req.creation_content.creator is None
        assert req.initial_state == [
            {
                "content": {
                    "guest_access": "forbidden",
                },
                "state_key": "",
                "type": "m.room.guest_access",
            },
            {
                "content": {
                    "join_rule": "invite",
                },
                "state_key": "",
                "type": "m.room.join_rules",
            },
        ]
        assert req.is_direct is False
        assert req.visibility == "private"
        # At the time this object is created, the room creator is not present in room
        # v10, this is added at the time of the request being processed. Users should be
        # empty
        assert len(req.power_level_content_override.users) == 0
        # By default, 3 event types are included in the events object
        self.assertDictEqual(
            req.power_level_content_override.events,
            {
                EventTypes.PowerLevels: CREATOR_POWER_LEVEL - 1,
                EventTypes.JoinRules: CREATOR_POWER_LEVEL - 1,
                EventTypes.GuestAccess: CREATOR_POWER_LEVEL - 1,
            },
        )
        assert req.power_level_content_override.ban == CREATOR_POWER_LEVEL - 1
        assert req.power_level_content_override.invite == CREATOR_POWER_LEVEL - 1
        assert req.power_level_content_override.kick == CREATOR_POWER_LEVEL - 1
        assert req.power_level_content_override.redact == 50
        assert req.power_level_content_override.events_default == 0
        assert req.power_level_content_override.users_default == 0
        assert req.power_level_content_override.state_default == 50

        # A single group was requested, make sure it is there
        assert "testgroup" in req.groups
        assert len(req.groups) == 1

    def test_with_none_powerlevel(self) -> None:
        """
        Test passing a None as the powerlevel override is prohibited
        """
        # Then try by setting as None. Expect a ValidationError
        room_config = {
            "room_alias_name": "testroom",
            "name": "Test Room",
            "power_level_content_override": None,
            "groups": ["testgroup"],
        }

        with pytest.raises(ValidationError):
            CreateManagedRoomRequest.model_validate(
                room_config, context={"room_creator": "@room_creator:example.com"}
            )

    def test_with_empty_powerlevel(self) -> None:
        """
        Test that adding an empty power level object does not allow circumventing all
        power levels
        """
        # First try with an empty dict, should still get the defaults
        room_config = {
            "room_alias_name": "testroom",
            "name": "Test Room",
            "power_level_content_override": {},
            "groups": [],
        }

        req = CreateManagedRoomRequest.model_validate(
            room_config, context={"room_creator": "@room_creator:example.com"}
        )
        assert len(req.power_level_content_override.users) == 0
        # By default, 3 event types are included in the events object
        self.assertDictEqual(
            req.power_level_content_override.events,
            {
                EventTypes.PowerLevels: CREATOR_POWER_LEVEL - 1,
                EventTypes.JoinRules: CREATOR_POWER_LEVEL - 1,
                EventTypes.GuestAccess: CREATOR_POWER_LEVEL - 1,
            },
        )
        assert req.power_level_content_override.ban == CREATOR_POWER_LEVEL - 1
        assert req.power_level_content_override.invite == CREATOR_POWER_LEVEL - 1
        assert req.power_level_content_override.kick == CREATOR_POWER_LEVEL - 1
        assert req.power_level_content_override.redact == 50
        assert req.power_level_content_override.events_default == 0
        assert req.power_level_content_override.users_default == 0
        assert req.power_level_content_override.state_default == 50

    def test_arbitrary_event_type_override_does_not_skip_defaults(self) -> None:
        """
        Test that using unprotected event types in the power level content override does
        not affect said overrides
        """
        room_config = {
            "room_alias_name": "testroom",
            "name": "Test Room",
            "power_level_content_override": {"events": {EventTypes.Message: 10}},
            "groups": [],
        }

        req = CreateManagedRoomRequest.model_validate(
            room_config, context={"room_creator": "@room_creator:example.com"}
        )
        assert len(req.power_level_content_override.users) == 0
        # By default, 3 event types are included in the events object, but we added one.
        # So there should be all 4 here
        self.assertDictEqual(
            req.power_level_content_override.events,
            {
                EventTypes.PowerLevels: CREATOR_POWER_LEVEL - 1,
                EventTypes.JoinRules: CREATOR_POWER_LEVEL - 1,
                EventTypes.GuestAccess: CREATOR_POWER_LEVEL - 1,
                EventTypes.Message: 10,
            },
        )
        assert req.power_level_content_override.ban == CREATOR_POWER_LEVEL - 1
        assert req.power_level_content_override.invite == CREATOR_POWER_LEVEL - 1
        assert req.power_level_content_override.kick == CREATOR_POWER_LEVEL - 1
        assert req.power_level_content_override.redact == 50
        assert req.power_level_content_override.events_default == 0
        assert req.power_level_content_override.users_default == 0
        assert req.power_level_content_override.state_default == 50

    def test_arbitrary_and_protected_event_type_override_can_error_appropriately(
        self,
    ) -> None:
        """
        Test that using both a protected and unprotected event type together in the
        power level content override does not allow the protected event type to be
        overridden
        """
        room_config = {
            "room_alias_name": "testroom",
            "name": "Test Room",
            "power_level_content_override": {
                "events": {EventTypes.Message: 10, EventTypes.JoinRules: 100}
            },
            "groups": [],
        }

        # Since one of the event types is protected, this should error
        with pytest.raises(ValidationError):
            CreateManagedRoomRequest.model_validate(
                room_config, context={"room_creator": "@room_creator:example.com"}
            )

    def test_with_user_powerlevel_override(self) -> None:
        """
        Test that adding a user override power level operates as expected
        """
        room_config = {
            "room_alias_name": "testroom",
            "name": "Test Room",
            "power_level_content_override": {"users": {"@alice:example.com": 100}},
            "groups": ["testgroup"],
        }

        req = CreateManagedRoomRequest.model_validate(
            room_config, context={"room_creator": "@room_creator:example.com"}
        )
        assert req.power_level_content_override.users_default == 0
        assert "@alice:example.com" in req.power_level_content_override.users
        assert req.power_level_content_override.users["@alice:example.com"] == 100
        # At the time this object is created, the room creator is not present in room
        # v10, this is added at the time of the request being processed. There should be
        # only the one we asked for
        assert len(req.power_level_content_override.users) == 1

    def test_not_adding_context_to_object_raises(self) -> None:
        """
        Test that not including the correct context for model validation causes failure.
        To be clear, this only will matter when the `users` object is present for the
        `power_level_content_override`. If the `users` object is not expressly being
        validated in the model, the context object is not required.
        """
        room_config = {
            "room_alias_name": "testroom",
            "name": "Test Room",
            "power_level_content_override": {"users": {"@alice:example.com": 100}},
            "groups": ["testgroup"],
        }

        # Note that the context is not added here. That data is needed to validate that
        # any users in the `user` override are not the room creator and should not be
        # skipped during power level validation
        with pytest.raises(ValidationError):
            CreateManagedRoomRequest.model_validate(room_config)

        # Also test that the wrong kind of object is not expected. It must be a dict
        # format
        with pytest.raises(ValidationError):
            CreateManagedRoomRequest.model_validate(
                room_config, context=["@room_creator:example.com"]
            )

        # Also test that "room_creator" being None is not allowed
        with pytest.raises(ValidationError):
            CreateManagedRoomRequest.model_validate(
                room_config, context={"room_creator": None}
            )
