"""
Tests for mizan.channels module.
"""

import json
from unittest.mock import AsyncMock, MagicMock, patch
from django.test import TestCase
from django.contrib.auth import get_user_model
from pydantic import BaseModel

from mizan.channels import (
    ReactChannel,
    register,
    get_channel,
    get_registered_channels,
    get_channels_schema,
    _registry,
)


User = get_user_model()


# =============================================================================
# Test Fixtures
# =============================================================================


class MockUser:
    """Mock user for testing."""

    def __init__(self, is_authenticated=True, email="test@example.com"):
        self.is_authenticated = is_authenticated
        self.email = email


class MockAnonymousUser:
    """Mock anonymous user."""

    is_authenticated = False
    email = ""


# =============================================================================
# ReactChannel Base Class Tests
# =============================================================================


class ReactChannelBaseTests(TestCase):
    """Tests for ReactChannel base class."""

    def test_react_channel_default_class_vars(self):
        """ReactChannel should have None defaults for nested classes."""
        self.assertIsNone(ReactChannel.Params)
        self.assertIsNone(ReactChannel.ReactMessage)
        self.assertIsNone(ReactChannel.DjangoMessage)

    def test_react_channel_requires_authorize_override(self):
        """ReactChannel subclass must override authorize()."""

        class IncompleteChannel(ReactChannel):
            pass

        channel = IncompleteChannel()
        channel.user = MockUser()

        with self.assertRaises(NotImplementedError) as ctx:
            channel.authorize()

        self.assertIn("must implement authorize()", str(ctx.exception))

    def test_react_channel_requires_group_override(self):
        """ReactChannel subclass must override group()."""

        class IncompleteChannel(ReactChannel):
            def authorize(self, params=None):
                return True

        channel = IncompleteChannel()
        channel.user = MockUser()

        with self.assertRaises(NotImplementedError) as ctx:
            channel.group()

        self.assertIn("must implement group()", str(ctx.exception))

    def test_react_channel_receive_default(self):
        """ReactChannel.receive() should return None by default."""

        class BasicChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test-group"

        channel = BasicChannel()
        result = channel.receive(None, MagicMock())

        self.assertIsNone(result)

    def test_react_channel_init_creates_empty_groups(self):
        """ReactChannel.__init__() should create empty _groups set."""

        class TestChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        channel = TestChannel()

        self.assertIsInstance(channel._groups, set)
        self.assertEqual(len(channel._groups), 0)


# =============================================================================
# Channel with Typed Messages Tests
# =============================================================================


class TypedMessagesTests(TestCase):
    """Tests for channels with Pydantic message types."""

    def test_channel_with_params(self):
        """Channel should accept Params Pydantic model."""

        class ParamsChannel(ReactChannel):
            class Params(BaseModel):
                room: str
                limit: int = 10

            def authorize(self, params):
                return True

            def group(self, params):
                return f"room_{params.room}"

        self.assertIsNotNone(ParamsChannel.Params)

        # Test params model
        params = ParamsChannel.Params(room="general")
        self.assertEqual(params.room, "general")
        self.assertEqual(params.limit, 10)

    def test_channel_with_react_message(self):
        """Channel should accept ReactMessage Pydantic model."""

        class MessageChannel(ReactChannel):
            class ReactMessage(BaseModel):
                text: str
                timestamp: int

            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "messages"

        self.assertIsNotNone(MessageChannel.ReactMessage)

        # Test message model
        msg = MessageChannel.ReactMessage(text="Hello", timestamp=12345)
        self.assertEqual(msg.text, "Hello")
        self.assertEqual(msg.timestamp, 12345)

    def test_channel_with_django_message(self):
        """Channel should accept DjangoMessage Pydantic model."""

        class BroadcastChannel(ReactChannel):
            class DjangoMessage(BaseModel):
                user: str
                text: str
                created_at: str

            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "broadcast"

        self.assertIsNotNone(BroadcastChannel.DjangoMessage)

        # Test message model
        msg = BroadcastChannel.DjangoMessage(
            user="john", text="Hello world", created_at="2024-01-15T10:00:00Z"
        )
        self.assertEqual(msg.user, "john")
        self.assertEqual(msg.text, "Hello world")

    def test_channel_receive_with_typed_messages(self):
        """Channel.receive() should work with typed messages."""

        class ChatChannel(ReactChannel):
            class Params(BaseModel):
                room: str

            class ReactMessage(BaseModel):
                text: str

            class DjangoMessage(BaseModel):
                user: str
                text: str

            def authorize(self, params):
                return True

            def group(self, params):
                return f"chat_{params.room}"

            def receive(self, params, msg):
                return self.DjangoMessage(user=self.user.email, text=msg.text)

        channel = ChatChannel()
        channel.user = MockUser(email="test@example.com")

        params = ChatChannel.Params(room="general")
        incoming = ChatChannel.ReactMessage(text="Hello!")

        result = channel.receive(params, incoming)

        self.assertIsInstance(result, ChatChannel.DjangoMessage)
        self.assertEqual(result.user, "test@example.com")
        self.assertEqual(result.text, "Hello!")


# =============================================================================
# Registration Tests
# =============================================================================


class RegistrationTests(TestCase):
    """Tests for channel registration."""

    def setUp(self):
        self._original_registry = dict(_registry)

    def tearDown(self):
        _registry.clear()
        _registry.update(self._original_registry)

    def test_register_adds_to_registry(self):
        """register() should add channel to registry."""

        class TestChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        register(TestChannel, "test-channel")

        self.assertIn("test-channel", _registry)
        self.assertEqual(_registry["test-channel"], TestChannel)

    def test_register_duplicate_raises(self):
        """register() should raise on duplicate name."""

        class Channel1(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        class Channel2(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        register(Channel1, "duplicate")

        with self.assertRaises(ValueError) as ctx:
            register(Channel2, "duplicate")

        self.assertIn("already registered", str(ctx.exception))

    def test_register_validates_authorize(self):
        """register() should validate that authorize method exists."""

        class NoAuthorizeChannel(ReactChannel):
            pass

        # Should still pass because ReactChannel has authorize
        # (just raises NotImplementedError when called)
        register(NoAuthorizeChannel, "no-authorize-test")
        self.assertIn("no-authorize-test", _registry)

    def test_get_channel_returns_registered(self):
        """get_channel() should return registered channel."""

        class MyChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "my"

        register(MyChannel, "my-channel")

        result = get_channel("my-channel")

        self.assertEqual(result, MyChannel)

    def test_get_channel_returns_none_for_unknown(self):
        """get_channel() should return None for unknown name."""
        result = get_channel("nonexistent-channel")

        self.assertIsNone(result)

    def test_get_registered_channels_returns_copy(self):
        """get_registered_channels() should return a copy of registry."""

        class TestChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        register(TestChannel, "copy-test")

        result = get_registered_channels()

        # Modifying result shouldn't affect original
        result["modified"] = "test"

        self.assertIn("copy-test", _registry)
        self.assertNotIn("modified", _registry)


# =============================================================================
# Schema Export Tests
# =============================================================================


class SchemaExportTests(TestCase):
    """Tests for channel schema export."""

    def setUp(self):
        self._original_registry = dict(_registry)

    def tearDown(self):
        _registry.clear()
        _registry.update(self._original_registry)

    def test_get_channels_schema_empty(self):
        """get_channels_schema() should return empty channels for empty registry."""
        _registry.clear()

        schema = get_channels_schema()

        self.assertIn("channels", schema)
        self.assertEqual(schema["channels"], {})

    def test_get_channels_schema_with_basic_channel(self):
        """get_channels_schema() should include basic channel info."""

        class BasicChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "basic"

        register(BasicChannel, "basic")

        schema = get_channels_schema()

        self.assertIn("basic", schema["channels"])
        channel_schema = schema["channels"]["basic"]

        self.assertEqual(channel_schema["name"], "basic")
        self.assertIsNone(channel_schema["params"])
        self.assertIsNone(channel_schema["reactMessage"])
        self.assertIsNone(channel_schema["djangoMessage"])

    def test_get_channels_schema_with_params(self):
        """get_channels_schema() should include params schema."""

        class ParamsChannel(ReactChannel):
            class Params(BaseModel):
                room: str
                limit: int = 50

            def authorize(self, params):
                return True

            def group(self, params):
                return f"room_{params.room}"

        register(ParamsChannel, "params-channel")

        schema = get_channels_schema()

        channel_schema = schema["channels"]["params-channel"]

        self.assertIsNotNone(channel_schema["params"])
        self.assertIn("properties", channel_schema["params"])
        self.assertIn("room", channel_schema["params"]["properties"])
        self.assertIn("limit", channel_schema["params"]["properties"])

    def test_get_channels_schema_with_messages(self):
        """get_channels_schema() should include message schemas."""

        class FullChannel(ReactChannel):
            class Params(BaseModel):
                channel_id: int

            class ReactMessage(BaseModel):
                text: str

            class DjangoMessage(BaseModel):
                user: str
                text: str
                timestamp: str

            def authorize(self, params):
                return True

            def group(self, params):
                return f"channel_{params.channel_id}"

        register(FullChannel, "full-channel")

        schema = get_channels_schema()

        channel_schema = schema["channels"]["full-channel"]

        # Check params
        self.assertIsNotNone(channel_schema["params"])
        self.assertIn("channel_id", channel_schema["params"]["properties"])

        # Check ReactMessage
        self.assertIsNotNone(channel_schema["reactMessage"])
        self.assertIn("text", channel_schema["reactMessage"]["properties"])

        # Check DjangoMessage
        self.assertIsNotNone(channel_schema["djangoMessage"])
        self.assertIn("user", channel_schema["djangoMessage"]["properties"])
        self.assertIn("text", channel_schema["djangoMessage"]["properties"])
        self.assertIn("timestamp", channel_schema["djangoMessage"]["properties"])

    def test_get_channels_schema_multiple_channels(self):
        """get_channels_schema() should include all registered channels."""

        class Channel1(ReactChannel):
            class Params(BaseModel):
                id: int

            def authorize(self, params):
                return True

            def group(self, params):
                return f"c1_{params.id}"

        class Channel2(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "c2"

        register(Channel1, "channel-one")
        register(Channel2, "channel-two")

        schema = get_channels_schema()

        self.assertIn("channel-one", schema["channels"])
        self.assertIn("channel-two", schema["channels"])

        # Channel 1 has params
        self.assertIsNotNone(schema["channels"]["channel-one"]["params"])

        # Channel 2 has no params
        self.assertIsNone(schema["channels"]["channel-two"]["params"])


# =============================================================================
# Authorization Tests
# =============================================================================


class AuthorizationTests(TestCase):
    """Tests for channel authorization."""

    def test_authorize_with_authenticated_user(self):
        """authorize() should work with authenticated users."""

        class AuthChannel(ReactChannel):
            def authorize(self, params=None):
                return self.user.is_authenticated

            def group(self, params=None):
                return "auth"

        channel = AuthChannel()
        channel.user = MockUser(is_authenticated=True)

        self.assertTrue(channel.authorize())

    def test_authorize_with_anonymous_user(self):
        """authorize() should work with anonymous users."""

        class AuthChannel(ReactChannel):
            def authorize(self, params=None):
                return self.user.is_authenticated

            def group(self, params=None):
                return "auth"

        channel = AuthChannel()
        channel.user = MockAnonymousUser()

        self.assertFalse(channel.authorize())

    def test_authorize_with_params(self):
        """authorize() should have access to params."""

        class RoomChannel(ReactChannel):
            class Params(BaseModel):
                room: str

            def authorize(self, params):
                # Only allow 'public' room for all users
                return params.room == "public" or self.user.is_authenticated

            def group(self, params):
                return f"room_{params.room}"

        channel = RoomChannel()
        channel.user = MockAnonymousUser()

        public_params = RoomChannel.Params(room="public")
        private_params = RoomChannel.Params(room="private")

        self.assertTrue(channel.authorize(public_params))
        self.assertFalse(channel.authorize(private_params))


# =============================================================================
# Group Tests
# =============================================================================


class GroupTests(TestCase):
    """Tests for channel group management."""

    def test_group_returns_string(self):
        """group() should return a string group name."""

        class TestChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test-group"

        channel = TestChannel()

        self.assertEqual(channel.group(), "test-group")

    def test_group_with_params(self):
        """group() should use params for dynamic group names."""

        class RoomChannel(ReactChannel):
            class Params(BaseModel):
                room_id: int

            def authorize(self, params):
                return True

            def group(self, params):
                return f"room_{params.room_id}"

        channel = RoomChannel()

        params1 = RoomChannel.Params(room_id=1)
        params2 = RoomChannel.Params(room_id=42)

        self.assertEqual(channel.group(params1), "room_1")
        self.assertEqual(channel.group(params2), "room_42")


# =============================================================================
# Async Methods Tests
# =============================================================================


class AsyncMethodsTests(TestCase):
    """Tests for async internal methods."""

    def test_join_group_adds_to_groups_set(self):
        """_join_group() should add group to _groups set."""
        import asyncio

        class TestChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        channel = TestChannel()
        channel._channel_layer = AsyncMock()
        channel._channel_name = "test-channel-name"

        async def test():
            await channel._join_group("my-group")
            return channel._groups

        groups = asyncio.get_event_loop().run_until_complete(test())

        self.assertIn("my-group", groups)
        channel._channel_layer.group_add.assert_called_once_with(
            "my-group", "test-channel-name"
        )

    def test_leave_group_removes_from_groups_set(self):
        """_leave_group() should remove group from _groups set."""
        import asyncio

        class TestChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        channel = TestChannel()
        channel._channel_layer = AsyncMock()
        channel._channel_name = "test-channel-name"
        channel._groups.add("my-group")

        async def test():
            await channel._leave_group("my-group")
            return channel._groups

        groups = asyncio.get_event_loop().run_until_complete(test())

        self.assertNotIn("my-group", groups)
        channel._channel_layer.group_discard.assert_called_once_with(
            "my-group", "test-channel-name"
        )

    def test_leave_group_ignores_unknown_group(self):
        """_leave_group() should ignore groups not in _groups."""
        import asyncio

        class TestChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        channel = TestChannel()
        channel._channel_layer = AsyncMock()
        channel._channel_name = "test-channel-name"

        async def test():
            await channel._leave_group("unknown-group")
            return channel._groups

        groups = asyncio.get_event_loop().run_until_complete(test())

        # Should not have called group_discard
        channel._channel_layer.group_discard.assert_not_called()

    def test_leave_all_groups(self):
        """_leave_all_groups() should leave all joined groups."""
        import asyncio

        class TestChannel(ReactChannel):
            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        channel = TestChannel()
        channel._channel_layer = AsyncMock()
        channel._channel_name = "test-channel-name"
        channel._groups = {"group1", "group2", "group3"}

        async def test():
            await channel._leave_all_groups()
            return channel._groups

        groups = asyncio.get_event_loop().run_until_complete(test())

        self.assertEqual(len(groups), 0)
        self.assertEqual(channel._channel_layer.group_discard.call_count, 3)

    def test_broadcast_sends_to_group(self):
        """_broadcast() should send message to channel layer."""
        import asyncio

        class TestChannel(ReactChannel):
            class DjangoMessage(BaseModel):
                text: str

            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        channel = TestChannel()
        channel._channel_layer = AsyncMock()

        message = TestChannel.DjangoMessage(text="Hello")

        async def test():
            await channel._broadcast("my-group", message)

        asyncio.get_event_loop().run_until_complete(test())

        channel._channel_layer.group_send.assert_called_once()
        call_args = channel._channel_layer.group_send.call_args

        self.assertEqual(call_args[0][0], "my-group")
        self.assertEqual(call_args[0][1]["type"], "channel.message")
        self.assertEqual(call_args[0][1]["data"]["text"], "Hello")


# =============================================================================
# Server Push Tests
# =============================================================================


class ServerPushTests(TestCase):
    """Tests for server push functionality."""

    def setUp(self):
        self._original_registry = dict(_registry)

    def tearDown(self):
        _registry.clear()
        _registry.update(self._original_registry)

    def test_push_without_params(self):
        """push() should work for channels without params."""
        import asyncio

        class NotificationChannel(ReactChannel):
            class DjangoMessage(BaseModel):
                title: str
                body: str

            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "notifications"

        with patch("channels.layers.get_channel_layer") as mock_get_layer:
            mock_layer = AsyncMock()
            mock_get_layer.return_value = mock_layer

            message = NotificationChannel.DjangoMessage(
                title="Alert", body="Something happened"
            )

            async def test():
                await NotificationChannel.push(message=message)

            asyncio.get_event_loop().run_until_complete(test())

            mock_layer.group_send.assert_called_once()
            call_args = mock_layer.group_send.call_args

            self.assertEqual(call_args[0][0], "notifications")
            self.assertEqual(call_args[0][1]["data"]["title"], "Alert")

    def test_push_with_params(self):
        """push() should work for channels with params."""
        import asyncio

        class RoomChannel(ReactChannel):
            class Params(BaseModel):
                room: str

            class DjangoMessage(BaseModel):
                text: str

            def authorize(self, params):
                return True

            def group(self, params):
                return f"room_{params.room}"

        with patch("channels.layers.get_channel_layer") as mock_get_layer:
            mock_layer = AsyncMock()
            mock_get_layer.return_value = mock_layer

            message = RoomChannel.DjangoMessage(text="Hello room!")

            async def test():
                await RoomChannel.push(room="general", message=message)

            asyncio.get_event_loop().run_until_complete(test())

            mock_layer.group_send.assert_called_once()
            call_args = mock_layer.group_send.call_args

            self.assertEqual(call_args[0][0], "room_general")
            self.assertEqual(call_args[0][1]["data"]["text"], "Hello room!")

    def test_push_without_channel_layer_warns(self):
        """push() should warn when no channel layer is configured."""
        import asyncio
        import logging

        class TestChannel(ReactChannel):
            class DjangoMessage(BaseModel):
                text: str

            def authorize(self, params=None):
                return True

            def group(self, params=None):
                return "test"

        with patch("channels.layers.get_channel_layer") as mock_get_layer:
            mock_get_layer.return_value = None

            message = TestChannel.DjangoMessage(text="test")

            with self.assertLogs("mizan.channels", level="WARNING") as cm:

                async def test():
                    await TestChannel.push(message=message)

                asyncio.get_event_loop().run_until_complete(test())

            self.assertTrue(
                any("No channel layer configured" in msg for msg in cm.output)
            )


# =============================================================================
# Management Command Tests
# =============================================================================


class ManagementCommandTests(TestCase):
    """Tests for the export_channels_schema management command."""

    def setUp(self):
        self._original_registry = dict(_registry)

    def tearDown(self):
        _registry.clear()
        _registry.update(self._original_registry)

    def test_export_command_outputs_openapi_json(self):
        """export_channels_schema should output valid OpenAPI JSON."""
        from io import StringIO
        from django.core.management import call_command

        out = StringIO()
        call_command("export_channels_schema", stdout=out)

        output = out.getvalue()

        # Should be valid JSON with OpenAPI structure
        schema = json.loads(output)

        self.assertIn("openapi", schema)
        self.assertIn("x-mizan-channels", schema)

    def test_export_command_includes_registered_channels(self):
        """export_channels_schema should include registered channels."""
        from io import StringIO
        from django.core.management import call_command

        class TestChannel(ReactChannel):
            class Params(BaseModel):
                id: int

            def authorize(self, params):
                return True

            def group(self, params):
                return f"test_{params.id}"

        register(TestChannel, "export-test")

        out = StringIO()
        call_command("export_channels_schema", stdout=out)

        output = out.getvalue()
        schema = json.loads(output)

        # Check that channel is in x-mizan-channels metadata
        channel_names = [c["name"] for c in schema["x-mizan-channels"]]
        self.assertIn("export-test", channel_names)

    def test_export_command_respects_indent(self):
        """export_channels_schema should respect --indent option."""
        from io import StringIO
        from django.core.management import call_command

        # With indent
        out_indent = StringIO()
        call_command("export_channels_schema", indent=2, stdout=out_indent)

        # Without indent (compact)
        out_compact = StringIO()
        call_command("export_channels_schema", indent=0, stdout=out_compact)

        # Indented should be longer (has whitespace)
        self.assertGreater(len(out_indent.getvalue()), len(out_compact.getvalue()))


# =============================================================================
# WebSocket RPC Tests
# =============================================================================


class WebSocketRPCTests(TestCase):
    """Tests for WebSocket RPC functionality."""

    def setUp(self):
        # Clear mizan registry
        from mizan_core.registry import clear_registry

        clear_registry()

        # Register test functions
        from mizan.client import client
        from mizan_core.registry import register
        from pydantic import BaseModel

        class EchoOutput(BaseModel):
            echo: str

        class AddOutput(BaseModel):
            result: int

        @client(websocket=True)
        def rpc_echo(request, message: str) -> EchoOutput:
            return EchoOutput(echo=f"Echo: {message}")

        register(rpc_echo, "rpc_echo")

        @client(websocket=True)
        def rpc_add(request, a: int, b: int) -> AddOutput:
            return AddOutput(result=a + b)

        register(rpc_add, "rpc_add")

        @client(websocket=True)
        def rpc_auth_required(request) -> EchoOutput:
            if not request.user.is_authenticated:
                raise PermissionError("Authentication required")
            return EchoOutput(echo=f"Hello, {request.user.email}")

        register(rpc_auth_required, "rpc_auth_required")

    def tearDown(self):
        from mizan_core.registry import clear_registry

        clear_registry()

    def test_handle_rpc_success(self):
        """_handle_rpc should execute function and return result."""
        import asyncio
        from mizan.channels.connection import DjangoReactConsumer

        consumer = DjangoReactConsumer()
        consumer.scope = {
            "user": MockUser(is_authenticated=True, email="test@example.com"),
        }
        consumer.sent_messages = []

        async def mock_send_json(data):
            consumer.sent_messages.append(data)

        consumer.send_json = mock_send_json

        async def test():
            await consumer._handle_rpc(
                {
                    "id": "test-123",
                    "fn": "rpc_echo",
                    "args": {"message": "Hello"},
                }
            )

        asyncio.get_event_loop().run_until_complete(test())

        self.assertEqual(len(consumer.sent_messages), 1)
        response = consumer.sent_messages[0]

        self.assertEqual(response["id"], "test-123")
        self.assertTrue(response["ok"])
        # data is the {result, invalidate, merge} envelope, as on the HTTP RPC path
        self.assertEqual(response["data"]["result"]["echo"], "Echo: Hello")

    def test_handle_rpc_with_multiple_args(self):
        """_handle_rpc should handle functions with multiple arguments."""
        import asyncio
        from mizan.channels.connection import DjangoReactConsumer

        consumer = DjangoReactConsumer()
        consumer.scope = {"user": MockUser()}
        consumer.sent_messages = []

        async def mock_send_json(data):
            consumer.sent_messages.append(data)

        consumer.send_json = mock_send_json

        async def test():
            await consumer._handle_rpc(
                {
                    "id": "add-123",
                    "fn": "rpc_add",
                    "args": {"a": 5, "b": 3},
                }
            )

        asyncio.get_event_loop().run_until_complete(test())

        response = consumer.sent_messages[0]
        self.assertTrue(response["ok"])
        self.assertEqual(response["data"]["result"]["result"], 8)

    def test_handle_rpc_function_not_found(self):
        """_handle_rpc should return error for unknown function."""
        import asyncio
        from mizan.channels.connection import DjangoReactConsumer

        consumer = DjangoReactConsumer()
        consumer.scope = {"user": MockUser()}
        consumer.sent_messages = []

        async def mock_send_json(data):
            consumer.sent_messages.append(data)

        consumer.send_json = mock_send_json

        async def test():
            await consumer._handle_rpc(
                {
                    "id": "test-456",
                    "fn": "nonexistent_function",
                    "args": {},
                }
            )

        asyncio.get_event_loop().run_until_complete(test())

        response = consumer.sent_messages[0]
        self.assertEqual(response["id"], "test-456")
        self.assertFalse(response["ok"])
        self.assertEqual(response["error"]["code"], "NOT_FOUND")

    def test_handle_rpc_validation_error(self):
        """_handle_rpc should return validation error for invalid input."""
        import asyncio
        from mizan.channels.connection import DjangoReactConsumer

        consumer = DjangoReactConsumer()
        consumer.scope = {"user": MockUser()}
        consumer.sent_messages = []

        async def mock_send_json(data):
            consumer.sent_messages.append(data)

        consumer.send_json = mock_send_json

        async def test():
            await consumer._handle_rpc(
                {
                    "id": "test-789",
                    "fn": "rpc_echo",
                    "args": {},  # Missing required 'message' field
                }
            )

        asyncio.get_event_loop().run_until_complete(test())

        response = consumer.sent_messages[0]
        self.assertEqual(response["id"], "test-789")
        self.assertFalse(response["ok"])
        self.assertEqual(response["error"]["code"], "VALIDATION_ERROR")

    def test_handle_rpc_missing_id(self):
        """_handle_rpc should return error when id is missing."""
        import asyncio
        from mizan.channels.connection import DjangoReactConsumer

        consumer = DjangoReactConsumer()
        consumer.scope = {"user": MockUser()}
        consumer.sent_messages = []

        async def mock_send_json(data):
            consumer.sent_messages.append(data)

        consumer.send_json = mock_send_json

        async def test():
            await consumer._handle_rpc(
                {
                    "fn": "rpc_echo",
                    "args": {"message": "test"},
                    # Missing 'id'
                }
            )

        asyncio.get_event_loop().run_until_complete(test())

        response = consumer.sent_messages[0]
        self.assertIn("error", response)
        self.assertIn("missing", response["error"].lower())

    def test_handle_rpc_missing_fn(self):
        """_handle_rpc should return error when fn is missing."""
        import asyncio
        from mizan.channels.connection import DjangoReactConsumer

        consumer = DjangoReactConsumer()
        consumer.scope = {"user": MockUser()}
        consumer.sent_messages = []

        async def mock_send_json(data):
            consumer.sent_messages.append(data)

        consumer.send_json = mock_send_json

        async def test():
            await consumer._handle_rpc(
                {
                    "id": "test-abc",
                    "args": {"message": "test"},
                    # Missing 'fn'
                }
            )

        asyncio.get_event_loop().run_until_complete(test())

        response = consumer.sent_messages[0]
        self.assertEqual(response["id"], "test-abc")
        self.assertFalse(response["ok"])
        self.assertEqual(response["error"]["code"], "BAD_REQUEST")

    def test_handle_rpc_with_unauthenticated_user(self):
        """_handle_rpc should handle permission errors correctly."""
        import asyncio
        from mizan.channels.connection import DjangoReactConsumer

        consumer = DjangoReactConsumer()
        consumer.scope = {"user": MockAnonymousUser()}
        consumer.sent_messages = []

        async def mock_send_json(data):
            consumer.sent_messages.append(data)

        consumer.send_json = mock_send_json

        async def test():
            await consumer._handle_rpc(
                {
                    "id": "auth-test",
                    "fn": "rpc_auth_required",
                    "args": {},
                }
            )

        asyncio.get_event_loop().run_until_complete(test())

        response = consumer.sent_messages[0]
        self.assertEqual(response["id"], "auth-test")
        self.assertFalse(response["ok"])
        self.assertEqual(response["error"]["code"], "FORBIDDEN")

    def test_websocket_request_adapter(self):
        """WebSocketRequest should provide correct user and session."""
        from mizan.channels.connection import WebSocketRequest

        mock_user = MockUser(email="ws@example.com")
        scope = {
            "user": mock_user,
            "session": {"key": "value"},
            "headers": [(b"x-custom-header", b"test-value")],
        }

        request = WebSocketRequest(scope)

        self.assertEqual(request.user, mock_user)
        self.assertEqual(request.session["key"], "value")
        self.assertIn("HTTP_X_CUSTOM_HEADER", request.META)
