import os
import unittest

from django.conf import settings as django_settings
from django.test import tag
from selenium import webdriver
from selenium.webdriver.common.by import By

from .base import FunctionalTest, ChannelsFunctionalTest
from .management.commands.create_session import create_pre_authenticated_session
from .room_page import _equip_earthman_deck, _fill_room_via_orm
from apps.applets.models import Applet
from apps.epic.models import Room, GateSlot, TableSeat
from apps.lyric.models import User


class RoleSelectTest(FunctionalTest):

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
        )
        Applet.objects.get_or_create(
            slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
        )

    # ------------------------------------------------------------------ #
    # Test 1 — PICK ROLES dismisses gatekeeper and reveals the table      #
    # ------------------------------------------------------------------ #

    def test_pick_roles_dismisses_gatekeeper_and_reveals_table(self):
        # 1. Founder logs in, creates room via UI, fills remaining slots via ORM
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self.live_server_url + "/gameboard/")
        self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_new_game_name")
        ).send_keys("Dragon's Den")
        self.browser.find_element(By.ID, "id_create_game_btn").click()
        self.wait_for(
            lambda: self.assertIn("/gate/", self.browser.current_url)
        )
        room_url = self.browser.current_url
        room = Room.objects.get(name="Dragon's Den")

        # Fill founder's slot via UI (slot 1)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
        ).click()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
        ).click()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
        )

        # Fill slots 2–6 via ORM
        emails = ["amigo@test.io", "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io"]
        for i, email in enumerate(emails, start=2):
            gamer, _ = User.objects.get_or_create(email=email)
            slot = room.gate_slots.get(slot_number=i)
            slot.gamer = gamer
            slot.status = GateSlot.FILLED
            slot.save()
        room.gate_status = Room.OPEN
        room.save()

        # 2. Browser sees the PICK ROLES button (gate is now open)
        self.browser.refresh()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".launch-game-btn")
        ).click()

        # 3. Gatekeeper overlay is gone
        self.wait_for(
            lambda: self.assertEqual(
                len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-overlay")), 0
            )
        )

        # 4. Table is visible and prominent
        table = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_game_table")
        )
        self.assertTrue(table.is_displayed())

        # 5. Card stack is present in the table centre
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
        )

        # 6. Six seat portraits are visible around the table
        seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat")
        self.assertEqual(len(seats), 6)

    # ------------------------------------------------------------------ #
    # Test 2 — Card stack signals eligibility to each gamer               #
    # ------------------------------------------------------------------ #

    def test_card_stack_glows_for_first_gamer_only(self):
        # Two browsers: founder (slot 1, eligible) and friend (slot 2, not yet)
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        friend, _ = User.objects.get_or_create(email="friend@test.io")
        room = Room.objects.create(name="Signal Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "friend@test.io",
            "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

        # Founder's browser
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)
        stack = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
        )
        self.assertIn("eligible", stack.get_attribute("data-state"))
        self.assertEqual(
            len(self.browser.find_elements(By.CSS_SELECTOR, ".card-stack .fa-ban")), 0
        )

        # Friend's browser
        options2 = webdriver.FirefoxOptions()
        if os.environ.get("HEADLESS"):
            options2.add_argument("--headless")
        self.browser2 = webdriver.Firefox(options=options2)
        try:
            self.browser2.get(self.live_server_url + "/404_no_such_url/")
            from django.conf import settings
            session_key = __import__(
                "functional_tests.management.commands.create_session",
                fromlist=["create_pre_authenticated_session"]
            ).create_pre_authenticated_session("friend@test.io")
            self.browser2.add_cookie(dict(
                name=settings.SESSION_COOKIE_NAME, value=session_key, path="/"
            ))
            self.browser2.get(room_url)
            stack2 = self.wait_for(
                lambda: self.browser2.find_element(By.CSS_SELECTOR, ".card-stack")
            )
            self.assertIn("ineligible", stack2.get_attribute("data-state"))
            self.wait_for(
                lambda: self.browser2.find_element(
                    By.CSS_SELECTOR, ".card-stack .fa-ban"
                )
            )
        finally:
            self.browser2.quit()

    # ------------------------------------------------------------------ #
    # Test 3 — Active gamer fans cards, inspects, selects a role          #
    # ------------------------------------------------------------------ #

    def test_active_gamer_fans_cards_and_selects_role(self):
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        _equip_earthman_deck(founder)
        room = Room.objects.create(name="Fan Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "amigo@test.io", "bud@test.io",
            "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)

        # 1. Click the card stack
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
        ).click()

        # 2. Role Select modal opens with 6 cards
        self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_role_select")
        )
        cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_role_select .card")
        self.assertEqual(len(cards), 6)

        # 3. Blur backdrop is present
        self.browser.find_element(By.CSS_SELECTOR, ".role-select-backdrop")

        # 4. Hover over first card — it flips to reveal front
        from selenium.webdriver.common.action_chains import ActionChains
        ActionChains(self.browser).move_to_element(cards[0]).perform()
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, "#id_role_select .card.flipped"
            )
        )

        # 5. Click first card to select it
        cards[0].click()
        self.confirm_guard()

        # 6. Modal closes
        self.wait_for(
            lambda: self.assertEqual(
                len(self.browser.find_elements(By.ID, "id_role_select")), 0
            )
        )

        # 7. Card stack returns to table centre
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
        )

    # ------------------------------------------------------------------ #
    # Test 3b — Chosen role absent from next gamer's fan                 #
    # ------------------------------------------------------------------ #

    def test_chosen_role_absent_from_next_gamer_fan(self):
        from apps.epic.models import TableSeat
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        friend, _ = User.objects.get_or_create(email="friend@test.io")
        _equip_earthman_deck(friend)  # friend is the active gamer (slot 2)
        room = Room.objects.create(name="Pool Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "friend@test.io",
            "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()

        # Simulate pick_roles: create a TableSeat per filled slot
        for slot in room.gate_slots.order_by("slot_number"):
            TableSeat.objects.create(
                room=room, gamer=slot.gamer, slot_number=slot.slot_number,
            )

        # Slot 1 (founder) has already chosen PC
        TableSeat.objects.filter(room=room, slot_number=1).update(role="PC")

        # Slot 2 (friend) is now the active gamer
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
        self.create_pre_authenticated_session("friend@test.io")
        self.browser.get(room_url)

        # Card stack is eligible for slot 2
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
            )
        ).click()

        # Fan opens — only 5 cards (PC is taken)
        self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_role_select")
        )
        cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_role_select .card")
        self.assertEqual(len(cards), 5)

        # Specifically, no PC card in the fan
        self.assertEqual(
            len(self.browser.find_elements(
                By.CSS_SELECTOR, "#id_role_select .card[data-role='PC']"
            )),
            0,
        )

    # ------------------------------------------------------------------ #
    # Test 3c — Card stack stays eligible after re-entering mid-session  #
    # ------------------------------------------------------------------ #

    def test_card_stack_remains_eligible_after_re_entering_mid_selection(self):
        """A gamer holding multiple slots should still see an eligible card
        stack when they re-enter the room after having already chosen a role
        for their earlier slot."""
        from apps.epic.models import TableSeat
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        _equip_earthman_deck(founder)  # active slot is slot 2 (also founder)
        room = Room.objects.create(name="Re-entry Test", owner=founder)
        # Founder holds slots 1 and 2; others fill the rest
        _fill_room_via_orm(room, [
            "founder@test.io", "founder@test.io",
            "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()

        for slot in room.gate_slots.order_by("slot_number"):
            TableSeat.objects.create(
                room=room, gamer=slot.gamer, slot_number=slot.slot_number,
            )
        # Founder's first slot has already chosen PC
        TableSeat.objects.filter(room=room, slot_number=1).update(role="PC")

        # Founder re-enters the room (simulating a page reload / re-navigation)
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)

        # Card stack must be eligible — slot 2 (also founder's) is the active seat
        stack = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
        )
        self.assertEqual(stack.get_attribute("data-state"), "eligible")

        # Fan shows 5 cards — PC already taken
        stack.click()
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
        cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_role_select .card")
        self.assertEqual(len(cards), 5)

    # ------------------------------------------------------------------ #
    # Test 4 — Click-away dismisses fan without selecting                 #
    # ------------------------------------------------------------------ #

    def test_click_away_dismisses_card_fan_without_selecting(self):
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        _equip_earthman_deck(founder)
        room = Room.objects.create(name="Dismiss Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "amigo@test.io", "bud@test.io",
            "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)

        # Open the fan
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".card-stack")
        ).click()
        self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_role_select")
        )

        # Click the backdrop (outside the fan)
        self.browser.find_element(By.CSS_SELECTOR, ".role-select-backdrop").click()

        # Modal closes; stack still present
        self.wait_for(
            lambda: self.assertEqual(
                len(self.browser.find_elements(By.ID, "id_role_select")), 0
            )
        )
        self.browser.find_element(By.CSS_SELECTOR, ".card-stack")


    # ------------------------------------------------------------------ #
    # Test 4b — Stack locks out immediately after selection (no WS)      #
    # ------------------------------------------------------------------ #

    def test_card_stack_ineligible_immediately_after_selection(self):
        """After clicking a role card the stack must flip to
        data-state='ineligible' straight away — before any WS turn_changed
        event could arrive.  This test runs without a Channels server so
        no WS event will fire; the fix must be entirely client-side."""
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        _equip_earthman_deck(founder)
        room = Room.objects.create(name="Lockout Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "amigo@test.io", "bud@test.io",
            "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()
        for slot in room.gate_slots.order_by("slot_number"):
            TableSeat.objects.create(
                room=room, gamer=slot.gamer, slot_number=slot.slot_number,
            )
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)

        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
            )
        ).click()
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
        self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
        self.confirm_guard()

        # No WS — only the JS fix can make this transition happen
        self.wait_for(
            lambda: self.assertEqual(
                self.browser.find_element(
                    By.CSS_SELECTOR, ".card-stack"
                ).get_attribute("data-state"),
                "ineligible",
            )
        )

    def test_card_stack_cannot_be_reopened_after_selection(self):
        """Clicking the card stack immediately after picking a role must
        not open a second fan — the listener must have been removed."""
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        _equip_earthman_deck(founder)
        room = Room.objects.create(name="No-reopen Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "amigo@test.io", "bud@test.io",
            "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()
        for slot in room.gate_slots.order_by("slot_number"):
            TableSeat.objects.create(
                room=room, gamer=slot.gamer, slot_number=slot.slot_number,
            )
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)

        # Open fan, pick a card
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
            )
        ).click()
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
        self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
        self.confirm_guard()

        # Wait for fan to close (selectRole closes it synchronously)
        self.wait_for(
            lambda: self.assertEqual(
                len(self.browser.find_elements(By.ID, "id_role_select")), 0
            )
        )

        # Attempt to reopen — must not work
        self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
        self.wait_for(
            lambda: self.assertEqual(
                len(self.browser.find_elements(By.ID, "id_role_select")), 0
            )
        )

    # ------------------------------------------------------------------ #
    # Test 8a — Hex seats carry role labels during role select           #
    # ------------------------------------------------------------------ #

    def test_seats_around_hex_have_role_labels(self):
        """During role select the 6 .table-seat elements carry data-role
        attributes matching the fixed slot→role mapping (PC at slot 1, etc.)."""
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        room = Room.objects.create(name="Seat Label Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "amigo@test.io", "bud@test.io",
            "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)

        expected = {1: "PC", 2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".table-seat")
        )
        for slot_number, role_label in expected.items():
            seat = self.browser.find_element(
                By.CSS_SELECTOR, f".table-seat[data-slot='{slot_number}']"
            )
            self.assertEqual(seat.get_attribute("data-role"), role_label)

    # ------------------------------------------------------------------ #
    # Test 8b — Hex seats show .fa-ban when empty                        #
    # ------------------------------------------------------------------ #

    def test_seats_show_ban_icon_when_empty(self):
        """All 6 seats carry .fa-ban before any role has been chosen."""
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        room = Room.objects.create(name="Seat Ban Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "amigo@test.io", "bud@test.io",
            "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()
        for slot in room.gate_slots.order_by("slot_number"):
            TableSeat.objects.create(
                room=room, gamer=slot.gamer, slot_number=slot.slot_number,
            )
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)

        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".table-seat")
        )
        seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat")
        self.assertEqual(len(seats), 6)
        for seat in seats:
            self.assertTrue(
                seat.find_elements(By.CSS_SELECTOR, ".fa-ban"),
                f"Expected .fa-ban on seat slot {seat.get_attribute('data-slot')}",
            )

    # ------------------------------------------------------------------ #
    # Test 8c — Hex seat gets .fa-circle-check after role selected       #
    # ------------------------------------------------------------------ #

    def test_seat_gets_check_after_role_selected(self):
        """After confirming a role pick the corresponding hex seat should
        show .fa-circle-check and lose .fa-ban."""
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        _equip_earthman_deck(founder)
        room = Room.objects.create(name="Seat Check Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "amigo@test.io", "bud@test.io",
            "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()
        for slot in room.gate_slots.order_by("slot_number"):
            TableSeat.objects.create(
                room=room, gamer=slot.gamer, slot_number=slot.slot_number,
            )
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)

        # Open fan, pick first card (SC — Shepherd), confirm guard
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
            )
        ).click()
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
        self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
        self.confirm_guard()

        # Wait for tray animation to complete
        self.wait_for(
            lambda: self.assertFalse(
                self.browser.execute_script("return Tray.isOpen()"),
                "Tray should close after fade-in sequence",
            )
        )

        # The SC seat (slot 1) now shows check, no ban
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".table-seat[data-role='SC'] .fa-circle-check"
            )
        )
        self.assertEqual(
            len(self.browser.find_elements(
                By.CSS_SELECTOR, ".table-seat[data-role='SC'] .fa-ban"
            )),
            0,
        )


class RoleSelectTrayTest(FunctionalTest):
    """After confirming a role pick, the role card enters the tray grid and
    the tray opens to reveal it.

    Portrait — card lands at the topmost grid square (first child, row 1 col 1).
    Landscape — card lands at the leftmost grid square (first child, row 1 col 1).
    """

    EMAILS = [
        "slot1@test.io", "slot2@test.io", "slot3@test.io",
        "slot4@test.io", "slot5@test.io", "slot6@test.io",
    ]

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
        )
        Applet.objects.get_or_create(
            slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
        )

    def _make_room(self):
        """Room in ROLE_SELECT with all 6 seats created, slot 1 eligible."""
        founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
        _equip_earthman_deck(founder)
        room = Room.objects.create(name="Tray Card Test", owner=founder)
        _fill_room_via_orm(room, self.EMAILS)
        room.table_status = Room.ROLE_SELECT
        room.save()
        for slot in room.gate_slots.order_by("slot_number"):
            TableSeat.objects.create(
                room=room, gamer=slot.gamer, slot_number=slot.slot_number
            )
        return room

    def _select_role(self):
        """Open the fan, pick the first card, confirm the guard dialog."""
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
            )
        ).click()
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
        self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
        self.confirm_guard()

    # ------------------------------------------------------------------ #
    # T1 — Portrait: role card marks first cell; tray opens then closes  #
    # ------------------------------------------------------------------ #

    def test_portrait_role_card_enters_topmost_grid_square(self):
        """Portrait: after confirming a role the first .tray-cell gets
        .tray-role-card; the grid still has exactly 8 cells; and the tray
        opens briefly then closes once the fade-in animation completes."""
        self.browser.set_window_size(390, 844)
        room = self._make_room()
        self.create_pre_authenticated_session("slot1@test.io")
        self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/")

        self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap"))
        self._select_role()

        # First cell receives the role card class.
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, "#id_tray_grid .tray-role-card"
            )
        )

        result = self.browser.execute_script("""
            var grid = document.getElementById('id_tray_grid');
            var card = grid.querySelector('.tray-role-card');
            return {
                isFirst: card !== null && card === grid.firstElementChild,
                count:   grid.children.length,
                role:    card ? card.dataset.role : null
            };
        """)
        self.assertTrue(result["isFirst"], "Role card should be the first cell")
        self.assertEqual(result["count"], 8, "Grid should still have exactly 8 cells")
        self.assertTrue(result["role"], "First cell should carry data-role")

        # Tray closes after the animation sequence.
        self.wait_for(
            lambda: self.assertFalse(
                self.browser.execute_script("return Tray.isOpen()"),
                "Tray should close after the fade-in sequence"
            )
        )

    # ------------------------------------------------------------------ #
    # T2 — Landscape: same contract in landscape                         #
    # ------------------------------------------------------------------ #

    @tag('two-browser')
    def test_landscape_role_card_enters_leftmost_grid_square(self):
        """Landscape: the first .tray-cell gets .tray-role-card; grid has
        8 cells; tray opens then closes."""
        self.browser.set_window_size(844, 390)
        room = self._make_room()
        self.create_pre_authenticated_session("slot1@test.io")
        self.browser.get(f"{self.live_server_url}/gameboard/room/{room.id}/gate/")

        self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_wrap"))
        self._select_role()

        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, "#id_tray_grid .tray-role-card"
            )
        )

        result = self.browser.execute_script("""
            var grid = document.getElementById('id_tray_grid');
            var card = grid.querySelector('.tray-role-card');
            return {
                isFirst: card !== null && card === grid.firstElementChild,
                count:   grid.children.length
            };
        """)
        self.assertTrue(result["isFirst"], "Role card should be the first cell")
        self.assertEqual(result["count"], 8, "Grid should still have exactly 8 cells")

        self.wait_for(
            lambda: self.assertFalse(
                self.browser.execute_script("return Tray.isOpen()"),
                "Tray should close after the fade-in sequence"
            )
        )


@tag('channels')
class RoleSelectChannelsTest(ChannelsFunctionalTest):

    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(
            slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
        )
        Applet.objects.get_or_create(
            slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
        )

    # ------------------------------------------------------------------ #
    # Test 6 — Observer sees seat arc move via WebSocket                  #
    # ------------------------------------------------------------------ #

    def test_observer_sees_seat_arc_during_selection(self):
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        _equip_earthman_deck(founder)
        User.objects.get_or_create(email="watcher@test.io")
        room = Room.objects.create(name="Arc Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "watcher@test.io",
            "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()
        for slot in room.gate_slots.order_by("slot_number"):
            TableSeat.objects.create(
                room=room, gamer=slot.gamer, slot_number=slot.slot_number,
            )
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

        # 1. Watcher (slot 2) loads the room
        self.create_pre_authenticated_session("watcher@test.io")
        self.browser.get(room_url)
        self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, ".card-stack[data-state='ineligible']"
        ))

        # 2. Founder picks a role in second browser
        self.browser2 = self._make_browser2("founder@test.io")
        try:
            self.browser2.get(room_url)
            self.wait_for(lambda: self.browser2.find_element(
                By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
            ))
            self.browser2.find_element(By.CSS_SELECTOR, ".card-stack").click()
            self.wait_for(lambda: self.browser2.find_element(By.ID, "id_role_select"))
            self.browser2.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
            self.confirm_guard(browser=self.browser2)

            # 3. Watcher's turn arrives via WS — card-stack becomes eligible
            self.wait_for(lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
            ))
        finally:
            self.browser2.quit()

    def _make_browser2(self, email):
        """Spin up a second Firefox, authenticate email, return the browser."""
        session_key = create_pre_authenticated_session(email)
        options = webdriver.FirefoxOptions()
        if os.environ.get("HEADLESS"):
            options.add_argument("--headless")
        b = webdriver.Firefox(options=options)
        b.get(self.live_server_url + "/404_no_such_url/")
        b.add_cookie(dict(
            name=django_settings.SESSION_COOKIE_NAME,
            value=session_key,
            path="/",
        ))
        return b

    # ------------------------------------------------------------------ #
    # Test 5 — Tray closes on turn advance (portrait)                    #
    # ------------------------------------------------------------------ #

    def _make_turn_test_room(self):
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        _equip_earthman_deck(founder)
        User.objects.get_or_create(email="friend@test.io")
        room = Room.objects.create(name="Turn Test", owner=founder)
        _fill_room_via_orm(room, [
            "founder@test.io", "friend@test.io",
            "bud@test.io", "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        room.table_status = Room.ROLE_SELECT
        room.save()
        for slot in room.gate_slots.order_by("slot_number"):
            TableSeat.objects.create(
                room=room, gamer=slot.gamer, slot_number=slot.slot_number,
            )
        return f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

    def test_portrait_tray_closes_on_turn_advance(self):
        """Portrait: after selecting a role the tray opens and the role card lands
        in the topmost grid square.  When turn_changed arrives via WS, the tray
        force-closes so the next player's card-stack is not obscured."""
        self.browser.set_window_size(390, 844)
        room_url = self._make_turn_test_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)
        self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
        ))

        # Select a role — card lands in topmost grid square.
        self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
        self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
        self.confirm_guard()

        # Wait for fetch .then() — card must be first child of grid.
        self.wait_for(lambda: self.assertTrue(self.browser.execute_script("""
            var card = document.querySelector('#id_tray_grid .tray-role-card');
            return card !== null && card === card.parentElement.firstElementChild;
        """)))

        # Turn advances via WS — tray must close (forceClose in handleTurnChanged).
        self.wait_for(lambda: self.assertFalse(
            self.browser.execute_script("return Tray.isOpen()"),
            "Tray should be closed after turn advances"
        ))

    def test_landscape_tray_closes_on_turn_advance(self):
        """Landscape: role card at leftmost grid square; tray closes when
        turn_changed arrives via WS."""
        self.browser.set_window_size(844, 390)
        room_url = self._make_turn_test_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)
        self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
        ))

        self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
        self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
        self.confirm_guard()

        # Wait for fetch .then() — card must be first child of grid.
        self.wait_for(lambda: self.assertTrue(self.browser.execute_script("""
            var card = document.querySelector('#id_tray_grid .tray-role-card');
            return card !== null && card === card.parentElement.firstElementChild;
        """)))

        # Turn advances via WS — tray must close (forceClose in handleTurnChanged).
        self.wait_for(lambda: self.assertFalse(
            self.browser.execute_script("return Tray.isOpen()"),
            "Tray should be closed after turn advances"
        ))

    # ------------------------------------------------------------------ #
    # Test 7 — SCAN SIGS appears + card stack removed on last role        #
    # ------------------------------------------------------------------ #

    def test_pick_sigs_appears_and_card_stack_removed_on_last_role(self):
        """When the sixth and final role is confirmed, the all_roles_filled
        WS event makes the SCAN SIGS button visible and removes the card
        stack from the DOM entirely."""
        emails = [
            "founder@test.io", "amigo@test.io", "bud@test.io",
            "pal@test.io", "dude@test.io", "bro@test.io",
        ]
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        _equip_earthman_deck(founder)
        room = Room.objects.create(name="Last Role Test", owner=founder)
        _fill_room_via_orm(room, emails)
        room.table_status = Room.ROLE_SELECT
        room.save()
        # Pre-assign 5 roles (slots 2–6); founder (slot 1) is the final picker.
        pre_assigned = {2: "NC", 3: "EC", 4: "SC", 5: "AC", 6: "BC"}
        for slot in room.gate_slots.order_by("slot_number"):
            TableSeat.objects.create(
                room=room,
                gamer=slot.gamer,
                slot_number=slot.slot_number,
                role=pre_assigned.get(slot.slot_number),
            )

        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)
        self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, ".card-stack[data-state='eligible']"
        ))

        # Founder picks the last remaining role (PC — the only card in the fan).
        self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
        self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
        self.confirm_guard()

        # SCAN SIGS wrap must become visible via the all_roles_filled WS event.
        self.wait_for(lambda: self.assertFalse(
            self.browser.find_element(By.ID, "id_pick_sigs_wrap").get_attribute("style"),
        ))
        # Card stack must be removed from the DOM entirely.
        self.wait_for(lambda: self.assertEqual(
            len(self.browser.find_elements(By.CSS_SELECTOR, ".card-stack")), 0,
        ))


