import time

from datetime import timedelta
from django.utils import timezone
from selenium.webdriver.common.by import By

from .base import FunctionalTest
from apps.applets.models import Applet
from apps.epic.models import Room, GateSlot, select_token
from apps.lyric.models import Token, User
from .room_page import _fill_room_via_orm


class GatekeeperTest(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"}
        )

    def test_founder_creates_room_and_sees_gatekeeper(self):
        # 1. Log in, navigate to gameboard
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self.live_server_url + "/gameboard/")
        # 2. New Game applet has room name input, create button
        self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_applet_new_game")
        )
        self.browser.find_element(By.ID, "id_new_game_name").send_keys("Test Room")
        self.browser.find_element(By.ID, "id_create_game_btn").click()
        # 3. User is redirected to Gatekeeper page for new room
        self.wait_for(
            lambda: self.assertIn("/gameboard/room/", self.browser.current_url)
        )
        self.wait_for(
            lambda: self.assertIn("/gate/", self.browser.current_url)
        )
        # 4. Page shows room name, GATHERING status
        body = self.browser.find_element(By.TAG_NAME, "body")
        self.assertIn("TEST ROOM", body.text)
        self.assertIn("GATHERING GAMERS", body.text)
        # 5. Six token slot circles are visible, all empty
        slots = self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot")
        self.assertEqual(len(slots), 6)
        for slot in slots:
            self.assertIn("empty", slot.get_attribute("class"))
        # 6. Shared coin slot is present; no individual drop buttons
        self.browser.find_element(By.CSS_SELECTOR, ".token-slot")
        self.assertEqual(
            len(self.browser.find_elements(By.CSS_SELECTOR, ".drop-token-btn")), 0
        )

    def test_founder_drops_token_and_slot_fills(self):
        # 1. Set up: log in, create room, arrive at gatekeeper
        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")
        )
        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)
        )
        # 2. Founder clicks Insert Token via the shared coin slot
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
        ).click()
        # 3. Slot 1 (lowest) now shows OK button; slot is reserved
        ok_btn = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
            )
        )
        # 4. Founder clicks OK → slot fills
        ok_btn.click()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
        )
        slots = self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot")
        self.assertIn("filled", slots[0].get_attribute("class"))
        self.assertEqual(
            len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0
        )

    def test_room_appears_in_my_games_after_creation(self):
        # 1. Set up founder, game room, name
        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")
        )
        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)
        )
        # 2. Navigate back to gameboard
        self.browser.get(self.live_server_url + "/gameboard/")
        my_games = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_applet_my_games")
        )
        self.assertIn("Dragon's Den", my_games.text)

    def test_second_gamer_drops_token_into_open_slot(self):
        # 1. Founder creates room, confirms slot 1
        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")
        )
        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
        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")
        )
        # 2. Founder invites friend via the bud-btn slide-out (replaces
        # the legacy inline #id_invite_email form post-bud-btn refactor).
        bud_btn = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_bud_btn")
        )
        bud_btn.click()
        recipient = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_recipient")
        )
        # Wait for the slide-out's `scaleX(0)→scaleX(1)` 0.2s animation
        # to settle before clicking OK — the bounding rect is near-zero
        # during the transition, and Firefox auto-scroll-into-view fails
        # against a zero-width target w. ElementNotInteractableException
        # on the OK click. `html.bud-open` is set synchronously at btn
        # click but the panel's `transform` matrix arrives asynchronously
        # via the CSS transition; we poll `getBoundingClientRect().width`
        # >100px to wait for the actual layout to finish. Pipeline #316.
        self.wait_for(
            lambda: self.assertGreater(
                self.browser.execute_script(
                    "return document.getElementById('id_bud_panel').getBoundingClientRect().width;"
                ),
                100,
            )
        )
        recipient.send_keys("friend@test.io")
        self.browser.find_element(
            By.CSS_SELECTOR, "#id_bud_panel .btn.btn-confirm"
        ).click()
        # 3. Friend logs in, sees invitation in My Games
        self.create_pre_authenticated_session("friend@test.io")
        self.browser.get(self.live_server_url + "/gameboard/")
        my_games = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_applet_my_games")
        )
        self.assertIn("Dragon's Den", my_games.text)
        # 4. Friend follows link to gatekeeper
        self.browser.find_element(By.LINK_TEXT, "Dragon's Den").click()
        # 5. Friend drops token via coin slot and confirms
        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()
        # 6. Now two slots filled
        self.wait_for(
            lambda: self.assertEqual(
                len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot.filled")), 2
            )
        )

    def test_gate_opens_when_all_slots_filled(self):
        # 1. Founder creates room
        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")
        )
        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)
        )
        # 2. Founder confirms slot 1 via coin slot
        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")
        )
        # 3. Fill slots 2–6 directly via ORM
        room = Room.objects.get(name="Dragon's Den")
        for i, email in enumerate([
            "g2@test.io", "g3@test.io", "g4@test.io", "g5@test.io", "g6@test.io"
        ], start=2):
            gamer = User.objects.create(email=email)
            slot = room.gate_slots.get(slot_number=i)
            slot.gamer = gamer
            slot.status = GateSlot.FILLED
            slot.save()
        room.refresh_from_db()
        room.gate_status = Room.OPEN
        room.save()
        # 4. Gate shows launch button when all slots filled
        # update this for ASGI after channels sprint!
        self.browser.refresh()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".launch-game-btn")
        )

    def test_owner_can_delete_room_via_gear_menu(self):
        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"))
        self.browser.find_element(By.ID, "id_new_game_name").send_keys("Doomed Room")
        self.browser.find_element(By.ID, "id_create_game_btn").click()
        self.wait_for(lambda: self.assertIn("/gate/", self.browser.current_url))

        self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-danger")
        ).click()
        self.confirm_guard()

        self.wait_for(lambda: self.assertEqual(
            self.browser.current_url, self.live_server_url + "/gameboard/"
        ))
        self.assertFalse(Room.objects.filter(name="Doomed Room").exists())

    def test_gatekeeper_overlay_persists_after_htmx_poll(self):
        # 1. Create room directly (GATHERING) and navigate to its gate URL
        self.create_pre_authenticated_session("founder@test.io")
        founder = User.objects.get(email="founder@test.io")
        room = Room.objects.create(name="Persistent Room", owner=founder)
        self.browser.get(self.live_server_url + f"/gameboard/room/{room.id}/gate/")
        # 2. Assert overlay visible on initial page load
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
        )
        # 3. Wait for HTMX poll cycle to fire (poll interval is 3s)
        time.sleep(4)
        # 4. Assert overlay still present and visible after poll
        overlays = self.browser.find_elements(By.CSS_SELECTOR, ".gate-overlay")
        self.assertEqual(len(overlays), 1)
        self.assertTrue(overlays[0].is_displayed())

    def test_gamer_can_abandon_room_via_gear_menu(self):
        founder = User.objects.create(email="founder@test.io")
        room = Room.objects.create(name="Dragon's Den", owner=founder)
        slot = room.gate_slots.get(slot_number=2)
        self.create_pre_authenticated_session("gamer@test.io")
        gamer, _ = User.objects.get_or_create(email="gamer@test.io")
        slot.gamer = gamer
        slot.status = "FILLED"
        slot.save()

        self.browser.get(self.live_server_url + f"/gameboard/room/{room.id}/gate/")

        self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_room_menu .btn-abandon")
        ).click()
        self.confirm_guard()

        self.wait_for(lambda: self.assertEqual(
            self.browser.current_url, self.live_server_url + "/gameboard/"
        ))
        slot.refresh_from_db()
        self.assertEqual(slot.status, "EMPTY")
        self.assertIsNone(slot.gamer)


class CoinSlotTest(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"}
        )
        self.create_pre_authenticated_session("founder@test.io")
        self.founder = User.objects.get(email="founder@test.io")
        self.room = Room.objects.create(name="Coin Room", owner=self.founder)
        self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/"

    def test_coin_slot_active_for_eligible_gamer(self):
        # Gamer with no slot arrives at gatekeeper — coin slot is active
        self.browser.get(self.gate_url)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.active")
        )
        self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")

    def test_drop_token_reserves_lowest_empty_slot(self):
        # Gamer drops token; slot 1 (lowest) becomes reserved with OK button
        self.browser.get(self.gate_url)
        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, ".gate-slot[data-slot='1'] .btn-confirm"
            )
        )
        slot = self.room.gate_slots.get(slot_number=1)
        slot.refresh_from_db()
        self.assertEqual(slot.status, GateSlot.RESERVED)
        self.assertEqual(slot.gamer, self.founder)

    def test_confirm_fills_slot_and_removes_ok_button(self):
        # Drop then confirm → slot 1 FILLED, OK button gone
        self.browser.get(self.gate_url)
        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")
        )
        self.assertEqual(
            len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0
        )
        slot = self.room.gate_slots.get(slot_number=1)
        slot.refresh_from_db()
        self.assertEqual(slot.status, GateSlot.FILLED)

    def test_gamer_can_return_pending_token(self):
        # Drop then return via Push to Return → slot remains empty
        self.browser.get(self.gate_url)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
        ).click()
        # Push to Return appears in coin slot
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-return-btn")
        ).click()
        # Slot 1 still empty; coin slot active again
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.active")
        )
        slot = self.room.gate_slots.get(slot_number=1)
        slot.refresh_from_db()
        self.assertEqual(slot.status, GateSlot.EMPTY)

    def test_coin_slot_locked_while_another_token_is_pending(self):
        # Pre-set slot 1 as RESERVED by a different user
        other = User.objects.create(email="other@test.io")
        slot = self.room.gate_slots.get(slot_number=1)
        slot.gamer = other
        slot.status = GateSlot.RESERVED
        slot.reserved_at = timezone.now()
        slot.save()
        # Current user (founder) sees coin slot locked
        self.browser.get(self.gate_url)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.locked")
        )
        self.assertEqual(
            len(self.browser.find_elements(By.CSS_SELECTOR, "button.token-rails")), 0
        )

    def test_last_gamer_sees_pick_roles_button(self):
        # Fill slots 1–5 via ORM; slot 6 empty
        for i, email in enumerate([
            "g1@test.io", "g2@test.io", "g3@test.io", "g4@test.io", "g5@test.io"
        ], start=1):
            gamer = User.objects.create(email=email)
            slot = self.room.gate_slots.get(slot_number=i)
            slot.gamer = gamer
            slot.status = GateSlot.FILLED
            slot.save()
        # Founder (no slot yet) drops token → gets slot 6
        self.browser.get(self.gate_url)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
        ).click()
        # Slot 6 shows PICK ROLES instead of OK
        self.wait_for(
            lambda: self.assertIn(
                "PICK ROLES",
                self.browser.find_element(
                    By.CSS_SELECTOR, ".gate-slot[data-slot='6']"
                ).text,
            )
        )
        self.assertEqual(
            len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0
        )


class TokenPriorityTest(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"}
        )
        self.create_pre_authenticated_session("gamer@test.io")
        self.gamer = User.objects.get(email="gamer@test.io")
        self.room = Room.objects.create(name="Token Room", owner=self.gamer)
        self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/"
        self.coin = Token.objects.get(user=self.gamer, token_type=Token.COIN)

    def test_coin_is_used_by_default(self):
        # 1. COIN token created at signup, not yet leased to a room
        self.assertEqual(self.coin.token_type, Token.COIN)
        self.assertIsNone(self.coin.current_room)
        # 2. Gamer drops token and confirms
        self.browser.get(self.gate_url)
        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")
        )
        # 3. Coin is now leased to this room, page not refreshed
        self.assertEqual(self.browser.current_url, self.gate_url)
        self.coin.refresh_from_db()
        self.assertEqual(self.coin.current_room, self.room)

    def test_free_token_used_when_coin_in_use(self):
        # 1. Coin already leased to another room
        other_room = Room.objects.create(name="Other Room", owner=self.gamer)
        self.coin.current_room = other_room
        self.coin.save()
        # 2. Gamer has one unexpired free token (signup gives one; delete it and add fresh)
        Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
        Token.objects.create(
            user=self.gamer,
            token_type=Token.FREE,
            expires_at=timezone.now() + timedelta(days=7),
        )
        # 3. Gamer drops token → Free Token consumed
        self.browser.get(self.gate_url)
        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")
        )
        self.assertEqual(
            Token.objects.filter(user=self.gamer, token_type=Token.FREE).count(), 0
        )
        # 4. Coin untouched, still leased to other room
        self.assertEqual(self.browser.current_url, self.gate_url)
        self.coin.refresh_from_db()
        self.assertEqual(self.coin.current_room, other_room)

    def test_tithe_token_used_when_free_tokens_exhausted(self):
        # 1. Coin in use, no Free Tokens, one Tithe Token
        other_room = Room.objects.create(name="Other Room", owner=self.gamer)
        self.coin.current_room = other_room
        self.coin.save()
        Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
        tithe = Token.objects.create(user=self.gamer, token_type=Token.TITHE)
        # 2. Gamer drops token → tithe consumed
        self.browser.get(self.gate_url)
        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")
        )
        # Tithe row deleted, page hasn't refreshed
        self.assertEqual(self.browser.current_url, self.gate_url)
        self.assertFalse(Token.objects.filter(pk=tithe.pk).exists())

    def test_slot_blocked_when_no_tokens_available(self):
        # Coin in use, no Free Tokens, no Tithe Tokens → depleted state
        other_room = Room.objects.create(name="Other Room", owner=self.gamer)
        self.coin.current_room = other_room
        self.coin.save()
        Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
        self.browser.get(self.gate_url)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.depleted")
        )
        self.assertEqual(
            len(self.browser.find_elements(By.CSS_SELECTOR, "button.token-rails")), 0
        )

    def test_staff_backstage_pass_bypasses_token_cost(self):
        # 1. Staff user has a PASS token, equipped (DON-ed) — equip slot
        #    now gates trinket use, so PASS must be the active trinket
        #    for the gate picker to fire it. Auto-equipped COIN gets
        #    swapped out.
        self.gamer.is_staff = True
        self.gamer.save()
        pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS)
        self.gamer.equipped_trinket = pass_token
        self.gamer.save(update_fields=["equipped_trinket"])
        # 2. Drops token, confirms as normal
        self.browser.get(self.gate_url)
        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")
        )
        # 3. Pass not consumed, coin not leased; no reload
        self.assertEqual(self.browser.current_url, self.gate_url)
        self.assertTrue(Token.objects.filter(pk=pass_token.pk).exists())
        self.coin.refresh_from_db()
        self.assertIsNone(self.coin.current_room)


class GameKitInsertTest(FunctionalTest):
    """Token selected from Game Kit, inserted via token-slot click."""

    def setUp(self):
        super().setUp()
        self.create_pre_authenticated_session("gamer@insert.io")
        self.gamer = User.objects.get(email="gamer@insert.io")
        self.coin = self.gamer.tokens.filter(token_type=Token.COIN).first()
        self.room = Room.objects.create(name="Insert Room", owner=self.gamer)
        self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/"

    def _select_token_from_kit(self, token):
        self.browser.find_element(By.ID, "id_kit_btn").click()
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, f"[data-token-id='{token.id}']"
            ).click()
        )
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.ready")
        )

    def test_coin_insert_via_kit_reserves_slot(self):
        self.browser.get(self.gate_url)
        self._select_token_from_kit(self.coin)
        self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.reserved")
        )
        self.assertEqual(self.browser.current_url, self.gate_url)

    def test_free_token_insert_via_kit_consumed_on_confirm(self):
        self.gamer.tokens.filter(token_type=Token.FREE).delete()
        token = Token.objects.create(
            user=self.gamer,
            token_type=Token.FREE,
            expires_at=timezone.now() + timedelta(days=7),
        )
        self.browser.get(self.gate_url)
        self._select_token_from_kit(token)
        self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
            )
        ).click()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
        )
        self.assertFalse(Token.objects.filter(id=token.id).exists())

    def test_tithe_token_insert_via_kit_consumed_on_confirm(self):
        token = Token.objects.create(user=self.gamer, token_type=Token.TITHE)
        self.browser.get(self.gate_url)
        self._select_token_from_kit(token)
        self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
            )
        ).click()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
        )
        self.assertFalse(Token.objects.filter(id=token.id).exists())

    def test_pass_token_insert_via_kit_not_consumed(self):
        self.gamer.is_staff = True
        self.gamer.save()
        pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS)
        self.gamer.equipped_trinket = pass_token
        self.gamer.save(update_fields=["equipped_trinket"])
        self.browser.get(self.gate_url)
        self._select_token_from_kit(pass_token)
        self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.reserved")
        )
        self.assertTrue(Token.objects.filter(id=pass_token.id).exists())
        self.assertEqual(self.browser.current_url, self.gate_url)


class PositionIndicatorsTest(FunctionalTest):
    """Hex-side position indicators — always rendered outside the gatekeeper modal."""

    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"}
        )
        self.create_pre_authenticated_session("founder@test.io")
        self.founder, _ = User.objects.get_or_create(email="founder@test.io")
        self.room = Room.objects.create(name="Position Test Room", owner=self.founder)
        self.gate_url = (
            f"{self.live_server_url}/gameboard/room/{self.room.id}/gate/"
        )

    # ------------------------------------------------------------------ #
    # Test P1 — 6 position circles present in strip alongside gatekeeper #
    # ------------------------------------------------------------------ #

    def test_position_indicators_visible_alongside_gatekeeper(self):
        self.browser.get(self.gate_url)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
        )
        # Six .gate-slot elements are rendered in .position-strip, outside modal
        strip = self.browser.find_element(By.CSS_SELECTOR, ".position-strip")
        slots = strip.find_elements(By.CSS_SELECTOR, ".gate-slot")
        self.assertEqual(len(slots), 6)
        for slot in slots:
            self.assertTrue(slot.is_displayed())

    # ------------------------------------------------------------------ #
    # Test P2 — URL drops /gate/ after pick_roles                        #
    # ------------------------------------------------------------------ #

    def test_url_drops_gate_after_pick_roles(self):
        _fill_room_via_orm(self.room, [
            "founder@test.io", "amigo@test.io", "bud@test.io",
            "pal@test.io",    "dude@test.io",  "bro@test.io",
        ])
        self.room.table_status = Room.ROLE_SELECT
        self.room.save()

        self.browser.get(self.gate_url)
        expected_url = f"{self.live_server_url}/gameboard/room/{self.room.id}/"
        self.wait_for(
            lambda: self.assertEqual(self.browser.current_url, expected_url)
        )

    # ------------------------------------------------------------------ #
    # Test P3 — Gate-slot circles live outside the modal                 #
    # ------------------------------------------------------------------ #

    def test_position_circles_outside_gatekeeper_modal(self):
        """The numbered position circles must NOT be descendants of .gate-modal —
        they live in .position-strip which sits above the backdrop."""
        self.browser.get(self.gate_url)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-modal")
        )
        # No .gate-slot inside the modal
        modal_slots = self.browser.find_elements(
            By.CSS_SELECTOR, ".gate-modal .gate-slot"
        )
        self.assertEqual(len(modal_slots), 0)
        # All 6 live in .position-strip
        strip_slots = self.browser.find_elements(
            By.CSS_SELECTOR, ".position-strip .gate-slot"
        )
        self.assertEqual(len(strip_slots), 6)

    # ------------------------------------------------------------------ #
    # Test P4 — Each circle displays its slot number                     #
    # ------------------------------------------------------------------ #

    def test_position_circle_shows_slot_number(self):
        self.browser.get(self.gate_url)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".position-strip")
        )
        for n in range(1, 7):
            slot_el = self.browser.find_element(
                By.CSS_SELECTOR, f".position-strip .gate-slot[data-slot='{n}']"
            )
            self.assertIn(str(n), slot_el.text)

    # ------------------------------------------------------------------ #
    # Test P5 — Filled slot carries .filled class in strip               #
    # ------------------------------------------------------------------ #

    def test_filled_slot_shown_in_strip(self):
        from apps.epic.models import GateSlot
        slot = self.room.gate_slots.get(slot_number=1)
        slot.gamer = self.founder
        slot.status = GateSlot.FILLED
        slot.save()

        self.browser.get(self.gate_url)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".position-strip")
        )
        slot1 = self.browser.find_element(
            By.CSS_SELECTOR, ".position-strip .gate-slot[data-slot='1']"
        )
        self.assertIn("filled", slot1.get_attribute("class"))
        slot2 = self.browser.find_element(
            By.CSS_SELECTOR, ".position-strip .gate-slot[data-slot='2']"
        )
        self.assertIn("empty", slot2.get_attribute("class"))
