import os

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 (
    SIG_SEAT_ORDER, _assign_all_roles, _fill_room_via_orm,
)
from apps.applets.models import Applet
from apps.epic.models import Room
from apps.lyric.models import User


# ── Significator Selection ────────────────────────────────────────────────────
#
# After all 6 roles are revealed the room enters SIG_SELECT.  Two parallel
# 18-card overlays appear (levity: PC/NC/SC; gravity: BC/EC/AC).  Each polarity
# group picks simultaneously — no sequential turn order.
#
# ─────────────────────────────────────────────────────────────────────────────


class SigSelectTest(FunctionalTest):
    """Significator Selection — non-WebSocket tests."""

    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 S1 — Seats reorder to canonical role sequence at SIG_SELECT  #
    # ------------------------------------------------------------------ #

    def test_seats_display_in_pc_nc_ec_sc_ac_bc_order_after_reveal(self):
        """Slots were filled in arbitrary token-drop order; after roles are
        revealed the seat portraits must appear in PC→NC→EC→SC→AC→BC order."""
        founder, _ = User.objects.get_or_create(email="founder@test.io")
        room = Room.objects.create(name="Seat Order Test", owner=founder)
        # Assign roles in reverse of canonical order so the reordering is visible
        _fill_room_via_orm(room, [
            "founder@test.io", "amigo@test.io", "bud@test.io",
            "pal@test.io",    "dude@test.io",  "bro@test.io",
        ])
        _assign_all_roles(room, role_order=["BC", "AC", "SC", "EC", "NC", "PC"])

        self.create_pre_authenticated_session("founder@test.io")
        room_url = f"{self.live_server_url}/gameboard/room/{room.id}/gate/"
        self.browser.get(room_url)

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

        seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat[data-role]")
        self.assertEqual(len(seats), 6)
        roles_in_order = [s.get_attribute("data-role") for s in seats]
        self.assertEqual(roles_in_order, SIG_SEAT_ORDER)



@tag("channels")
class SigSelectChannelsTest(ChannelsFunctionalTest):
    """Significator Selection — WebSocket tests."""

    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_browser2(self, email):
        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

    def _setup_sig_select_room(self):
        """Create a full SIG_SELECT room; return (room, [user_pc, user_nc, ...])."""
        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=emails[0])
        room = Room.objects.create(name="Cursor Colour Test", owner=founder)
        gamers = _fill_room_via_orm(room, emails)
        _assign_all_roles(room)
        return room, gamers

    # ── SC1: NC hover → PC sees mid cursor active, coloured --priYl ────────── #

    @tag('channels')
    def test_nc_hover_activates_mid_cursor_in_pc_browser(self):
        """
        When NC (levity mid) hovers a card, PC (levity left) must see the
        --mid cursor become active, coloured --priYl (rgb 255 207 52).
        Verifies: WS broadcast pipeline + JS applyHover + CSS role colouring.
        """
        room, gamers = self._setup_sig_select_room()
        room_url = self.live_server_url + f"/gameboard/room/{room.pk}/"

        # ── Browser 1: PC (founder) ───────────────────────────────────────────
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))

        # ── Browser 2: NC (amigo) ─────────────────────────────────────────────
        browser2 = self._make_browser2("amigo@test.io")
        try:
            browser2.get(room_url)
            self.wait_for(lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-overlay"))

            # Grab the first card ID visible in browser2's deck
            first_card = browser2.find_element(By.CSS_SELECTOR, ".sig-card")
            card_id = first_card.get_attribute("data-card-id")

            # Hover over it — triggers sendHover() → WS broadcast
            from selenium.webdriver.common.action_chains import ActionChains
            ActionChains(browser2).move_to_element(first_card).perform()

            # ── Browser 1 should see --mid cursor go active (anchor carries class) ─
            mid_cursor_sel = f'.sig-card[data-card-id="{card_id}"] .sig-cursor--mid'
            self.wait_for(
                lambda: self.browser.find_element(
                    By.CSS_SELECTOR, mid_cursor_sel + ".active"
                )
            )

            # CSS colour check: portal float has data-role="NC" → --priYl = 255, 207, 52
            portal_sel = '.sig-cursor-float[data-role="NC"]'
            portal_cursor = self.browser.find_element(By.CSS_SELECTOR, portal_sel)
            color = self.browser.execute_script(
                "return window.getComputedStyle(arguments[0]).color",
                portal_cursor,
            )
            self.assertEqual(color, "rgb(255, 207, 52)", f"Expected --priYl colour for NC cursor, got {color}")

            # ── Mouse-off: anchor class removed, portal float gone ────────────
            ActionChains(browser2).move_to_element(
                browser2.find_element(By.CSS_SELECTOR, ".sig-stage")
            ).perform()
            self.wait_for(
                lambda: not self.browser.find_elements(
                    By.CSS_SELECTOR, mid_cursor_sel + ".active"
                )
            )

        finally:
            browser2.quit()

    # ── SC2: NC reserves → PC sees card border coloured --priYl ──────────── #

    @tag('channels')
    def test_nc_reservation_glows_priYl_in_pc_browser(self):
        """
        When NC (levity mid) clicks OK on a card, PC must see that card's border
        coloured --priYl (rgb 255 207 52) via the data-reserved-by CSS selector.
        Verifies: sig_reserve view → WS broadcast → applyReservation → CSS glow.
        """
        room, gamers = self._setup_sig_select_room()
        room_url = self.live_server_url + f"/gameboard/room/{room.pk}/"

        # ── Browser 1: PC (founder) ───────────────────────────────────────────
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(room_url)
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))

        # ── Browser 2: NC (amigo) ─────────────────────────────────────────────
        browser2 = self._make_browser2("amigo@test.io")
        try:
            browser2.get(room_url)
            self.wait_for(lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-overlay"))

            # Get first card in B2's deck
            first_card = browser2.find_element(By.CSS_SELECTOR, ".sig-card")
            card_id = first_card.get_attribute("data-card-id")

            # Click card body → .sig-focused → OK button appears
            from selenium.webdriver.common.action_chains import ActionChains
            ActionChains(browser2).move_to_element(first_card).perform()
            first_card.click()

            ok_btn = self.wait_for(
                lambda: browser2.find_element(By.CSS_SELECTOR, ".sig-focused .sig-ok-btn")
            )
            ok_btn.click()

            # ── B1 should see the card's border turn --priYl ──────────────────
            reserved_card_sel = f'.sig-card[data-card-id="{card_id}"]'
            self.wait_for(
                lambda: self.browser.find_element(
                    By.CSS_SELECTOR, reserved_card_sel + '[data-reserved-by="NC"]'
                )
            )

            reserved_card = self.browser.find_element(By.CSS_SELECTOR, reserved_card_sel)
            box_shadow = self.browser.execute_script(
                "return window.getComputedStyle(arguments[0]).boxShadow",
                reserved_card,
            )
            self.assertIn(
                "255, 207, 52", box_shadow,
                f"Expected --priYl (255,207,52) in box-shadow for NC reservation, got {box_shadow}",
            )

        finally:
            browser2.quit()



# ── Polarity theming: qualifier text + no correspondence ─────────────────────

class SigSelectThemeTest(FunctionalTest):
    """Polarity-qualifier display (Graven/Leavened) and correspondence suppression.
    No WebSocket needed — stage updates are local; uses plain FunctionalTest."""

    EMAILS = [
        "founder@test.io", "amigo@test.io", "bud@test.io",
        "pal@test.io",     "dude@test.io",  "bro@test.io",
    ]

    def setUp(self):
        super().setUp()
        self.browser.set_window_size(800, 1200)
        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 _setup_sig_room(self):
        founder, _ = User.objects.get_or_create(email=self.EMAILS[0])
        room = Room.objects.create(name="Theme Test", owner=founder)
        _fill_room_via_orm(room, self.EMAILS)
        _assign_all_roles(room)
        return room

    def _hover_card(self, css):
        from selenium.webdriver.common.action_chains import ActionChains
        card = self.browser.find_element(By.CSS_SELECTOR, css)
        ActionChains(self.browser).move_to_element(card).perform()
        return card

    # ── ST1: Levity (Leavened) qualifier ──────────────────────────────────── #

    def test_levity_non_major_card_shows_elevated_above(self):
        """Hovering a non-major card in the levity overlay shows 'Elevated' in
        qualifier-above and nothing in qualifier-below."""
        room = self._setup_sig_room()
        self.create_pre_authenticated_session("founder@test.io")  # PC = levity
        self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))

        self._hover_card('.sig-card[data-arcana="Middle Arcana"]')

        above = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
        )
        self.assertEqual(above.text, "Elevated")
        below = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
        self.assertEqual(below.text, "")

    def test_levity_major_card_shows_enlightened_below(self):
        """Hovering a major arcana card in the levity overlay shows 'Enlightened' in
        qualifier-below and nothing in qualifier-above."""
        room = self._setup_sig_room()
        self.create_pre_authenticated_session("founder@test.io")  # PC = levity
        self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))

        self._hover_card('.sig-card[data-arcana="Major Arcana"]')

        below = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-below")
        )
        self.assertEqual(below.text, "Enlightened")
        above = self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
        self.assertEqual(above.text, "")

    # ── ST2: Gravity (Graven) qualifier ───────────────────────────────────── #

    def test_gravity_non_major_card_shows_graven_above(self):
        """EC (bud) sees the gravity overlay; hovering a non-major card shows 'Graven'."""
        room = self._setup_sig_room()
        self.create_pre_authenticated_session("bud@test.io")  # EC = gravity
        self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))

        self._hover_card('.sig-card[data-arcana="Middle Arcana"]')

        above = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-qualifier-above")
        )
        self.assertEqual(above.text, "Graven")

    # ── ST3: Correspondence not shown ─────────────────────────────────────── #

    def test_correspondence_not_shown_in_sig_select(self):
        """The Minchiate-equivalence field must always be blank on the stage card."""
        room = self._setup_sig_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))

        # Hover any card — correspondence should remain empty regardless
        self._hover_card(".sig-card")
        self.wait_for(lambda: self.browser.find_element(
            By.CSS_SELECTOR, ".sig-stage-card"
        ))
        corr = self.browser.find_element(By.CSS_SELECTOR, ".fan-card-correspondence")
        self.assertEqual(corr.text, "")


# ── SAVE SIG / WAIT NVM — ready gate ──────────────────────────────────────────
#
# SAVE SIG (.btn.btn-primary) appears at the bottom-left corner of the card
# stage preview once a gamer has clicked OK on a card (SigReservation exists).
# Clicking it sets the gamer's status to ready and changes the btn to WAIT NVM.
# WAIT NVM cancels the ready status and reverts back to SAVE SIG.
#
# When all three gamers in a polarity WS room are ready, a 12-second countdown
# starts.  Any WAIT NVM during the countdown cancels it; the saved remaining time
# is resumed when all three are ready again.  When the countdown completes
# (client POSTs sig_confirm) the polarity group returns to the table hex.
# When both polarity groups have confirmed, CAST SKY btn appears in the hex
# center for all six gamers.
#
# ─────────────────────────────────────────────────────────────────────────────


class SigReadyGateTest(FunctionalTest):
    """Single-browser tests for SAVE SIG / WAIT NVM btn."""

    def setUp(self):
        super().setUp()
        self.browser.set_window_size(800, 1200)
        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 _setup_sig_room(self):
        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=emails[0])
        room = Room.objects.create(name="Ready Gate Test", owner=founder)
        _fill_room_via_orm(room, emails)
        _assign_all_roles(room)
        return room

    def _click_ok_on_any_card(self):
        """Click the first sig card to stage it, then click OK."""
        card = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-card")
        )
        card.click()
        ok_btn = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-ok-btn")
        )
        ok_btn.click()

    # ── SRG1: SAVE SIG btn not visible before OK ──────────────────────── #

    def test_take_sig_btn_not_visible_before_ok_click(self):
        """SAVE SIG must be absent until the gamer has OK'd a card."""
        room = self._setup_sig_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))

        take_sig_btns = self.browser.find_elements(By.ID, "id_take_sig_btn")
        self.assertEqual(len(take_sig_btns), 0)

    # ── SRG2: SAVE SIG btn appears after OK ──────────────────────────── #

    def test_take_sig_btn_appears_after_ok_click(self):
        room = self._setup_sig_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
        self._click_ok_on_any_card()

        take_sig_btn = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_take_sig_btn")
        )
        self.assertIn("SAVE SIG", take_sig_btn.text.upper())

    # ── SRG3: SAVE SIG → WAIT NVM ─────────────────────────────────────── #

    def test_take_sig_btn_becomes_wait_no_after_click(self):
        room = self._setup_sig_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
        self._click_ok_on_any_card()

        take_sig_btn = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_take_sig_btn")
        )
        take_sig_btn.click()

        wait_no_btn = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_take_sig_btn")
        )
        self.assertIn("WAIT NVM", wait_no_btn.text.upper())

        # WAIT NVM pulses a --terOr glow: btn-cancel class appears within one tick
        self.wait_for(
            lambda: "btn-cancel" in self.browser.find_element(
                By.ID, "id_take_sig_btn"
            ).get_attribute("class")
        )

    # ── SRG4: WAIT NVM → SAVE SIG ─────────────────────────────────────── #

    def test_wait_no_reverts_to_take_sig(self):
        room = self._setup_sig_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".sig-overlay"))
        self._click_ok_on_any_card()

        btn = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_take_sig_btn")
        )
        btn.click()  # → WAIT NVM
        self.wait_for(lambda: "WAIT NVM" in self.browser.find_element(
            By.ID, "id_take_sig_btn").text.upper()
        )
        btn = self.browser.find_element(By.ID, "id_take_sig_btn")
        btn.click()  # → SAVE SIG again

        self.wait_for(
            lambda: self.assertIn(
                "SAVE SIG",
                self.browser.find_element(By.ID, "id_take_sig_btn").text.upper(),
            )
        )


@tag("channels")
class SigReadyCountdownChannelsTest(ChannelsFunctionalTest):
    """Multi-browser WebSocket tests for the polarity-room countdown and CAST SKY."""

    def setUp(self):
        super().setUp()
        self.browser.set_window_size(800, 1200)
        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_browser_for(self, email):
        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.set_window_size(800, 1200)
        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

    def _ok_card_in_browser(self, b):
        """Reserve any available sig-card, then JS-click its OK button to trigger
        the page's applyReservation() and reveal #id_take_sig_btn.

        Iterates through cards until one succeeds — multiple browsers in the same
        polarity group would otherwise all try to reserve the same first card and
        get 409 conflicts."""
        self.wait_for(
            lambda: b.find_element(By.CSS_SELECTOR, ".sig-card"), browser=b
        )
        result = b.execute_async_script("""
            var cb = arguments[arguments.length - 1];
            var overlay = document.querySelector('.sig-overlay');
            var cards   = document.querySelectorAll('.sig-card');
            if (!overlay || !cards.length) { cb({error: 'no overlay or cards'}); return; }
            var reserveUrl = overlay.dataset.reserveUrl;
            var csrfM = document.cookie.match(/csrftoken=([^;]+)/);
            var csrf  = csrfM ? csrfM[1] : '';
            function tryCard(idx) {
                if (idx >= cards.length) { cb({error: 'all cards taken'}); return; }
                var cardId = cards[idx].dataset.cardId;
                fetch(reserveUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'X-CSRFToken': csrf,
                    },
                    body: 'action=reserve&card_id=' + encodeURIComponent(cardId),
                }).then(function (res) {
                    if (res.status === 409) { tryCard(idx + 1); return; }
                    cb({status: res.status, ok: res.ok, cardId: cardId});
                }).catch(function (e) { cb({error: e.message}); });
            }
            tryCard(0);
        """)
        if not (result and result.get('ok')):
            raise AssertionError(f"sig_reserve fetch failed: {result}")
        # Fetch confirmed 200 — JS-click the *correct* card's OK button so
        # applyReservation() runs in page context and reveals #id_take_sig_btn.
        # (Idempotent re-reserve of the same card → 200, safe.)
        card_id = result['cardId']
        ok_btn = b.find_element(
            By.CSS_SELECTOR, f'.sig-card[data-card-id="{card_id}"] .sig-ok-btn'
        )
        b.execute_script("arguments[0].click()", ok_btn)

    def _setup_sig_select_room(self):
        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=emails[0])
        room = Room.objects.create(name="Countdown Test", owner=founder)
        _fill_room_via_orm(room, emails)
        _assign_all_roles(room)
        return room, emails

    # ── SRG5: countdown appears when all three polarity ready ─────────── #

    @tag("channels")
    def test_countdown_element_appears_when_all_three_levity_gamers_ready(self):
        """When PC, NC, and SC each click SAVE SIG the countdown becomes visible."""
        room, emails = self._setup_sig_select_room()
        levity_emails = [emails[0], emails[1], emails[3]]  # PC, NC, SC
        browsers = []
        try:
            for email in levity_emails:
                b = self._make_browser_for(email)
                b.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
                browsers.append(b)

            # Each levity gamer OK's a card then clicks SAVE SIG
            for b in browsers:
                self._ok_card_in_browser(b)
                self.wait_for(
                    lambda: b.find_element(By.ID, "id_take_sig_btn"), browser=b
                )
                b.find_element(By.ID, "id_take_sig_btn").click()

            # All three browsers should now see the countdown button (numeral text)
            for b in browsers:
                self.wait_for(
                    lambda: b.find_element(By.ID, "id_take_sig_btn").text.isdigit(),
                    browser=b,
                )
        finally:
            for b in browsers:
                b.quit()

    # ── SRG6: countdown disappears when WAIT NVM clicked ──────────────── #

    @tag("channels")
    def test_countdown_disappears_when_any_levity_gamer_clicks_wait_no(self):
        """Any WAIT NVM during the countdown cancels it for all three browsers."""
        room, emails = self._setup_sig_select_room()
        levity_emails = [emails[0], emails[1], emails[3]]
        browsers = []
        try:
            for email in levity_emails:
                b = self._make_browser_for(email)
                b.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
                browsers.append(b)

            # All go ready
            for b in browsers:
                self._ok_card_in_browser(b)
                self.wait_for(
                    lambda: b.find_element(By.ID, "id_take_sig_btn"), browser=b
                )
                b.find_element(By.ID, "id_take_sig_btn").click()

            # Confirm countdown started for all (button text is a numeral)
            for b in browsers:
                self.wait_for(
                    lambda: b.find_element(By.ID, "id_take_sig_btn").text.isdigit(),
                    browser=b,
                )

            # PC clicks the countdown button to cancel
            browsers[0].find_element(By.ID, "id_take_sig_btn").click()

            # Countdown should cancel for all three (button back to WAIT NVM)
            for b in browsers:
                self.wait_for(
                    lambda: b.find_element(By.ID, "id_take_sig_btn").text == "WAIT NVM",
                    browser=b,
                )
        finally:
            for b in browsers:
                b.quit()

    # ── SRG7: CAST SKY btn appears after both polarity groups confirm ─── #

    @tag("channels")
    def test_pick_sky_btn_appears_in_hex_after_both_groups_confirm(self):
        """Once both levity and gravity countdowns complete, all six browsers
        see the CAST SKY btn in the table hex center."""
        # This test drives the full flow end-to-end but uses ORM shortcuts
        # to set all-ready state for one polarity, letting the other complete
        # via the UI, to keep execution time manageable.
        room, emails = self._setup_sig_select_room()
        # Pre-confirm gravity via ORM: set significators on EC/AC/BC seats
        from apps.epic.models import TarotCard, DeckVariant
        earthman = DeckVariant.objects.get(slug="earthman")
        grav_roles = ["EC", "AC", "BC"]
        grav_suits = ["GRAILS", "BLADES", "CROWNS"]
        for role, suit in zip(grav_roles, grav_suits):
            card = TarotCard.objects.get(
                deck_variant=earthman, arcana="MIDDLE", suit=suit, number=11
            )
            seat = room.table_seats.get(role=role)
            seat.significator = card
            seat.save()

        levity_emails = [emails[0], emails[1], emails[3]]
        browsers = []
        try:
            for email in levity_emails:
                b = self._make_browser_for(email)
                b.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
                browsers.append(b)

            # All levity gamers OK and SAVE SIG
            for b in browsers:
                self._ok_card_in_browser(b)
                self.wait_for(
                    lambda: b.find_element(By.ID, "id_take_sig_btn"), browser=b
                )
                b.find_element(By.ID, "id_take_sig_btn").click()

            # Wait for countdown to expire or be confirmed; CAST SKY appears in hex
            # countdown is 12 s so use wait_for_slow (MAX_WAIT=10 is not enough)
            for b in browsers:
                self.wait_for_slow(
                    lambda: b.find_element(By.ID, "id_pick_sky_btn"), browser=b
                )
        finally:
            for b in browsers:
                b.quit()

    # ── SRG8: first-done group sees waiting message ───────────────────── #

    @tag("channels")
    def test_first_done_polarity_sees_other_group_settling_message(self):
        """After levity confirms but gravity hasn't yet, levity gamers see
        'Gravity settling . . .' on the dormant hex."""
        room, emails = self._setup_sig_select_room()
        levity_emails = [emails[0], emails[1], emails[3]]
        browsers = []
        try:
            for email in levity_emails:
                b = self._make_browser_for(email)
                b.get(self.live_server_url + f"/gameboard/room/{room.pk}/")
                browsers.append(b)

            for b in browsers:
                self._ok_card_in_browser(b)
                self.wait_for(
                    lambda: b.find_element(By.ID, "id_take_sig_btn"), browser=b
                )
                b.find_element(By.ID, "id_take_sig_btn").click()

            # Wait for levity confirm → hex revealed, waiting message visible
            # (countdown is 12 s, so wait_for's 10 s MAX_WAIT is not enough)
            for b in browsers:
                self.wait_for_slow(
                    lambda: "settling" in b.find_element(
                        By.ID, "id_hex_waiting_msg"
                    ).text.lower(),
                    browser=b,
                )
        finally:
            for b in browsers:
                b.quit()


# ── SKY OVERLAY (natal wheel) — DEFERRED / PENDING PYSWISS ──────────────────
#
# These FTs outline the sky overlay behavior but are left as stubs.
# The sky overlay will be built after the PySwiss microservice (step 18)
# and the D3 natal wheel implementation.  A prototype already exists and
# will be reviewed before these tests are filled in.
#
# class PickSkyTrayFlowTest(FunctionalTest):
#
#     def test_pick_sky_btn_opens_tray_with_sig_card_in_slot_2(self):
#         """Clicking CAST SKY opens #id_tray; tray cell 2 shows the gamer's
#         sig card icon (Blank.svg placeholder until card-specific icons land)."""
#         ...
#
#     def test_tray_close_dismisses_sig_overlay_and_reveals_hex(self):
#         """After tray closes the sig select modal is gone and the table hex
#         is visible again."""
#         ...
#
#     def test_sky_overlay_appears_over_hex_after_tray_closes(self):
#         """The sky overlay (#id_sky_overlay) appears over the hex once the
#         tray animation completes."""
#         ...
#
#     def test_sky_overlay_prompts_for_input_date(self):
#         """The sky overlay contains a date input field for natal wheel
#         calculation via the PySwiss microservice API."""
#         ...
#
#     def test_sky_overlay_renders_natal_wheel_for_given_date(self):
#         """Submitting a date triggers a D3-drawn natal wheel (pyswisseph
#         data).  Each house/planet is individually navigable."""
#         ...
#
#     def test_sky_overlay_accessible_during_play_for_timeframe_changes(self):
#         """During IN_GAME phase a gamer can reopen the sky overlay to change
#         the timeframe or check aspects presiding over the current scene."""
#         ...
#
# ─────────────────────────────────────────────────────────────────────────────
