from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By

from .base import FunctionalTest
from apps.applets.models import Applet
from apps.epic.models import DeckVariant
from apps.lyric.models import ShopItem, Token, User


class WalletDisplayTest(FunctionalTest):
    def setUp(self):
        super().setUp()
        Applet.objects.get_or_create(slug="wallet", defaults={"name": "Wallet"})
        for slug, name, cols, rows, order in [
            ("wallet-shop",     "Shop",           12, 3,  10),
            ("wallet-balances", "Wallet Balances", 3, 3, 100),
            ("wallet-tokens",   "Wallet Tokens",   3, 3, 100),
            ("wallet-payment",  "Payment Methods", 6, 3, 100),
        ]:
            Applet.objects.update_or_create(
                slug=slug,
                defaults={
                    "name": name, "grid_cols": cols, "grid_rows": rows,
                    "context": "wallet", "display_order": order,
                },
            )
        # Seed the 3 starting ShopItems — migration `lyric/0009_seed_shop_items`
        # populates these in fresh DBs, but FTs run on `TransactionTestCase`
        # which wipes all data between tests (post-migrate signal pollution
        # doesn't restore RunPython-seeded rows). Mirror the migration's
        # locked row shapes here.
        ShopItem.objects.update_or_create(
            slug="tithe-1",
            defaults={
                "name": "Tithe Token", "description": "1 Tithe Token + 12 Writs",
                "icon": "fa-piggy-bank", "badge_text": "",
                "price_cents": 100, "granted_token_type": Token.TITHE,
                "granted_count": 1, "granted_writs": 12,
                "max_owned": None, "display_order": 10, "active": True,
            },
        )
        ShopItem.objects.update_or_create(
            slug="tithe-5",
            defaults={
                "name": "Tithe Bundle", "description": "5 Tithe Tokens + 60 Writs",
                "icon": "fa-piggy-bank", "badge_text": "×5",
                "price_cents": 400, "granted_token_type": Token.TITHE,
                "granted_count": 5, "granted_writs": 60,
                "max_owned": None, "display_order": 20, "active": True,
            },
        )
        ShopItem.objects.update_or_create(
            slug="band-1",
            defaults={
                "name": "Wristband", "description": "Admit All Entry",
                "shoptalk": "Unlimited free entry (BYOB)",
                "icon": "fa-ring", "badge_text": "",
                "price_cents": 2000, "granted_token_type": Token.BAND,
                "granted_count": 1, "granted_writs": 0,
                "max_owned": 1, "display_order": 30, "active": True,
            },
        )

    def test_new_user_wallet_shows_starting_balances(self):
        # 1. Log in as new user
        self.create_pre_authenticated_session("capman@test.io")
        # 2. Navigate to dashboard
        self.browser.get(self.live_server_url)
        # 3. Find wallet applet summary card
        self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_applet_wallet")
        )
        # 4. Click thru to full wallet page via manage link
        self.browser.find_element(
            By.CSS_SELECTOR, "#id_applet_wallet a.wallet-manage-link"
        ).click()
        # 5. Assert user landed on wallet page
        self.wait_for(
            lambda: self.assertRegex(self.browser.current_url, r"/dashboard/wallet/$")
        )
        # 6. Assert writs balance shows 144 (complimentary bundle on signup)
        self.wait_for(
            lambda: self.assertEqual(
                "144",
                self.browser.find_element(By.ID, "id_writs_balance").text,
            )
        )
        # 7. Assert esteem balance shows 0
        self.assertEqual(
            "0",
            self.browser.find_element(By.ID, "id_esteem_balance").text,
        )
        # 8. Assert Coin-on-a-String token element present
        coin = self.browser.find_element(By.ID, "id_coin_on_a_string")
        # 9. Hover over it; assert tooltip appears w. name, entry text, 'no expiry'
        ActionChains(self.browser).move_to_element(coin).perform()
        self.wait_for(
            lambda: self.assertTrue(
                self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
            )
        )
        coin_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
        self.assertIn("Coin-on-a-String", coin_tooltip)
        self.assertIn("Admit 1 Entry", coin_tooltip)
        self.assertIn("no expiry", coin_tooltip)
        # 10. Assert ×1 Free Token present (complimentary on signup)
        free_token = self.browser.find_element(By.ID, "id_free_token")
        # 11. Hover over it; assert tooltip shows name, entry text, expiry date
        ActionChains(self.browser).move_to_element(free_token).perform()
        self.wait_for(
            lambda: self.assertTrue(
                self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
            )
        )
        free_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
        self.assertIn("Free Token", free_tooltip)
        self.assertIn("Admit 1 Entry", free_tooltip)
        self.assertIn("Expires", free_tooltip)

    def test_wallet_payment_section_renders(self):
        # 1. Log in, navigate directly to wallet page
        self.create_pre_authenticated_session("capman@test.io")
        self.browser.get(self.live_server_url + "/dashboard/wallet/")
        # 2. Assert saved payment methods section present
        self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_payment_methods")
        )
        # 3. Assert the add-payment-method button is visible
        self.browser.find_element(By.ID, "id_add_payment_method")
        # 4. Assert Stripe Payment Element mount point exists
        self.browser.find_element(By.ID, "id_stripe_payment_element")

    def test_user_can_save_a_payment_method(self):
        # 1. Log in, navigate to wallet page
        self.create_pre_authenticated_session("capman@test.io")
        self.browser.get(self.live_server_url + "/dashboard/wallet/")
        # 2. Click add-payment-method btn
        self.browser.find_element(By.ID, "id_add_payment_method").click()
        # 3. Wait for Stripe Payment Element iframe to appear inside mount point
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, "#id_stripe_payment_element iframe"
            )
        )
        # 4. Switch into Stripe iframe to interact w. card fields
        stripe_frame = self.browser.find_element(
            By.CSS_SELECTOR, "#id_stripe_payment_element iframe"
        )
        self.browser.switch_to.frame(stripe_frame)
        # 4a. Wait for card inputs to render inside iframe
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, 'input[placeholder="1234 1234 1234 1234"]'
            )
        )
        # 5. Fill in Stripe test card details
        self.browser.find_element(
            By.CSS_SELECTOR, 'input[placeholder="1234 1234 1234 1234"]'
        ).send_keys("4242424242424242")
        self.browser.find_element(
            By.CSS_SELECTOR, 'input[placeholder="MM / YY"]'
        ).send_keys("12 / 26")
        self.browser.find_element(
            By.CSS_SELECTOR, 'input[placeholder="CVC"]'
        ).send_keys("424")
        self.browser.find_element(
            By.CSS_SELECTOR, 'input[placeholder="12345"]'
        ).send_keys("42424")
        # 6. Return to main doc & submit form
        self.browser.switch_to.default_content()
        self.wait_for(
            lambda: self.assertFalse(
                self.browser.find_element(By.ID, "id_save_payment_method").get_attribute("hidden")
            )
        )
        self.browser.find_element(By.ID, "id_save_payment_method").click()
        # 7. Wait for saved card to appear in payment methods list
        # Assert last 4 digits shown (Stripe confirmSetup + server round-trip can be slow)
        self.wait_for_slow(
            lambda: self.assertIn(
                "4242",
                self.browser.find_element(By.ID, "id_payment_methods").text,
            )
        )

    def test_user_can_cancel_adding_payment_method(self):
        # 1. Log in, navigate to wallet page
        self.create_pre_authenticated_session("capman@test.io")
        self.browser.get(self.live_server_url + "/dashboard/wallet/")
        # 2. Click Add Payment Method
        self.wait_for_slow(
            lambda: self.browser.find_element(By.ID, "id_add_payment_method")
        ).click()
        # 3. Wait for Cancel button to appear (visible after setup-intent fetch returns)
        self.wait_for_slow(
            lambda: self.assertFalse(
                self.browser.find_element(By.ID, "id_cancel_payment_method").get_attribute("hidden")
            )
        )
        # 3a. Assert applet expanded to 15 rows
        rows = self.browser.execute_script(
            "return document.getElementById('id_payment_methods')"
            ".style.getPropertyValue('--applet-rows').trim()"
        )
        self.assertEqual(rows, '15')
        # 4. Click Cancel
        self.browser.find_element(By.ID, "id_cancel_payment_method").click()
        # 5. Assert Cancel + Save buttons are hidden again
        self.wait_for_slow(
            lambda: self.assertTrue(
                self.browser.find_element(By.ID, "id_cancel_payment_method").get_attribute("hidden")
            )
        )
        self.assertTrue(
            self.browser.find_element(By.ID, "id_save_payment_method").get_attribute("hidden")
        )
        # 6. Assert applet collapses back to 3 grid rows
        rows = self.browser.execute_script(
            "return document.getElementById('id_payment_methods')"
            ".style.getPropertyValue('--applet-rows').trim()"
        )
        self.assertEqual(rows, '3')

    def test_wallet_tokens_applet_shows_all_owned_trinket_types(self):
        """Wallet Tokens applet renders every owned trinket-as-token type
        independently — PASS (staff), BAND, COIN, CARTE — alongside FREE
        + TITHE. Pre-2026-05-22 the template had an if/elif chain that
        only showed ONE of {PASS, BAND, COIN}; that hid BAND + COIN +
        CARTE entirely from the wallet for any staff user holding a PASS,
        even though all three are usable at the gate. Chunk 1 of the
        Shop applet rollout drops that exclusivity so each trinket gets
        its own tooltipped icon in the row."""
        # 1. Build a staff user holding every trinket-as-token type
        staff = User.objects.create(email="ledger@test.io", is_staff=True)
        # post_save signal already created COIN (auto-equipped) + FREE + PASS;
        # mint the rest manually so we exercise the full inventory render.
        Token.objects.create(user=staff, token_type=Token.BAND)
        Token.objects.create(user=staff, token_type=Token.CARTE)
        Token.objects.create(user=staff, token_type=Token.TITHE)
        # 2. Log in + land on wallet page
        self.create_pre_authenticated_session("ledger@test.io")
        self.browser.get(self.live_server_url + "/dashboard/wallet/")
        # 3. Every trinket-as-token icon is present (no if/elif suppression)
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_pass_token"))
        self.browser.find_element(By.ID, "id_band_token")
        self.browser.find_element(By.ID, "id_coin_on_a_string")
        self.browser.find_element(By.ID, "id_carte_token")
        # 4. Consumable tokens still present
        self.browser.find_element(By.ID, "id_free_token")
        self.browser.find_element(By.ID, "id_tithe_token")
        # 5. BAND tile carries its tooltip content in the DOM (the wallet's
        # `initWalletTooltips` clones `.tt` innerHTML into the portal on
        # hover — already exercised by the COIN/FREE hover paths in
        # `test_new_user_wallet_shows_starting_balances`; here we just pin
        # that the new BAND/CARTE template blocks server-render their full
        # tooltip prose, which is the Chunk 1 contract).
        band_tt = self.browser.find_element(By.CSS_SELECTOR, "#id_band_token .tt").get_attribute("innerHTML")
        self.assertIn("Wristband", band_tt)
        self.assertIn("Admit All Entry", band_tt)
        self.assertIn("no expiry", band_tt)
        # 6. CARTE tile carries its tooltip content too
        carte_tt = self.browser.find_element(By.CSS_SELECTOR, "#id_carte_token .tt").get_attribute("innerHTML")
        self.assertIn("Carte Blanche", carte_tt)
        self.assertIn("Admit up to +6", carte_tt)
        self.assertIn("no expiry", carte_tt)


    def test_shop_applet_renders_seeded_items_with_icons_and_badges(self):
        """Chunk 4 of [[project-wallet-shop-expansion]] — the new Shop
        applet renders the 3 seeded items (`tithe-1`, `tithe-5`, `band-1`)
        as tooltipped tiles. The bundle (`tithe-5`) carries a `×5` badge;
        the single-tithe + band tiles render without a badge.

        Pinned visual contract: tile presence (id), icon class, badge text,
        and price text inside the tooltip prose."""
        self.create_pre_authenticated_session("capman@test.io")
        self.browser.get(self.live_server_url + "/dashboard/wallet/")
        # 1. Shop applet is present
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_wallet_shop"))
        # 2. Each of the 3 seeded items renders w. its own tile
        tithe1 = self.browser.find_element(By.ID, "id_shop_tithe-1")
        tithe5 = self.browser.find_element(By.ID, "id_shop_tithe-5")
        band1 = self.browser.find_element(By.ID, "id_shop_band-1")
        # 3. Icons are correct
        self.assertIn("fa-piggy-bank", tithe1.find_element(By.CSS_SELECTOR, "i").get_attribute("class"))
        self.assertIn("fa-piggy-bank", tithe5.find_element(By.CSS_SELECTOR, "i").get_attribute("class"))
        self.assertIn("fa-ring",       band1.find_element(By.CSS_SELECTOR, "i").get_attribute("class"))
        # 4. Bundle carries the ×5 badge; singles don't
        self.assertIn("×5", tithe5.find_element(By.CSS_SELECTOR, ".shop-badge").text)
        self.assertEqual(tithe1.find_elements(By.CSS_SELECTOR, ".shop-badge"), [])
        self.assertEqual(band1.find_elements(By.CSS_SELECTOR, ".shop-badge"), [])
        # 5. Tooltip prose includes name + price (read .tt innerHTML directly
        #    — hover→portal cloning is already exercised by the existing
        #    COIN/FREE hover tests).
        tithe1_tt = tithe1.find_element(By.CSS_SELECTOR, ".tt").get_attribute("innerHTML")
        self.assertIn("Tithe Token", tithe1_tt)
        self.assertIn("$1", tithe1_tt)
        band1_tt = band1.find_element(By.CSS_SELECTOR, ".tt").get_attribute("innerHTML")
        self.assertIn("Wristband", band1_tt)
        self.assertIn("$20", band1_tt)

    def test_shop_buy_click_opens_guard_portal_with_purchase_prompt(self):
        """BUY ITEM click opens `#id_guard_portal` w. an 'Are you sure?'-style
        prompt naming the item + price. Click NVM dismisses; click OK
        triggers the Stripe.js dance (mocked out / deferred to a follow-up
        FT)."""
        self.create_pre_authenticated_session("capman@test.io")
        self.browser.get(self.live_server_url + "/dashboard/wallet/")
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_wallet_shop"))
        # 1. Find BUY ITEM btn inside the tithe-1 tile's microtooltip
        buy_btn = self.browser.find_element(
            By.CSS_SELECTOR, "#id_shop_tithe-1 .tt-buy-btn"
        )
        # 2. Click it via JS (microtooltip lives in the portal layer + may
        #    not be hover-visible during Selenium's strict-target check)
        self.browser.execute_script("arguments[0].click();", buy_btn)
        # 3. Guard portal opens w. a prompt mentioning Tithe + $1
        portal = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_guard_portal")
        )
        self.wait_for(
            lambda: self.assertIn("active", portal.get_attribute("class"))
        )
        msg = portal.find_element(By.CSS_SELECTOR, ".guard-message").text
        self.assertIn("Tithe", msg)
        self.assertIn("$1", msg)
        # 4. NVM dismisses
        portal.find_element(By.CSS_SELECTOR, ".guard-no").click()
        self.wait_for(
            lambda: self.assertNotIn("active", portal.get_attribute("class"))
        )

    def test_shop_buy_guard_portal_pins_item_tooltip(self):
        """While the BUY-ITEM guard portal is open, the item's main +
        mini tooltip stay pinned — they don't dismiss when the cursor
        leaves the BUY btn area to reach the guard's OK / NVM. Fixes
        the orphan-prompt UX where "Buy {item} for ${price}?" floated
        on its own w. no visual referent. Pinning is released on
        either confirm OR dismiss; both schedule the normal hide."""
        import time
        self.create_pre_authenticated_session("capman@test.io")
        self.browser.get(self.live_server_url + "/dashboard/wallet/")
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_wallet_shop"))
        # 1. Programmatically trigger the tile-enter → both portals active.
        self.browser.execute_script(
            "document.getElementById('id_shop_tithe-5').dispatchEvent("
            "new MouseEvent('mouseenter', {bubbles: true}));"
        )
        self.wait_for(
            lambda: self.assertIn(
                "active",
                self.browser.find_element(By.ID, "id_tooltip_portal").get_attribute("class"),
            )
        )
        self.assertIn(
            "active",
            self.browser.find_element(By.ID, "id_mini_tooltip_portal").get_attribute("class"),
        )
        # 2. Click the cloned BUY btn inside the mini portal — opens guard.
        self.browser.execute_script(
            "document.querySelector('#id_mini_tooltip_portal .tt-buy-btn').click();"
        )
        guard = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_guard_portal")
        )
        self.wait_for(
            lambda: self.assertIn("active", guard.get_attribute("class"))
        )
        # 3. Simulate cursor leaving both portals to reach the guard btns.
        self.browser.execute_script("""
            document.getElementById('id_mini_tooltip_portal').dispatchEvent(
                new MouseEvent('mouseleave', {bubbles: true}));
            document.getElementById('id_tooltip_portal').dispatchEvent(
                new MouseEvent('mouseleave', {bubbles: true}));
        """)
        # 4. Wait longer than the wallet.js hide delay (200ms) + buffer.
        time.sleep(0.4)
        # 5. Both portals are STILL active — pinned by the open guard.
        self.assertIn(
            "active",
            self.browser.find_element(By.ID, "id_tooltip_portal").get_attribute("class"),
        )
        self.assertIn(
            "active",
            self.browser.find_element(By.ID, "id_mini_tooltip_portal").get_attribute("class"),
        )
        # 6. Click NVM — guard dismisses, pin releases.
        guard.find_element(By.CSS_SELECTOR, ".guard-no").click()
        self.wait_for(
            lambda: self.assertNotIn("active", guard.get_attribute("class"))
        )
        # 7. After unpin + hide delay, portals fade out.
        time.sleep(0.4)
        self.assertNotIn(
            "active",
            self.browser.find_element(By.ID, "id_tooltip_portal").get_attribute("class"),
        )
        self.assertNotIn(
            "active",
            self.browser.find_element(By.ID, "id_mini_tooltip_portal").get_attribute("class"),
        )


    def test_shop_band_already_owned_shows_disabled_buy_btn(self):
        """User who already owns 1 BAND (`max_owned=1`) sees the band-1
        tile w. its BUY btn disabled + an 'Already owned' microtooltip
        — visible-but-unbuyable per the decision in [[project-wallet-
        shop-expansion]]."""
        owner = User.objects.create(email="bander@test.io")
        Token.objects.create(user=owner, token_type=Token.BAND)
        self.create_pre_authenticated_session("bander@test.io")
        self.browser.get(self.live_server_url + "/dashboard/wallet/")
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_wallet_shop"))
        band_tile = self.browser.find_element(By.ID, "id_shop_band-1")
        # Capped item — NO buy btn at all (parity w. Game Kit's status-
        # only "Equipped" / "In-Use: X" pills, which never pair status
        # text w. a disabled action btn).
        self.assertEqual(band_tile.find_elements(By.CSS_SELECTOR, ".tt-buy-btn"), [])
        # 'Already owned' microtext lives in the `.tt-micro` sibling-of-.tt
        # (cloned into `#id_mini_tooltip_portal` on hover by wallet.js).
        micro_html = band_tile.find_element(By.CSS_SELECTOR, ".tt-micro").get_attribute("innerHTML")
        self.assertIn("Already owned", micro_html)


    # Legacy `test_user_can_purchase_tithe_token_bundle` FT (asserting
    # `#id_tithe_token_shop` inside Balances) was removed in Chunk 5 of
    # [[project-wallet-shop-expansion]] — the tithe purchase surface
    # moved to the dedicated Shop applet. Coverage now lives in:
    #   - `test_shop_applet_renders_seeded_items_with_icons_and_badges`
    #     (tile + icon + badge + price)
    #   - `test_shop_buy_click_opens_guard_portal_with_purchase_prompt`
    #     (BUY → guard portal → NVM dismisses)
    #   - `test_shop_band_already_owned_shows_disabled_buy_btn`
    #     (max_owned cap renders BUY as `.btn-disabled` w. microtext)


class WalletShopFreeDeckTest(FunctionalTest):
    """Free decks (RWS + Minchiate Fiorentine) appear in the Shop applet at
    $0 with a FREE ITEM btn (`.tt-free-btn`) reusing the deck's own tooltip.
    Claiming one swaps the btn for an 'Already owned' pill + unlocks the deck
    so it renders in the Game Kit applet on the gameboard."""

    def setUp(self):
        super().setUp()
        # Wallet applets — Shop lives here. TransactionTestCase flushes the
        # migration-seeded Applet rows, so re-seed up front.
        Applet.objects.get_or_create(slug="wallet", defaults={"name": "Wallet"})
        Applet.objects.update_or_create(
            slug="wallet-shop",
            defaults={"name": "Shop", "grid_cols": 12, "grid_rows": 3,
                      "context": "wallet", "display_order": 10},
        )
        # Gameboard applets — the claim's end state is the deck showing up in
        # Game Kit on /gameboard/.
        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"})
        Applet.objects.get_or_create(slug="game-kit", defaults={"name": "Game Kit", "context": "gameboard"})
        # The two free-in-shop decks (mirror the seed-migration row shapes).
        DeckVariant.objects.update_or_create(
            slug="tarot-rider-waite-smith",
            defaults={"name": "Tarot (Rider-Waite-Smith)", "card_count": 78,
                      "family": "english", "has_card_images": False,
                      "is_polarized": False, "free_in_shop": True},
        )
        DeckVariant.objects.update_or_create(
            slug="minchiate-fiorentine-1860-1890",
            defaults={"name": "Minchiate Fiorentine (1860–1890)", "card_count": 97,
                      "family": "italian", "has_card_images": True,
                      "is_polarized": False, "free_in_shop": True,
                      "description": "97-card Minchiate Fiorentine deck."},
        )

    def test_claiming_free_deck_swaps_to_owned_and_appears_in_game_kit(self):
        self.create_pre_authenticated_session("decker@test.io")
        self.browser.get(self.live_server_url + "/dashboard/wallet/")
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_wallet_shop"))
        # 1. RWS free-deck tile carries the deck's own tooltip + a $0 price
        #    + a FREE ITEM btn (and NO BUY btn — it's free).
        tile = self.browser.find_element(By.ID, "id_shop_tarot-rider-waite-smith")
        tt_html = tile.find_element(By.CSS_SELECTOR, ".tt").get_attribute("innerHTML")
        self.assertIn("Tarot (Rider-Waite-Smith)", tt_html)
        self.assertIn("78-card Tarot deck", tt_html)
        self.assertIn("$0", tt_html)
        free_btn = tile.find_element(By.CSS_SELECTOR, ".tt-free-btn")
        # `.text` is '' for the hover-hidden microtooltip btn — read textContent.
        self.assertEqual(
            free_btn.get_attribute("textContent").strip().upper(), "FREE ITEM"
        )
        self.assertEqual(tile.find_elements(By.CSS_SELECTOR, ".tt-buy-btn"), [])
        # 2. Claim it (JS-click — the microtooltip lives in the portal layer
        #    + may not be hover-visible under Selenium's strict-target check).
        self.browser.execute_script("arguments[0].click();", free_btn)
        # 3. The tile now shows 'Already owned' instead of the FREE ITEM btn.
        self.wait_for(lambda: self.assertIn(
            "Already owned",
            self.browser.find_element(
                By.CSS_SELECTOR, "#id_shop_tarot-rider-waite-smith .tt-micro"
            ).get_attribute("innerHTML"),
        ))
        self.assertEqual(
            self.browser.find_elements(
                By.CSS_SELECTOR, "#id_shop_tarot-rider-waite-smith .tt-free-btn"
            ), [],
        )
        # 4. The claimed deck now appears in the Game Kit applet — short_key
        #    of `tarot-rider-waite-smith` is `tarot` → `id_kit_tarot_deck`.
        self.browser.get(self.live_server_url + "/gameboard/")
        self.wait_for(lambda: self.browser.find_element(By.ID, "id_kit_tarot_deck"))
