import time
import unittest

from django.test import tag
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By

from .base import FunctionalTest
from .room_page import _assign_all_roles, _fill_room_via_orm
from apps.applets.models import Applet
from apps.epic.models import DeckVariant, Room
from apps.lyric.models import User


# ── Seat Tray ────────────────────────────────────────────────────────────────
#
# The Tray is a per-seat, per-room slide-out panel anchored to the right edge
# of the viewport.  #id_tray_btn is a drawer-handle-shaped button: a circle
# with an icon (the "ivory centre") with decorative lines curving from its top
# and bottom to the right edge of the screen.
#
# Behaviour:
#   - Closed by default; tray panel (#id_tray) is not visible.
#   - Clicking the button while closed: wobbles the handle (adds "wobble"
#     class) but does NOT open the tray.
#   - Dragging the button leftward: reveals the tray.
#   - Clicking the button while open: slides the tray closed.
#   - On page reload: tray always starts closed (JS in-memory only).
#
# Contents (populated in later sprints): Role card, Significator, Celtic Cross
# draw, sky wheel, committed dice/cards for this table.
#
# ─────────────────────────────────────────────────────────────────────────────


class TrayTest(FunctionalTest):

    def setUp(self):
        # Portrait viewport for T1–T5 (768×1024). Use _make_browser so
        # headless CI gets --width/--height args and the CSS orientation
        # media query is correct from first paint.
        self.browser = self._make_browser(768, 1024)
        self.test_server = None

    def _switch_to_landscape(self):
        """Recreate the browser, navigate to about:blank, then resize to
        900×500 and wait until window.innerWidth > window.innerHeight confirms
        the CSS orientation media query will fire correctly on the next page."""
        self.browser.quit()
        self.browser = self._make_browser(900, 500)
        self.browser.get('about:blank')
        self.browser.set_window_size(900, 500)
        time.sleep(0.5)  # allow Firefox to flush the resize before navigating
        self.wait_for(lambda: self.assertTrue(
            self.browser.execute_script(
                'return window.innerWidth > window.innerHeight'
            )
        ))

    def _simulate_drag(self, btn, offset_x):
        """Dispatch JS pointer events directly — more reliable than GeckoDriver drag."""
        start_x = btn.rect['x'] + btn.rect['width'] / 2
        end_x = start_x + offset_x
        self.browser.execute_script("""
            var btn = arguments[0], startX = arguments[1], endX = arguments[2];
            btn.dispatchEvent(new PointerEvent("pointerdown", {clientX: startX, bubbles: true}));
            document.dispatchEvent(new PointerEvent("pointermove", {clientX: endX, bubbles: true}));
            document.dispatchEvent(new PointerEvent("pointerup",   {clientX: endX, bubbles: true}));
        """, btn, start_x, end_x)

    def _simulate_drag_y(self, btn, offset_y):
        """Dispatch JS pointer events on the Y axis for landscape drag tests."""
        start_y = btn.rect['y'] + btn.rect['height'] / 2
        end_y = start_y + offset_y
        self.browser.execute_script("""
            var btn = arguments[0], startY = arguments[1], endY = arguments[2];
            btn.dispatchEvent(new PointerEvent("pointerdown", {clientY: startY, clientX: 0, bubbles: true}));
            document.dispatchEvent(new PointerEvent("pointermove", {clientY: endY,   clientX: 0, bubbles: true}));
            document.dispatchEvent(new PointerEvent("pointerup",   {clientY: endY,   clientX: 0, bubbles: true}));
        """, btn, start_y, end_y)

    def _make_role_select_room(self, founder_email="founder@test.io"):
        from apps.epic.models import TableSeat
        founder, _ = User.objects.get_or_create(email=founder_email)
        room = Room.objects.create(name="Tray Test Room", owner=founder)
        emails = [founder_email, "nc@test.io", "bud@test.io",
                  "pal@test.io", "dude@test.io", "bro@test.io"]
        _fill_room_via_orm(room, emails)
        room.table_status = Room.ROLE_SELECT
        room.save()
        for i, email in enumerate(emails, start=1):
            gamer, _ = User.objects.get_or_create(email=email)
            TableSeat.objects.get_or_create(room=room, gamer=gamer, slot_number=i)
        return room

    def _make_sig_select_room(self, founder_email="founder@test.io"):
        founder, _ = User.objects.get_or_create(email=founder_email)
        room = Room.objects.create(name="Tray Test Room", owner=founder)
        _fill_room_via_orm(room, [
            founder_email, "nc@test.io",  "bud@test.io",
            "pal@test.io", "dude@test.io", "bro@test.io",
        ])
        _assign_all_roles(room)
        return room

    def _room_url(self, room):
        return f"{self.live_server_url}/gameboard/room/{room.id}/gate/"

    # ------------------------------------------------------------------ #
    # Test T1 — tray button is present and anchored to the right edge    #
    # ------------------------------------------------------------------ #

    def test_tray_btn_is_present_on_room_page(self):
        room = self._make_sig_select_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self._room_url(room))

        btn = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_tray_btn")
        )
        self.assertTrue(btn.is_displayed())

        # Button should be anchored near the right edge of the viewport
        vp_width = self.browser.execute_script("return window.innerWidth")
        btn_right = btn.location["x"] + btn.size["width"]
        self.assertGreater(btn_right, vp_width * 0.8)

    # ------------------------------------------------------------------ #
    # Test T2 — tray is closed by default; clicking wobbles the handle   #
    # ------------------------------------------------------------------ #

    def test_tray_is_closed_by_default_and_click_wobbles(self):
        room = self._make_sig_select_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self._room_url(room))

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

        # Tray panel not visible when closed
        tray = self.browser.find_element(By.ID, "id_tray")
        self.assertFalse(tray.is_displayed())

        # Clicking the closed btn adds a wobble class to the wrap.
        # Use a MutationObserver to capture the transient class change — in CI
        # headless Firefox the 0.45s animation may complete before the first
        # wait_for poll (0.5s), causing a false miss.
        self.browser.execute_script("""
            window._trayWobbled = false;
            var wrap = document.getElementById('id_tray_wrap');
            var obs = new MutationObserver(function(muts) {
                muts.forEach(function(m) {
                    if (m.type === 'attributes' && m.attributeName === 'class') {
                        if (m.target.classList.contains('wobble')) {
                            window._trayWobbled = true;
                            obs.disconnect();
                        }
                    }
                });
            });
            obs.observe(wrap, {attributes: true, attributeFilter: ['class']});
        """)
        self.browser.find_element(By.ID, "id_tray_btn").click()
        self.wait_for(
            lambda: self.assertTrue(
                self.browser.execute_script("return window._trayWobbled;")
            )
        )
        # Tray still not visible — a click alone must not open it
        self.assertFalse(tray.is_displayed())

    # ------------------------------------------------------------------ #
    # Test T3 — dragging tray btn leftward opens the tray                #
    # ------------------------------------------------------------------ #

    def test_dragging_tray_btn_left_opens_tray(self):
        room = self._make_sig_select_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self._room_url(room))

        btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
        tray = self.browser.find_element(By.ID, "id_tray")
        self.assertFalse(tray.is_displayed())

        self._simulate_drag(btn, -300)

        self.wait_for(lambda: self.assertTrue(tray.is_displayed()))

    # ------------------------------------------------------------------ #
    # Test T4 — clicking btn while tray is open slides it closed         #
    # ------------------------------------------------------------------ #

    def test_clicking_open_tray_btn_closes_tray(self):
        room = self._make_sig_select_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self._room_url(room))

        btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
        self._simulate_drag(btn, -300)

        tray = self.browser.find_element(By.ID, "id_tray")
        self.wait_for(lambda: self.assertTrue(tray.is_displayed()))

        self.browser.find_element(By.ID, "id_tray_btn").click()
        self.wait_for(lambda: self.assertFalse(tray.is_displayed()))

    # ------------------------------------------------------------------ #
    # Test T5 — tray reverts to closed on page reload                    #
    # ------------------------------------------------------------------ #

    def test_tray_reverts_to_closed_on_reload(self):
        room = self._make_sig_select_room()
        self.create_pre_authenticated_session("founder@test.io")
        room_url = self._room_url(room)
        self.browser.get(room_url)

        btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
        self._simulate_drag(btn, -300)

        tray = self.browser.find_element(By.ID, "id_tray")
        self.wait_for(lambda: self.assertTrue(tray.is_displayed()))

        # Reload — tray must start closed regardless of previous state
        self.browser.get(room_url)
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
        tray = self.browser.find_element(By.ID, "id_tray")
        self.assertFalse(tray.is_displayed())

    # ------------------------------------------------------------------ #
    # Test T6 — landscape: tray btn is near the top edge of the viewport #
    # ------------------------------------------------------------------ #

    @tag('two-browser')
    def test_tray_btn_anchored_near_top_in_landscape(self):
        room = self._make_sig_select_room()
        self._switch_to_landscape()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self._room_url(room))

        btn = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_tray_btn")
        )
        self.assertTrue(btn.is_displayed())

        # In landscape the handle sits at the top of the content area;
        # btn bottom should be within the top 40% of the viewport.
        vh = self.browser.execute_script("return window.innerHeight")
        btn_bottom = btn.location["y"] + btn.size["height"]
        self.assertLess(btn_bottom, vh * 0.4)

    # ------------------------------------------------------------------ #
    # Test T7 — landscape: dragging btn downward opens the tray          #
    # ------------------------------------------------------------------ #

    @tag('two-browser')
    def test_dragging_tray_btn_down_opens_tray_in_landscape(self):
        room = self._make_sig_select_room()
        self._switch_to_landscape()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self._room_url(room))

        btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
        # In landscape, #id_tray is always display:block; position controls visibility.
        # Use Tray.isOpen() to check logical state.
        self.assertFalse(self.browser.execute_script("return Tray.isOpen()"))

        self._simulate_drag_y(btn, 300)

        self.wait_for(
            lambda: self.assertTrue(self.browser.execute_script("return Tray.isOpen()"))
        )

    # ------------------------------------------------------------------ #
    # Test T8 — portrait: 1 column × 8 rows of square cells              #
    # ------------------------------------------------------------------ #

    @unittest.skip("portrait grid layout flaky in CI headless Firefox — revisit")
    @tag('two-browser')
    def test_tray_grid_is_1_column_by_8_rows_in_portrait(self):
        room = self._make_role_select_room()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self._room_url(room))

        btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
        self._simulate_drag(btn, -300)
        self.wait_for(
            lambda: self.assertTrue(
                self.browser.find_element(By.ID, "id_tray").is_displayed()
            )
        )

        cells = self.browser.find_elements(By.CSS_SELECTOR, "#id_tray_grid .tray-cell")
        self.assertEqual(len(cells), 8)

        # 8 explicit rows set via grid-template-rows
        row_count = self.browser.execute_script("""
            var s = getComputedStyle(document.getElementById('id_tray_grid'));
            return s.gridTemplateRows.trim().split(/\\s+/).length;
        """)
        self.assertEqual(row_count, 8)

        # All 8 cells share the same x position — one column only
        xs = {round(c.location['x']) for c in cells}
        self.assertEqual(len(xs), 1)

        # Cells are square
        cell = cells[0]
        self.assertAlmostEqual(cell.size['width'], cell.size['height'], delta=2)

    # ------------------------------------------------------------------ #
    # Test T9 — landscape: 8 columns × 1 row of square cells             #
    # ------------------------------------------------------------------ #
    # T9a — column/row count (structure)
    @unittest.skip("landscape grid layout flaky in CI headless Firefox — revisit")
    @tag('two-browser')
    def test_tray_grid_is_8_columns_by_1_row_in_landscape(self):
        room = self._make_sig_select_room()
        self._switch_to_landscape()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self._room_url(room))

        btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
        self._simulate_drag_y(btn, 300)
        self.wait_for(
            lambda: self.assertTrue(self.browser.execute_script("return Tray.isOpen()"))
        )

        cells = self.browser.find_elements(By.CSS_SELECTOR, "#id_tray_grid .tray-cell")
        self.assertEqual(len(cells), 8)

        # 8 explicit columns set via grid-template-columns
        col_count = self.browser.execute_script("""
            var s = getComputedStyle(document.getElementById('id_tray_grid'));
            return s.gridTemplateColumns.trim().split(/\\s+/).length;
        """)
        self.assertEqual(col_count, 8)

        # All 8 cells share the same y position — one row only
        ys = {round(c.location['y']) for c in cells}
        self.assertEqual(len(ys), 1)

        # Cells are square
        cell = cells[0]
        self.assertAlmostEqual(cell.size['width'], cell.size['height'], delta=2)

    # ------------------------------------------------------------------ #
    # Test T9b — landscape: all 8 cells visible within the tray interior #
    # ------------------------------------------------------------------ #

    @unittest.skip("landscape cell bounds flaky in CI headless Firefox — revisit with T9a")
    @tag('two-browser')
    def test_landscape_tray_all_8_cells_visible(self):
        room = self._make_sig_select_room()
        self._switch_to_landscape()
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(self._room_url(room))

        btn = self.wait_for(lambda: self.browser.find_element(By.ID, "id_tray_btn"))
        self._simulate_drag_y(btn, 300)
        self.wait_for(
            lambda: self.assertTrue(self.browser.execute_script("return Tray.isOpen()"))
        )

        tray = self.browser.find_element(By.ID, "id_tray")
        cells = self.browser.find_elements(By.CSS_SELECTOR, "#id_tray_grid .tray-cell")
        self.assertEqual(len(cells), 8)

        tray_right  = tray.location['x'] + tray.size['width']
        tray_bottom = tray.location['y'] + tray.size['height']

        # Each cell must fit within the tray interior (2px rounding slack)
        for cell in cells:
            self.assertLessEqual(
                cell.location['x'] + cell.size['width'], tray_right + 2,
                msg="Cell overflows tray right edge"
            )
            self.assertLessEqual(
                cell.location['y'] + cell.size['height'], tray_bottom + 2,
                msg="Cell overflows tray bottom edge"
            )


# ─────────────────────────────────────────────────────────────────────────────
#
# Tarot deck + Game Kit FTs — migrated from the legacy
# test_component_cards_tarot.py (2026-05-12). These exercise the in-room tarot
# deck page (Celtic Cross deal), the Game Kit deck-variant selection w. hover
# tooltips + Equip/Equipped state, and the dedicated game-kit page w. its
# four applet rows + tarot fan modal. The admin-side tarot browse FT split
# off into test_admin_tarot.py at the same time.
#
# ─────────────────────────────────────────────────────────────────────────────


class TarotDeckTest(FunctionalTest):
    """A room founder can view the tarot deck page and deal a Celtic Cross spread."""

    def setUp(self):
        super().setUp()
        # DeckVariant + TarotCard rows are flushed by TransactionTestCase — recreate
        from apps.epic.models import TarotCard
        self.earthman, _ = DeckVariant.objects.get_or_create(
            slug="earthman",
            defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
        )
        # Seed 8 major cards — enough for a 6-card cross deal (with buffer)
        major_stubs = [
            (0,  "The Schiz",         "the-schiz-ft"),
            (1,  "Pope I: President", "pope-i-president-ft"),
            (2,  "Pope II: Tsar",     "pope-ii-tsar-ft"),
            (3,  "Pope III: Chairman","pope-iii-chairman-ft"),
            (4,  "Pope IV: Emperor",  "pope-iv-emperor-ft"),
            (5,  "Pope V: Chancellor","pope-v-chancellor-ft"),
            (10, "Wheel of Fortune",  "wheel-of-fortune-em-ft"),
            (11, "The Junkboat",      "the-junkboat-ft"),
        ]
        for number, name, slug in major_stubs:
            TarotCard.objects.get_or_create(
                deck_variant=self.earthman, slug=slug,
                defaults={"name": name, "arcana": "MAJOR", "number": number},
            )
        self.founder = User.objects.create(email="founder@test.io")
        # Signal sets equipped_deck to Earthman (now it exists)
        self.founder.refresh_from_db()
        self.room = Room.objects.create(name="Whispering Pines", owner=self.founder)

    # ------------------------------------------------------------------ #
    # Test 2 — tarot deck page reports 108 cards (Earthman default)       #
    # ------------------------------------------------------------------ #

    def test_founder_can_reach_room_tarot_page_and_sees_full_deck(self):
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(
            self.live_server_url + f"/gameboard/room/{self.room.id}/tarot/"
        )

        # Browser tab title confirms we're on the tarot page
        self.wait_for(
            lambda: self.assertIn("Tarot", self.browser.title)
        )

        # Deck status shows all 108 Earthman cards remaining
        status = self.browser.find_element(By.CSS_SELECTOR, "[data-tarot-remaining]")
        self.assertEqual(status.get_attribute("data-tarot-remaining"), "108")

    # ------------------------------------------------------------------ #
    # Test 3 — dealing a Celtic Cross spread shows 10 positioned cards    #
    # ------------------------------------------------------------------ #

    def test_dealing_celtic_cross_spread_shows_ten_unique_cards(self):
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(
            self.live_server_url + f"/gameboard/room/{self.room.id}/tarot/"
        )

        # Click the "Deal Celtic Cross" button
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "[data-deal-spread]")
        ).click()

        # Six cross positions appear in the spread (staff positions filled via gameplay)
        positions = self.wait_for(
            lambda: self.browser.find_elements(By.CSS_SELECTOR, ".tarot-position")
        )
        self.assertEqual(len(positions), 6)

        # Each position shows a card name and an orientation label
        names = set()
        for pos in positions:
            name = pos.find_element(By.CSS_SELECTOR, ".tarot-card-name").text
            orientation = pos.find_element(By.CSS_SELECTOR, ".tarot-card-orientation").text
            self.assertTrue(len(name) > 0, "Card name should not be empty")
            self.assertIn(orientation, ["Upright", "Reversed"])
            names.add(name)

        # All 6 cards are unique
        self.assertEqual(len(names), 6, "All 6 drawn cards must be unique")

    # ------------------------------------------------------------------ #
    # Test 4 — deck count decreases after the spread is dealt             #
    # ------------------------------------------------------------------ #

    def test_remaining_count_decreases_after_dealing_spread(self):
        self.create_pre_authenticated_session("founder@test.io")
        self.browser.get(
            self.live_server_url + f"/gameboard/room/{self.room.id}/tarot/"
        )

        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "[data-deal-spread]")
        ).click()

        # After dealing 6 cross cards from the 108-card Earthman deck, 102 remain
        remaining = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "[data-tarot-remaining]")
        )
        self.assertEqual(remaining.get_attribute("data-tarot-remaining"), "102")


class GameKitDeckSelectionTest(FunctionalTest):
    """
    Game Kit applet on gameboard shows available deck variants with hover
    tooltips and an equip/equipped state — following the same mini-tooltip
    pattern as trinket selection.

    Test scenario: the gamer's active deck is explicitly set to Fiorentine
    (non-default) in setUp, so we can exercise switching back to Earthman.
    Once DeckVariant model exists, replace the TODO stubs with real ORM calls.
    """

    def setUp(self):
        super().setUp()
        for slug, name, cols, rows in [
            ("new-game",  "New Game",  6, 3),
            ("my-games",  "My Games",  6, 3),
            ("game-kit",  "Game Kit",  6, 3),
        ]:
            Applet.objects.get_or_create(
                slug=slug,
                defaults={
                    "name": name, "grid_cols": cols,
                    "grid_rows": rows, "context": "gameboard",
                },
            )
        # DeckVariant rows are flushed by TransactionTestCase — recreate before
        # creating the user so the post_save signal can set equipped_deck = earthman.
        self.earthman, _ = DeckVariant.objects.get_or_create(
            slug="earthman",
            defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
        )
        self.rws, _ = DeckVariant.objects.get_or_create(
            slug="tarot-rider-waite-smith",
            defaults={"name": "Tarot (Rider-Waite-Smith)", "card_count": 78, "is_default": False},
        )
        self.gamer = User.objects.create(email="gamer@deck.io")
        # Signal sets equipped_deck = earthman and unlocked_decks = [earthman].
        # Explicitly grant fiorentine too, then switch equipped_deck to it so
        # the test can exercise switching back to Earthman.
        self.gamer.refresh_from_db()
        self.gamer.unlocked_decks.add(self.rws)
        self.gamer.equipped_deck = self.rws
        self.gamer.save(update_fields=["equipped_deck"])

    # ------------------------------------------------------------------ #
    # Test 5 — Game Kit shows deck cards with correct equip/equipped state #
    # ------------------------------------------------------------------ #

    def test_game_kit_deck_cards_show_equip_state_and_switching_works(self):
        """
        Gamer (currently on Fiorentine) visits gameboard, hovers over the
        Earthman deck — sees it is NOT equipped. Hovers to Fiorentine — sees
        it IS equipped. Hovers back to Earthman and clicks Equip.
        """
        self.create_pre_authenticated_session("gamer@deck.io")
        self.browser.get(self.live_server_url + "/gameboard/")
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit"))

        # ── Hover over Earthman deck ──────────────────────────────────────
        earthman_el = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_kit_earthman_deck")
        )
        self.browser.execute_script(
            "arguments[0].scrollIntoView({block: 'center'})", earthman_el
        )
        ActionChains(self.browser).move_to_element(earthman_el).perform()

        # Main tooltip shows deck name and card count
        self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
        )
        portal = self.browser.find_element(By.ID, "id_tooltip_portal")
        self.assertIn("Earthman", portal.text)
        self.assertIn("108", portal.text)

        # Mini shows "Not Equipped"; DON button is active in the main portal
        mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
        self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
        self.assertIn("Not Equipped", mini.text)
        don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
        self.assertNotIn("btn-disabled", don.get_attribute("class"))

        # ── Hover over Tarot (Rider-Waite-Smith) deck ────────────────────
        rws_el = self.browser.find_element(By.ID, "id_kit_tarot_deck")
        self.browser.execute_script(
            "arguments[0].scrollIntoView({block: 'center'})", rws_el
        )
        ActionChains(self.browser).move_to_element(rws_el).perform()

        self.wait_for(
            lambda: self.assertIn(
                "Rider-Waite-Smith",
                self.browser.find_element(By.ID, "id_tooltip_portal").text,
            )
        )
        portal = self.browser.find_element(By.ID, "id_tooltip_portal")
        self.assertIn("78", portal.text)

        # Mini tooltip shows "Equipped" — RWS is the active deck
        mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
        self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
        self.assertIn("Equipped", mini.text)

        # ── Hover back to Earthman and click DON ─────────────────────────
        ActionChains(self.browser).move_to_element(earthman_el).perform()
        self.wait_for(
            lambda: self.assertIn(
                "Earthman",
                self.browser.find_element(By.ID, "id_tooltip_portal").text,
            )
        )
        portal = self.browser.find_element(By.ID, "id_tooltip_portal")
        mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
        self.wait_for(lambda: self.assertTrue(mini.is_displayed()))
        portal.find_element(By.CSS_SELECTOR, ".btn-equip").click()

        # DON becomes disabled; mini updates to "Equipped"; data attr set optimistically
        self.wait_for(
            lambda: self.assertIn(
                "btn-disabled",
                portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"),
            )
        )
        self.assertIn("Equipped", self.browser.find_element(By.ID, "id_mini_tooltip_portal").text)
        game_kit = self.browser.find_element(By.ID, "id_game_kit")
        self.assertNotEqual(game_kit.get_attribute("data-equipped-deck-id"), "")

    # ------------------------------------------------------------------ #
    # Test 6 — new user's Game Kit shows only the default Earthman deck  #
    # ------------------------------------------------------------------ #

    def test_new_user_game_kit_shows_only_earthman_deck(self):
        """A fresh user's game kit contains only the Earthman deck card;
        the Fiorentine deck is not visible because it has not been unlocked."""
        newcomer = User.objects.create(email="newcomer@deck.io")
        newcomer.unlocked_decks.add(self.earthman)
        self.create_pre_authenticated_session("newcomer@deck.io")
        self.browser.get(self.live_server_url + "/gameboard/")
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_game_kit"))

        deck_cards = self.browser.find_elements(By.CSS_SELECTOR, "#id_game_kit .deck-variant")
        self.assertEqual(len(deck_cards), 1)
        self.browser.find_element(By.ID, "id_kit_earthman_deck")
        rws_cards = self.browser.find_elements(By.ID, "id_kit_tarot_deck")
        self.assertEqual(len(rws_cards), 0)


class GameKitPageTest(FunctionalTest):
    """
    User navigates from gameboard to the dedicated game-kit page.
    The page shows four rows: trinkets, tokens, card decks, dice placeholder.
    Clicking a deck card opens a tarot fan modal with coverflow navigation.
    """

    def setUp(self):
        super().setUp()
        from apps.epic.models import TarotCard
        for slug, name, cols, rows in [
            ("new-game", "New Game", 6, 3),
            ("my-games", "My Games", 6, 3),
            ("game-kit", "Game Kit", 6, 3),
        ]:
            Applet.objects.get_or_create(
                slug=slug,
                defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "gameboard"},
            )
        for slug, name in [
            ("gk-trinkets", "Trinkets"),
            ("gk-tokens", "Tokens"),
            ("gk-decks", "Card Decks"),
            ("gk-dice", "Dice Sets"),
        ]:
            Applet.objects.get_or_create(
                slug=slug,
                defaults={"name": name, "grid_cols": 3, "grid_rows": 3, "context": "game-kit"},
            )
        self.earthman, _ = DeckVariant.objects.get_or_create(
            slug="earthman",
            defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
        )
        # Seed 10 cards — enough to demonstrate full 7-card coverflow
        for i in range(10):
            TarotCard.objects.get_or_create(
                deck_variant=self.earthman,
                slug=f"gkp-card-{i}",
                defaults={"name": f"Card {i}", "arcana": "MAJOR", "number": i},
            )
        # Create user after decks so signal sets equipped_deck + unlocked_decks
        self.gamer = User.objects.create(email="gamer@kit.io")
        self.gamer.refresh_from_db()
        self.create_pre_authenticated_session("gamer@kit.io")

    # ------------------------------------------------------------------ #
    # Test 7 — gameboard Game Kit heading links to dedicated page         #
    # ------------------------------------------------------------------ #

    def test_gameboard_game_kit_heading_links_to_game_kit_page(self):
        self.browser.get(self.live_server_url + "/gameboard/")
        link = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_applet_game_kit h2 a")
        )
        link.click()
        self.wait_for(lambda: self.assertIn("/gameboard/game-kit/", self.browser.current_url))

    # ------------------------------------------------------------------ #
    # Test 8 — game-kit page shows four rows                             #
    # ------------------------------------------------------------------ #

    def test_game_kit_page_shows_four_rows(self):
        self.browser.get(self.live_server_url + "/gameboard/game-kit/")
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_gk_trinkets"))
        self.browser.find_element(By.ID, "id_gk_tokens")
        self.browser.find_element(By.ID, "id_gk_decks")
        self.browser.find_element(By.ID, "id_gk_dice")

    # ------------------------------------------------------------------ #
    # Test 9 — clicking a deck card opens the tarot fan modal            #
    # ------------------------------------------------------------------ #

    def test_clicking_deck_opens_tarot_fan_modal(self):
        self.browser.get(self.live_server_url + "/gameboard/game-kit/")
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
        ).click()
        dialog = self.browser.find_element(By.ID, "id_tarot_fan_dialog")
        self.wait_for(lambda: self.assertTrue(dialog.is_displayed()))

    # ------------------------------------------------------------------ #
    # Test 10 — fan shows active center card plus receding cards         #
    # ------------------------------------------------------------------ #

    def test_fan_shows_active_card_and_receding_cards(self):
        self.browser.get(self.live_server_url + "/gameboard/game-kit/")
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
        ).click()
        self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active"))
        visible = self.browser.find_elements(
            By.CSS_SELECTOR, "#id_fan_content .fan-card:not([style*='display: none'])"
        )
        self.assertGreater(len(visible), 1)

    # ------------------------------------------------------------------ #
    # Test 11 — clicking outside the modal closes it                    #
    # ------------------------------------------------------------------ #

    def test_pressing_escape_closes_fan_modal(self):
        from selenium.webdriver.common.keys import Keys
        self.browser.get(self.live_server_url + "/gameboard/game-kit/")
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
        ).click()
        dialog = self.browser.find_element(By.ID, "id_tarot_fan_dialog")
        self.wait_for(lambda: self.assertTrue(dialog.is_displayed()))
        dialog.send_keys(Keys.ESCAPE)
        self.wait_for(lambda: self.assertFalse(dialog.is_displayed()))
