"""Channels ITs for MySeaSpectateConsumer — the async-witness WS that pushes
the owner's draw to watching invitees (user-spec 2026-05-29).

Same shape as the voice consumer tests: scope injected directly, in-memory
channel layer, `@tag("channels")` (excluded from the default run).
"""

from channels.db import database_sync_to_async
from channels.testing.websocket import WebsocketCommunicator
from django.contrib.auth.models import AnonymousUser
from django.test import TransactionTestCase, override_settings, tag
from django.utils import timezone

from apps.gameboard.consumers import MySeaSpectateConsumer
from apps.gameboard.models import SeaInvite
from apps.lyric.models import User


TEST_CHANNEL_LAYERS = {
    "default": {"BACKEND": "channels.layers.InMemoryChannelLayer"},
}


@tag("channels")
@override_settings(CHANNEL_LAYERS=TEST_CHANNEL_LAYERS)
class MySeaSpectateConsumerTest(TransactionTestCase):
    def setUp(self):
        self.owner = User.objects.create(email="owner@test.io", username="discoman")
        self.bud = User.objects.create(email="bud@test.io", username="budster")

    def _comm(self, user):
        comm = WebsocketCommunicator(
            MySeaSpectateConsumer.as_asgi(),
            f"/ws/my-sea-spectate/{self.owner.id}/",
        )
        comm.scope["user"] = user
        comm.scope["url_route"] = {"kwargs": {"owner_id": self.owner.id}}
        return comm

    def _invite(self, present):
        return SeaInvite.objects.create(
            owner=self.owner, invitee=self.bud, invitee_email=self.bud.email,
            status=SeaInvite.ACCEPTED, accepted_at=timezone.now(),
            token_deposited_at=timezone.now() if present else None,
        )

    async def test_owner_can_watch(self):
        comm = self._comm(self.owner)
        connected, _ = await comm.connect()
        self.assertTrue(connected)
        await comm.disconnect()

    async def test_unauthenticated_is_rejected(self):
        comm = self._comm(AnonymousUser())
        connected, _ = await comm.connect()
        self.assertFalse(connected)

    async def test_stranger_is_rejected(self):
        comm = self._comm(self.bud)   # no invite
        connected, _ = await comm.connect()
        self.assertFalse(connected)

    async def test_accepted_but_not_present_is_rejected(self):
        await database_sync_to_async(self._invite)(present=False)
        comm = self._comm(self.bud)
        connected, _ = await comm.connect()
        self.assertFalse(connected)

    async def test_present_invitee_receives_the_owners_hand_and_spread(self):
        await database_sync_to_async(self._invite)(present=True)
        comm = self._comm(self.bud)
        connected, _ = await comm.connect()
        self.assertTrue(connected)
        from channels.layers import get_channel_layer
        hand = [{"position": "lay", "card_id": 1,
                 "reversed": False, "polarity": "gravity"}]
        # The spread rides along so a spectator under a stale spread re-syncs its
        # cross layout/captions before filling the hand (asymmetry fix
        # 2026-05-30).
        await get_channel_layer().group_send(
            f"mysea_{self.owner.id}",
            {"type": "sea_draw", "hand": hand, "spread": "escape-velocity"})
        msg = await comm.receive_json_from()
        self.assertEqual(
            msg,
            {"type": "sea_draw", "hand": hand, "spread": "escape-velocity"})
        await comm.disconnect()

    async def test_present_invitee_receives_spread_only_event(self):
        # The owner closing her SPREAD modal pushes a hand-less `sea_spread` so
        # the spectator re-lays-out before any card lands (2026-05-30).
        await database_sync_to_async(self._invite)(present=True)
        comm = self._comm(self.bud)
        connected, _ = await comm.connect()
        self.assertTrue(connected)
        from channels.layers import get_channel_layer
        await get_channel_layer().group_send(
            f"mysea_{self.owner.id}",
            {"type": "sea_spread", "spread": "mind-body-spirit"})
        msg = await comm.receive_json_from()
        self.assertEqual(
            msg, {"type": "sea_spread", "spread": "mind-body-spirit"})
        await comm.disconnect()

    async def test_present_invitee_receives_seat_updates(self):
        await database_sync_to_async(self._invite)(present=True)
        comm = self._comm(self.bud)
        connected, _ = await comm.connect()
        self.assertTrue(connected)
        from channels.layers import get_channel_layer
        seats = [{"n": 1, "label": "1C", "present": True, "token": "owner-x"}]
        await get_channel_layer().group_send(
            f"mysea_{self.owner.id}", {"type": "sea_seats", "seats": seats})
        msg = await comm.receive_json_from()
        self.assertEqual(msg, {"type": "sea_seats", "seats": seats})
        await comm.disconnect()
