"""FTs for the Game Sign (a.k.a. My Significator) picker + billboard applet.

Sprint 4a of [[project-my-sea-roadmap]]. The picker lives at
`/billboard/my-sign/` — solo lift of the room's sig-select grid (no
countdown / polarity / multi-user). Selection persists on
User.significator + User.significator_reversed. "Significator" remains
the storage-layer term + room sig-select context; this billboard surface
is branded "Sign" / "Game Sign".
"""
from selenium.webdriver.common.by import By

from .base import FunctionalTest
from .sig_page import _assign_sig, _seed_earthman_sig_pile, _seed_minchiate_image_fixtures
from apps.applets.models import Applet
from apps.epic.models import TarotCard, personal_sig_cards
from apps.lyric.models import User


def _seed_my_sign_applet():
    Applet.objects.get_or_create(
        slug="my-sign",
        defaults={"name": "My Sign", "context": "billboard",
                  "default_visible": True, "grid_cols": 4, "grid_rows": 6},
    )


class MySignPickerTest(FunctionalTest):
    """Happy-path picker: a user with the Earthman deck equipped lands at
    /billboard/my-sign/, picks a card, clicks SAVE SIGN, and sees the sig
    propagate to the Game Sign applet on /billboard/."""

    def setUp(self):
        super().setUp()
        _seed_earthman_sig_pile()
        _seed_my_sign_applet()
        # Seed the rest of the billboard applets so /billboard/ renders
        # without missing-applet errors.
        for slug, name in [
            ("my-scrolls", "My Scrolls"),
            ("my-buds", "My Buds"),
            ("most-recent-scroll", "Most Recent Scroll"),
        ]:
            Applet.objects.get_or_create(
                slug=slug, defaults={"name": name, "context": "billboard"},
            )
        self.email = "sig@test.io"
        self.gamer = User.objects.create(email=self.email)
        # post_save signal auto-equips Earthman. Picker uses personal_sig_cards
        # (= 16 middle arcana + Major 0 & 1, filtered by Note unlocks) so the
        # target must come from that subset, not the full deck.
        sig_pile = personal_sig_cards(self.gamer)
        self.target_card = sig_pile[0] if sig_pile else None
        self.assertIsNotNone(
            self.target_card,
            "personal_sig_cards(user) returned no cards — check Earthman seed"
            " + DeckVariant fixture availability in the FT DB.",
        )

    # ── Test 1 ───────────────────────────────────────────────────────────────

    def test_landing_renders_dry_hex_with_scan_sign_button(self):
        """GET /billboard/my-sign/ → the DRY table hex (1-chair) renders w.
        a central .btn-primary reading "SCAN SIGN"; the card grid is hidden
        until SCAN SIGN is clicked."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        self.wait_for(
            lambda: self.assertIn(
                "GAMESIGN",
                "".join(
                    self.browser.find_element(By.CSS_SELECTOR, "h2").text.upper().split()
                ),
            )
        )
        # Landing phase markers: hex + 1 chair + SCAN SIGN btn
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".my-sign-page .table-hex"
            )
        )
        chairs = self.browser.find_elements(
            By.CSS_SELECTOR, ".my-sign-page .table-seat"
        )
        self.assertEqual(len(chairs), 1, "Landing hex should render exactly 1 chair")
        scan_btn = self.browser.find_element(By.ID, "id_scan_sign_btn")
        self.assertIn("SCAN", scan_btn.text.upper())
        self.assertIn("SIGN", scan_btn.text.upper())
        # Picker grid not yet visible — landing phase only
        grid = self.browser.find_element(By.CSS_SELECTOR, ".my-sign-deck-grid")
        self.assertFalse(
            grid.is_displayed(),
            "Picker grid should be hidden on landing phase",
        )

    # ── Test 2 ───────────────────────────────────────────────────────────────

    def test_scan_sign_click_transitions_to_picker_phase(self):
        """Click SCAN SIGN → landing hex hides; picker grid + populated stage
        frame appear. Target card present in the grid."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        scan_btn = self.wait_for(
            lambda: self.browser.find_element(By.ID, "id_scan_sign_btn")
        )
        self.browser.execute_script("arguments[0].click()", scan_btn)
        # Phase swap — picker now visible, hex now hidden
        self.wait_for(
            lambda: self.assertTrue(
                self.browser.find_element(
                    By.CSS_SELECTOR, ".my-sign-deck-grid"
                ).is_displayed(),
                "Picker grid should be visible after SCAN SIGN click",
            )
        )
        # Hex collapses (display:none) after transition
        self.assertFalse(
            self.browser.find_element(
                By.CSS_SELECTOR, ".my-sign-page .table-hex"
            ).is_displayed(),
            "Landing hex should hide once picker phase is active",
        )
        # Target card present in the grid
        self.browser.find_element(
            By.CSS_SELECTOR,
            f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]',
        )

    # ── Test 3 ───────────────────────────────────────────────────────────────

    def test_click_thumbnail_shows_OK_btn_without_locking(self):
        """In picker phase: click a thumbnail → .sig-focused class + OK btn
        visible on it; stat block + FLIP still hidden (no lock yet); SAVE
        SIGN still disabled."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        self.browser.execute_script(
            "document.getElementById('id_scan_sign_btn').click()"
        )
        card_el = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR,
                f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]',
            )
        )
        self.browser.execute_script("arguments[0].click()", card_el)
        # .sig-focused class → OK btn visible
        self.wait_for(
            lambda: self.assertIn(
                "sig-focused",
                self.browser.find_element(
                    By.CSS_SELECTOR,
                    f'.sig-card[data-card-id="{self.target_card.id}"]',
                ).get_attribute("class"),
            )
        )
        ok_btn = self.browser.find_element(
            By.CSS_SELECTOR,
            f'.sig-card[data-card-id="{self.target_card.id}"] .sig-ok-btn',
        )
        self.assertTrue(ok_btn.is_displayed(), "OK btn should be visible on focused thumb")
        # Stat block still hidden (no lock yet)
        stage = self.browser.find_element(By.CSS_SELECTOR, ".my-sign-stage")
        self.assertNotIn("sig-stage--frozen", stage.get_attribute("class"))
        # SAVE SIGN still disabled
        save_btn = self.browser.find_element(By.ID, "id_save_sign_btn")
        self.assertTrue(save_btn.get_attribute("disabled"))

    # ── Test 4 ───────────────────────────────────────────────────────────────

    def test_OK_click_locks_thumbnail_and_enables_save_sign(self):
        """In picker phase: click thumbnail → click its OK btn → stage locks
        (.sig-stage--frozen), NVM btn visible on the thumbnail, SAVE SIGN
        enables. Then save persists + applet shows the card."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        self.browser.execute_script(
            "document.getElementById('id_scan_sign_btn').click()"
        )
        card_el = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR,
                f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]',
            )
        )
        self.browser.execute_script("arguments[0].click()", card_el)
        ok_btn = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR,
                f'.sig-card[data-card-id="{self.target_card.id}"] .sig-ok-btn',
            )
        )
        self.browser.execute_script("arguments[0].click()", ok_btn)
        # Lock applied
        self.wait_for(
            lambda: self.assertIn(
                "sig-stage--frozen",
                self.browser.find_element(
                    By.CSS_SELECTOR, ".my-sign-stage"
                ).get_attribute("class"),
            )
        )
        # NVM btn on the same thumbnail now visible
        nvm_btn = self.browser.find_element(
            By.CSS_SELECTOR,
            f'.sig-card[data-card-id="{self.target_card.id}"] .sig-nvm-btn',
        )
        self.assertTrue(nvm_btn.is_displayed(), "NVM btn should be visible after OK confirm")
        # SAVE SIGN enabled
        save_btn = self.browser.find_element(By.ID, "id_save_sign_btn")
        self.assertFalse(save_btn.get_attribute("disabled"))
        # Save persists
        self.browser.execute_script("arguments[0].click()", save_btn)
        self.wait_for(
            lambda: self.gamer.refresh_from_db() or self.assertEqual(
                self.gamer.significator_id, self.target_card.id,
            )
        )
        # Applet on /billboard/ shows the saved card
        self.browser.get(self.live_server_url + "/billboard/")
        self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR,
                f'#id_applet_my_sign .my-sign-applet-card[data-card-id="{self.target_card.id}"]',
            )
        )

    # ── Test 5 ───────────────────────────────────────────────────────────────

    def test_NVM_click_deselects_and_disables_save_sign(self):
        """After OK-lock, click NVM on the thumbnail → lock clears (no
        .sig-stage--frozen), .sig-focused removed, SAVE SIGN disables."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        self.browser.execute_script(
            "document.getElementById('id_scan_sign_btn').click()"
        )
        card_el = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR,
                f'.my-sign-deck-grid .sig-card[data-card-id="{self.target_card.id}"]',
            )
        )
        self.browser.execute_script("arguments[0].click()", card_el)
        ok_btn = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR,
                f'.sig-card[data-card-id="{self.target_card.id}"] .sig-ok-btn',
            )
        )
        self.browser.execute_script("arguments[0].click()", ok_btn)
        # Now NVM
        nvm_btn = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR,
                f'.sig-card[data-card-id="{self.target_card.id}"] .sig-nvm-btn',
            )
        )
        self.browser.execute_script("arguments[0].click()", nvm_btn)
        # Lock cleared
        self.wait_for(
            lambda: self.assertNotIn(
                "sig-stage--frozen",
                self.browser.find_element(
                    By.CSS_SELECTOR, ".my-sign-stage"
                ).get_attribute("class"),
            )
        )
        # SAVE SIGN disables again
        save_btn = self.browser.find_element(By.ID, "id_save_sign_btn")
        self.assertTrue(save_btn.get_attribute("disabled"))
        # .sig-focused removed from thumb
        self.assertNotIn(
            "sig-focused",
            self.browser.find_element(
                By.CSS_SELECTOR,
                f'.sig-card[data-card-id="{self.target_card.id}"]',
            ).get_attribute("class"),
        )

    # ── Test 6 ───────────────────────────────────────────────────────────────

    def test_landing_previews_saved_sig_on_stage(self):
        """If user already has a saved significator, the landing collapses to
        a read-only stage: the saved card preview + its stat block (no SCAN
        SIGN btn, no hex — user must DEL the saved sig to re-enter picker)."""
        # Pre-save a sig in the DB
        self.gamer.significator = self.target_card
        self.gamer.save(update_fields=["significator"])
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        # Stage card is visible w. the saved card populated (corner-rank +
        # name pinned by data-card-id on the stage card wrapper)
        stage_card = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".my-sign-stage .sig-stage-card"
            )
        )
        self.assertTrue(
            stage_card.is_displayed(),
            "Stage card should preview the saved sig on landing",
        )
        self.assertEqual(
            stage_card.get_attribute("data-card-id"),
            str(self.target_card.id),
        )
        # Stage is frozen → stat block visible next to the card
        stage = self.browser.find_element(By.CSS_SELECTOR, ".my-sign-stage")
        self.assertIn("sig-stage--frozen", stage.get_attribute("class"))
        self.assertTrue(
            self.browser.find_element(
                By.CSS_SELECTOR, ".my-sign-stage .sig-stat-block"
            ).is_displayed(),
            "Stat block should be visible alongside the saved sig",
        )
        # Hex + SCAN SIGN gone — user must DEL to re-enter picker
        self.assertEqual(
            len(self.browser.find_elements(By.ID, "id_scan_sign_btn")),
            0,
            "SCAN SIGN btn should not render when a sig is already saved",
        )
        self.assertEqual(
            len(self.browser.find_elements(
                By.CSS_SELECTOR, ".my-sign-page .table-hex"
            )),
            0,
            "Table hex should not render when a sig is already saved",
        )

    # ── Test 3 ───────────────────────────────────────────────────────────────

    def test_applet_renders_blank_state_when_no_sig_chosen(self):
        """Fresh user with no significator → applet shows the empty-state
        copy, no card."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/")
        empty = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, "#id_applet_my_sign .my-sign-applet-empty"
            )
        )
        self.assertIn("No sign chosen", empty.text)
        self.assertEqual(
            len(self.browser.find_elements(
                By.CSS_SELECTOR, "#id_applet_my_sign .my-sign-applet-card"
            )),
            0,
        )


class MySignBackupDeckTest(FunctionalTest):
    """User with no equipped_deck + no saved sig should still be able to pick.

    Sprint 4a-follow: no-equipped-deck path. Page renders a Brief banner
    nudging "Look!—no deck is equipped. Navigate to the game kit to equip
    one (FYI) or (NVM) proceed with Earthman [Shabby Paperboard] deck." —
    NVM dismisses + picker stays usable w. backup deck cards; FYI links
    to the gameboard (Game Kit applet)."""

    def setUp(self):
        super().setUp()
        _seed_earthman_sig_pile()
        for slug, name in [
            ("my-sign", "Game Sign"), ("my-scrolls", "My Scrolls"),
            ("my-buds", "My Buds"), ("most-recent-scroll", "Most Recent Scroll"),
        ]:
            Applet.objects.get_or_create(
                slug=slug, defaults={"name": name, "context": "billboard"},
            )
        self.email = "dekless@test.io"
        self.gamer = User.objects.create(email=self.email)
        # Simulate "deck-in-use elsewhere" — clear equipped_deck (the
        # symmetry of the room flow that hands the deck off to a TableSeat).
        self.gamer.equipped_deck = None
        self.gamer.save(update_fields=["equipped_deck"])

    # ── Test 1 ───────────────────────────────────────────────────────────────

    def test_brief_banner_renders_when_no_deck_equipped_and_no_sig(self):
        """Banner should appear via Brief.showBanner() w. title "Default deck
        warning" + the Shabby Cardstock copy + FYI + NVM buttons."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        banner = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".my-sign-intro-banner"
            )
        )
        self.assertIn("Default deck warning", banner.text)
        self.assertIn("no deck is equipped", banner.text)
        self.assertIn("Shabby Cardstock", banner.text)
        # Brief banner action buttons: .note-banner__nvm + .note-banner__fyi
        self.assertTrue(
            banner.find_element(By.CSS_SELECTOR, ".note-banner__fyi").is_displayed()
        )
        self.assertTrue(
            banner.find_element(By.CSS_SELECTOR, ".note-banner__nvm").is_displayed()
        )

    # ── Test 2 ───────────────────────────────────────────────────────────────

    def test_picker_renders_18_card_pile_using_backup_deck(self):
        """No equipped deck → personal_sig_cards falls back to Earthman, so the
        picker grid still has the full sig pile (16 cards w. no Note unlocks)."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        # Grid present + populated (find_elements returns N cards)
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".my-sign-deck-grid")
        )
        cards = self.browser.find_elements(
            By.CSS_SELECTOR, ".my-sign-deck-grid .sig-card"
        )
        self.assertEqual(len(cards), 16)

    # ── Test 3 ───────────────────────────────────────────────────────────────

    def test_nvm_dismisses_banner_and_keeps_picker_usable(self):
        """NVM click hides the banner; the grid remains interactive (can
        click a sig-card + SAVE SIGN persists against the backup deck)."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        banner = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, ".my-sign-intro-banner")
        )
        banner.find_element(By.CSS_SELECTOR, ".note-banner__nvm").click()
        self.wait_for(
            lambda: self.assertEqual(
                len(self.browser.find_elements(
                    By.CSS_SELECTOR, ".my-sign-intro-banner"
                )),
                0,
            )
        )

    # ── Test 4 ───────────────────────────────────────────────────────────────

    def test_fyi_links_to_gameboard(self):
        """FYI button is `.note-banner__fyi` rendered as <a href> by
        Brief.showBanner pointing at the Brief's `post_url` — here /gameboard/
        so the user can equip a deck via Game Kit."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        fyi = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".my-sign-intro-banner .note-banner__fyi"
            )
        )
        href = fyi.get_attribute("href") or ""
        self.assertTrue(
            href.endswith("/gameboard/"),
            f"FYI should link to /gameboard/, got {href!r}",
        )


class MySignClearTest(FunctionalTest):
    """Clear-sign affordance on the SCAN SIGN landing screen.

    Sprint 4b-adjacent (2026-05-19): the only way to undo a saved sig was
    DB surgery — which blocked manual verification of [[sprint-my-sea-sign-
    gate-may19]]'s no-sig branch on dev users w. a sig already set. Design
    pre-spec'd in the 4b memory: a small CLEAR btn on the SCAN SIGN
    landing screen, visible only when `user.significator` is set; POST
    clears the FK + reversed flag + reloads to the no-sig landing."""

    def setUp(self):
        super().setUp()
        _seed_earthman_sig_pile()
        _seed_my_sign_applet()
        self.email = "clear@test.io"
        self.gamer = User.objects.create(email=self.email)
        # Pre-save a sig so the DEL affordance is visible on landing.
        self.target_card = _assign_sig(self.gamer)

    # ── Test 1 ───────────────────────────────────────────────────────────────

    def test_clear_sign_btn_renders_on_landing_when_sig_saved(self):
        """When `user.significator` is set, the landing screen shows a
        DEL btn alongside SCAN SIGN. Uses .btn-danger for destructive
        action treatment (mirrors post.html gear menu DEL)."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        btn = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, "#id_clear_sign_btn"
            )
        )
        self.assertTrue(btn.is_displayed())
        self.assertIn("DEL", btn.text.upper())
        self.assertIn("btn-danger", btn.get_attribute("class"))

    # ── Test 2 ───────────────────────────────────────────────────────────────

    def test_clear_sign_btn_absent_when_no_sig_saved(self):
        """Fresh user w. no significator → no CLEAR btn on the landing."""
        # Wipe the sig set in setUp so this user lands in the no-sig state.
        self.gamer.significator = None
        self.gamer.save(update_fields=["significator"])
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        # Wait for the page to settle on the SCAN SIGN btn (proxy for landing).
        self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_scan_sign_btn")
        )
        self.assertEqual(
            len(self.browser.find_elements(
                By.CSS_SELECTOR, "#id_clear_sign_btn"
            )),
            0,
        )

    # ── Test 3 ───────────────────────────────────────────────────────────────

    def test_clear_sign_click_wipes_sig_and_reloads_to_no_sig_landing(self):
        """Click CLEAR SIGN → POST → page reloads → landing shows the
        no-sig state (no stage card preview, no CLEAR btn) + the DB has
        `user.significator = None`."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        btn = self.wait_for(
            lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_clear_sign_btn")
        )
        btn.click()
        # After the POST → redirect → GET, the CLEAR btn should no longer
        # exist + the stage card preview should be hidden.
        self.wait_for(
            lambda: self.assertEqual(
                len(self.browser.find_elements(
                    By.CSS_SELECTOR, "#id_clear_sign_btn"
                )),
                0,
            )
        )
        self.gamer.refresh_from_db()
        self.assertIsNone(self.gamer.significator)
        self.assertFalse(self.gamer.significator_reversed)


class MySignImageRenderingTest(FunctionalTest):
    """Sprint A.3 — when the user's equipped deck has card images (Minchiate
    Fiorentine 1860-1890 today), the saved-sig stage card renders as an <img>
    pointing at the deck's image asset, not the text-only fan-card scaffold.

    First visible-surface FT in the image-rendering rollout per
    [[project-image-based-deck-face-rendering]]. Other 5 surfaces (my_sea,
    both billboard applets, room, game_kit) follow in A.5+.
    """

    def setUp(self):
        super().setUp()
        # Earthman is auto-equipped by the User post_save signal — seed its
        # pile first so the signal succeeds, then override the equipped deck
        # to Minchiate (the image-deck under test).
        _seed_earthman_sig_pile()
        self.minchiate = _seed_minchiate_image_fixtures()
        Applet.objects.get_or_create(
            slug="my-sign",
            defaults={"name": "My Sign", "context": "billboard",
                      "default_visible": True, "grid_cols": 4, "grid_rows": 6},
        )
        for slug, name in [
            ("my-scrolls", "My Scrolls"),
            ("my-buds", "My Buds"),
            ("most-recent-scroll", "Most Recent Scroll"),
        ]:
            Applet.objects.get_or_create(
                slug=slug, defaults={"name": name, "context": "billboard"},
            )
        self.email = "img-sig@test.io"
        # Superuser so post_save grants super-nomad + super-schizo Notes →
        # `_filter_major_unlocks` lets Il Matto (Major 0) through into the
        # picker grid. Without the Notes, Minchiate's sig pool is empty for
        # this user (no MIDDLE arcana cards + the 2 Major-0/1 cards filtered).
        self.gamer = User.objects.create(email=self.email, is_superuser=True)
        self.gamer.unlocked_decks.add(self.minchiate)
        self.gamer.equipped_deck = self.minchiate
        self.gamer.save(update_fields=["equipped_deck"])
        # Save Il Matto as the user's sig (bypass the picker UI — the FT is
        # about render output, not the pick flow).
        self.il_matto = TarotCard.objects.get(
            deck_variant=self.minchiate, slug="il-matto",
        )
        _assign_sig(self.gamer, card=self.il_matto)

    def test_saved_sig_renders_as_img_for_image_deck(self):
        """Visit /billboard/my-sign/ with a Minchiate sig saved → the stage
        card contains an <img> child whose src points at the deck's image
        asset under the v2 naming convention. The text fan-card scaffold is
        hidden in image mode."""
        self.create_pre_authenticated_session(self.email)
        self.browser.get(self.live_server_url + "/billboard/my-sign/")
        stage_card = self.wait_for(
            lambda: self.browser.find_element(
                By.CSS_SELECTOR, ".my-sign-stage .sig-stage-card"
            )
        )
        self.assertTrue(
            stage_card.is_displayed(),
            "Stage card should preview the saved sig on landing",
        )
        # <img> child renders w. the v2-convention filename for Il Matto.
        img = self.wait_for(
            lambda: stage_card.find_element(By.TAG_NAME, "img")
        )
        src = img.get_attribute("src") or ""
        self.assertIn(
            "minchiate-fiorentine-1860-1890-trumps-00-il-matto.png",
            src,
            f"Expected Minchiate Il Matto image src, got: {src}",
        )
        # Image mode toggle — the stage card carries a marker class so SCSS
        # can hide the fan-card text scaffold + show the <img>.
        self.assertIn(
            "sig-stage-card--image", stage_card.get_attribute("class"),
            "Stage card should carry .sig-stage-card--image class in image mode",
        )
