describe("RoleSelect", () => {
    let testDiv;

    beforeEach(() => {
        RoleSelect._testReset();  // zero _placeCardDelay; clear _animationPending/_pendingTurnChange
        testDiv = document.createElement("div");
        testDiv.innerHTML = `
            <div class="room-page"
                 data-select-role-url="/epic/room/test-uuid/select-role">
            </div>
        `;
        document.body.appendChild(testDiv);
        window.fetch = jasmine.createSpy("fetch").and.returnValue(
            Promise.resolve({ ok: true })
        );
        // Default stub: auto-confirm so existing card-click tests pass unchanged.
        // The click-guard integration describe overrides this with a capturing spy.
        window.showGuard = (_anchor, _msg, onConfirm) => onConfirm && onConfirm();
    });

    afterEach(() => {
        RoleSelect.closeFan();
        testDiv.remove();
        const warning = document.querySelector(".role-no-deck-warning");
        if (warning) warning.remove();
        delete window.showGuard;
    });

    // ------------------------------------------------------------------ //
    // openFan()                                                            //
    // ------------------------------------------------------------------ //

    describe("openFan()", () => {
        it("creates .role-select-backdrop in the DOM", () => {
            RoleSelect.openFan();
            expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
        });

        it("creates #id_role_select inside the backdrop", () => {
            RoleSelect.openFan();
            expect(document.getElementById("id_role_select")).not.toBeNull();
        });

        it("renders exactly 6 .card elements", () => {
            RoleSelect.openFan();
            const cards = document.querySelectorAll("#id_role_select .card");
            expect(cards.length).toBe(6);
        });

        it("does not open a second backdrop if already open", () => {
            RoleSelect.openFan();
            RoleSelect.openFan();
            expect(document.querySelectorAll(".role-select-backdrop").length).toBe(1);
        });
    });

    // ------------------------------------------------------------------ //
    // closeFan()                                                           //
    // ------------------------------------------------------------------ //

    describe("closeFan()", () => {
        it("removes .role-select-backdrop from the DOM", () => {
            RoleSelect.openFan();
            RoleSelect.closeFan();
            expect(document.querySelector(".role-select-backdrop")).toBeNull();
        });

        it("removes #id_role_select from the DOM", () => {
            RoleSelect.openFan();
            RoleSelect.closeFan();
            expect(document.getElementById("id_role_select")).toBeNull();
        });

        it("does not throw if no fan is open", () => {
            expect(() => RoleSelect.closeFan()).not.toThrow();
        });
    });

    // ------------------------------------------------------------------ //
    // Card interactions                                                    //
    // ------------------------------------------------------------------ //

    describe("card interactions", () => {
        beforeEach(() => {
            RoleSelect.openFan();
        });

        it("mouseenter adds .flipped to the card", () => {
            const card = document.querySelector("#id_role_select .card");
            card.dispatchEvent(new MouseEvent("mouseenter"));
            expect(card.classList.contains("flipped")).toBe(true);
        });

        it("mouseleave removes .flipped from the card", () => {
            const card = document.querySelector("#id_role_select .card");
            card.dispatchEvent(new MouseEvent("mouseenter"));
            card.dispatchEvent(new MouseEvent("mouseleave"));
            expect(card.classList.contains("flipped")).toBe(false);
        });

        it("clicking a card closes the fan", () => {
            const card = document.querySelector("#id_role_select .card");
            card.click();
            expect(document.getElementById("id_role_select")).toBeNull();
        });

        it("clicking a card POSTs to the select_role URL", () => {
            const card = document.querySelector("#id_role_select .card");
            card.click();
            expect(window.fetch).toHaveBeenCalledWith(
                "/epic/room/test-uuid/select-role",
                jasmine.objectContaining({ method: "POST" })
            );
        });

    });

    // ------------------------------------------------------------------ //
    // Backdrop click                                                       //
    // ------------------------------------------------------------------ //

    describe("backdrop click", () => {
        it("closes the fan", () => {
            RoleSelect.openFan();
            document.querySelector(".role-select-backdrop").click();
            expect(document.getElementById("id_role_select")).toBeNull();
        });

    });

    // ------------------------------------------------------------------ //
    // room:all_roles_filled event                                         //
    // ------------------------------------------------------------------ //

    describe("room:all_roles_filled event", () => {
        let pickSigsWrap;

        beforeEach(() => {
            pickSigsWrap = document.createElement("div");
            pickSigsWrap.id = "id_pick_sigs_wrap";
            pickSigsWrap.style.display = "none";
            testDiv.appendChild(pickSigsWrap);
        });

        it("shows #id_pick_sigs_wrap", () => {
            window.dispatchEvent(new CustomEvent("room:all_roles_filled", { detail: {} }));
            expect(pickSigsWrap.style.display).toBe("");
        });
    });

    // ------------------------------------------------------------------ //
    // room:sig_select_started event                                       //
    // ------------------------------------------------------------------ //

    describe("room:sig_select_started event", () => {
        let reloadCalled;

        beforeEach(() => {
            reloadCalled = false;
            RoleSelect.setReload(() => { reloadCalled = true; });
        });

        afterEach(() => {
            RoleSelect.setReload(() => { window.location.reload(); });
        });

        it("triggers a page reload", () => {
            window.dispatchEvent(new CustomEvent("room:sig_select_started", { detail: {} }));
            expect(reloadCalled).toBe(true);
        });
    });

    // ------------------------------------------------------------------ //
    // room:turn_changed event                                              //
    // ------------------------------------------------------------------ //

    describe("room:turn_changed event", () => {
        let stack, trayWrap;

        beforeEach(() => {
            // Six table seats, slot 1 starts active
            for (let i = 1; i <= 6; i++) {
                const seat = document.createElement("div");
                seat.className = "table-seat" + (i === 1 ? " active" : "");
                seat.dataset.slot = String(i);
                seat.innerHTML = '<div class="seat-card-arc"></div>';
                testDiv.appendChild(seat);
            }
            stack = document.createElement("div");
            stack.className = "card-stack";
            stack.dataset.state = "ineligible";
            stack.dataset.userSlots = "1";
            stack.dataset.starterRoles = "";
            testDiv.appendChild(stack);

            trayWrap = document.createElement("div");
            trayWrap.id = "id_tray_wrap";
            trayWrap.className = "role-select-phase";
            testDiv.appendChild(trayWrap);
        });

        it("calls Tray.forceClose() on turn change", () => {
            spyOn(Tray, "forceClose");
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2 }
            }));
            expect(Tray.forceClose).toHaveBeenCalled();
        });

        it("clears .active from all seats on turn change", () => {
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2 }
            }));
            expect(testDiv.querySelector(".table-seat.active")).toBeNull();
        });

        it("re-adds role-select-phase to tray wrap on turn change", () => {
            trayWrap.classList.remove("role-select-phase");  // simulate it was shown
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2 }
            }));
            expect(trayWrap.classList.contains("role-select-phase")).toBe(true);
        });

        it("removes .active from the previously active seat", () => {
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2 }
            }));
            expect(
                testDiv.querySelector(".table-seat[data-slot='1']").classList.contains("active")
            ).toBe(false);
        });

        it("sets data-state to eligible when active_slot matches user slot", () => {
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 1 }
            }));
            expect(stack.dataset.state).toBe("eligible");
        });

        it("sets data-state to ineligible when active_slot does not match", () => {
            stack.dataset.state = "eligible";
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2 }
            }));
            expect(stack.dataset.state).toBe("ineligible");
        });

        it("clicking stack opens fan when newly eligible", () => {
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 1 }
            }));
            stack.click();
            expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
        });

        it("clicking stack does not open fan when ineligible", () => {
            // Make eligible first (adds listener), then flip back to ineligible
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 1 }
            }));
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2 }
            }));
            stack.click();
            expect(document.querySelector(".role-select-backdrop")).toBeNull();
        });

        it("updates seat icon to fa-circle-check when role appears in starter_roles", () => {
            const seat = document.createElement("div");
            seat.className = "table-seat";
            seat.dataset.role = "PC";
            const ban = document.createElement("i");
            ban.className = "position-status-icon fa-solid fa-ban";
            seat.appendChild(ban);
            testDiv.appendChild(seat);

            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2, starter_roles: ["PC"] }
            }));

            expect(seat.querySelector(".fa-ban")).toBeNull();
            expect(seat.querySelector(".fa-circle-check")).not.toBeNull();
        });

        it("adds role-confirmed to seat when role appears in starter_roles", () => {
            const seat = document.createElement("div");
            seat.className = "table-seat";
            seat.dataset.role = "PC";
            testDiv.appendChild(seat);

            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2, starter_roles: ["PC"] }
            }));

            expect(seat.classList.contains("role-confirmed")).toBe(true);
        });

        it("leaves seat icon as fa-ban when role not in starter_roles", () => {
            const seat = document.createElement("div");
            seat.className = "table-seat";
            seat.dataset.role = "NC";
            const ban = document.createElement("i");
            ban.className = "position-status-icon fa-solid fa-ban";
            seat.appendChild(ban);
            testDiv.appendChild(seat);

            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2, starter_roles: ["PC"] }
            }));

            expect(seat.querySelector(".fa-ban")).not.toBeNull();
            expect(seat.querySelector(".fa-circle-check")).toBeNull();
        });

        it("adds role-assigned to slot-1 circle when 1 role assigned", () => {
            const circle = document.createElement("div");
            circle.className = "gate-slot filled";
            circle.dataset.slot = "1";
            testDiv.appendChild(circle);

            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2, starter_roles: ["PC"] }
            }));

            expect(circle.classList.contains("role-assigned")).toBe(true);
        });

        it("leaves slot-2 circle visible when only 1 role assigned", () => {
            const circle = document.createElement("div");
            circle.className = "gate-slot filled";
            circle.dataset.slot = "2";
            testDiv.appendChild(circle);

            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2, starter_roles: ["PC"] }
            }));

            expect(circle.classList.contains("role-assigned")).toBe(false);
        });

        it("updates data-active-slot on card stack to the new active slot", () => {
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2, starter_roles: [] }
            }));
            expect(stack.dataset.activeSlot).toBe("2");
        });
    });

    // ------------------------------------------------------------------ //
    // selectRole slot-circle fade-out                                      //
    // ------------------------------------------------------------------ //

    describe("selectRole() slot-circle behaviour", () => {
        let circle, stack;

        beforeEach(() => {
            // Gate-slot circle for slot 1 (active turn)
            circle = document.createElement("div");
            circle.className = "gate-slot filled";
            circle.dataset.slot = "1";
            testDiv.appendChild(circle);

            // Card stack with active-slot=1 so selectRole() knows which circle to hide
            stack = document.createElement("div");
            stack.className = "card-stack";
            stack.dataset.state = "eligible";
            stack.dataset.starterRoles = "";
            stack.dataset.userSlots = "1";
            stack.dataset.activeSlot = "1";
            testDiv.appendChild(stack);

            spyOn(Tray, "placeCard");
        });

        it("adds role-assigned to the active slot's circle immediately on confirm", () => {
            RoleSelect.openFan();
            document.querySelector("#id_role_select .card").click();
            expect(circle.classList.contains("role-assigned")).toBe(true);
        });
    });

    // ------------------------------------------------------------------ //
    // Tray card placement after successful role selection                  //
    // ------------------------------------------------------------------ //
    // The tray-role-card is created in the fetch .then() callback, so     //
    // these tests are async — await Promise.resolve() flushes the         //
    // microtask queue before asserting.                                    //
    // ------------------------------------------------------------------ //

    describe("tray card after successful role selection", () => {
        let guardConfirm, trayWrap;

        beforeEach(() => {
            trayWrap = document.createElement("div");
            trayWrap.id = "id_tray_wrap";
            trayWrap.className = "role-select-phase";
            testDiv.appendChild(trayWrap);

            // Spy on Tray.placeCard: call the onComplete callback immediately.
            spyOn(Tray, "placeCard").and.callFake((roleCode, cb) => {
                if (cb) cb();
            });

            // Capturing guard spy — holds onConfirm so we can fire it per-test
            window.showGuard = jasmine.createSpy("showGuard").and.callFake(
                (anchor, message, onConfirm) => { guardConfirm = onConfirm; }
            );

            RoleSelect.openFan();
            document.querySelector("#id_role_select .card").click();
        });

        it("calls Tray.placeCard() on success", async () => {
            guardConfirm();
            await Promise.resolve();                    // flush fetch .then()
            await new Promise(r => setTimeout(r, 0));  // flush _placeCardDelay setTimeout
            expect(Tray.placeCard).toHaveBeenCalled();
        });

        it("passes the role code string to Tray.placeCard", async () => {
            guardConfirm();
            await Promise.resolve();                    // flush fetch .then()
            await new Promise(r => setTimeout(r, 0));  // flush _placeCardDelay setTimeout
            const roleCode = Tray.placeCard.calls.mostRecent().args[0];
            expect(typeof roleCode).toBe("string");
            expect(roleCode.length).toBeGreaterThan(0);
        });

        it("does not call Tray.placeCard() on server rejection", async () => {
            window.fetch = jasmine.createSpy("fetch").and.returnValue(
                Promise.resolve({ ok: false })
            );
            guardConfirm();
            await Promise.resolve();
            expect(Tray.placeCard).not.toHaveBeenCalled();
        });

        it("removes role-select-phase from tray wrap on successful pick", async () => {
            guardConfirm();
            await Promise.resolve();
            expect(trayWrap.classList.contains("role-select-phase")).toBe(false);
        });

        it("adds role-confirmed class to the seated position after placeCard completes", async () => {
            // Add a seat element matching the first available role (SC — Shepherd)
            const seat = document.createElement("div");
            seat.className = "table-seat";
            seat.dataset.role = "SC";
            seat.innerHTML = '<i class="position-status-icon fa-solid fa-ban"></i>';
            testDiv.appendChild(seat);

            guardConfirm();
            await Promise.resolve();                    // fetch resolves + placeCard fires
            await new Promise(r => setTimeout(r, 0));  // flush _postTrayDelay setTimeout

            expect(seat.classList.contains("role-confirmed")).toBe(true);
        });
    });

    // ------------------------------------------------------------------ //
    // WS turn_changed pause during animation                              //
    // ------------------------------------------------------------------ //

    describe("WS turn_changed pause during placeCard animation", () => {
        let stack, guardConfirm;

        beforeEach(() => {
            // Six table seats, slot 1 starts active
            for (let i = 1; i <= 6; i++) {
                const seat = document.createElement("div");
                seat.className = "table-seat" + (i === 1 ? " active" : "");
                seat.dataset.slot = String(i);
                testDiv.appendChild(seat);
            }
            stack = document.createElement("div");
            stack.className = "card-stack";
            stack.dataset.state = "ineligible";
            stack.dataset.userSlots = "1";
            stack.dataset.starterRoles = "";
            testDiv.appendChild(stack);

            const grid = document.createElement("div");
            grid.id = "id_tray_grid";
            testDiv.appendChild(grid);

            // placeCard spy that holds the onComplete callback
            let heldCallback = null;
            spyOn(Tray, "placeCard").and.callFake((roleCode, cb) => {
                heldCallback = cb;  // don't call immediately — simulate animation in-flight
            });
            spyOn(Tray, "forceClose");

            // Expose heldCallback so tests can fire it
            Tray._testFirePlaceCardComplete = () => {
                if (heldCallback) heldCallback();
            };

            window.showGuard = jasmine.createSpy("showGuard").and.callFake(
                (anchor, message, onConfirm) => { guardConfirm = onConfirm; }
            );
        });

        afterEach(() => {
            delete Tray._testFirePlaceCardComplete;
            RoleSelect._testReset();
        });

        it("turn_changed during animation does not call Tray.forceClose immediately", async () => {
            RoleSelect.openFan();
            document.querySelector("#id_role_select .card").click();
            guardConfirm();
            await Promise.resolve();  // fetch resolves; placeCard called; animation pending

            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2, starter_roles: [] }
            }));
            expect(Tray.forceClose).not.toHaveBeenCalled();
        });

        it("turn_changed during animation does not immediately move the active seat", async () => {
            RoleSelect.openFan();
            document.querySelector("#id_role_select .card").click();
            guardConfirm();
            await Promise.resolve();

            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2, starter_roles: [] }
            }));
            const activeSeat = testDiv.querySelector(".table-seat.active");
            expect(activeSeat && activeSeat.dataset.slot).toBe("1");  // still slot 1
        });

        it("deferred turn_changed is processed when animation completes", async () => {
            RoleSelect.openFan();
            document.querySelector("#id_role_select .card").click();
            guardConfirm();
            await Promise.resolve();                    // flush fetch .then()
            await new Promise(r => setTimeout(r, 0));  // flush _placeCardDelay → placeCard called, heldCallback set

            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2, starter_roles: [] }
            }));

            // Fire onComplete — post-tray delay (0 in tests) still uses setTimeout
            Tray._testFirePlaceCardComplete();
            await new Promise(r => setTimeout(r, 0));  // flush _postTrayDelay setTimeout

            // Seat glow is JS-only (tray animation window); after deferred
            // handleTurnChanged runs, all seat glows are cleared.
            expect(testDiv.querySelector(".table-seat.active")).toBeNull();
        });

        it("turn_changed after animation completes is processed immediately", () => {
            // No animation in flight — turn_changed should run right away
            window.dispatchEvent(new CustomEvent("room:turn_changed", {
                detail: { active_slot: 2, starter_roles: [] }
            }));
            expect(Tray.forceClose).toHaveBeenCalled();
            // Seats are not persistently glowed; all active cleared
            expect(testDiv.querySelector(".table-seat.active")).toBeNull();
        });
    });

    // ------------------------------------------------------------------ //
    // click-guard integration                                              //
    // ------------------------------------------------------------------ //
    // NOTE: cascade prevention (outside-click on backdrop not closing the  //
    // fan while the guard is active) relies on the guard portal's capture- //
    // phase stopPropagation, which lives in base.html and requires         //
    // integration testing. The callback contract is fully covered below.   //
    // ------------------------------------------------------------------ //

    describe("click-guard integration", () => {
        let guardAnchor, guardMessage, guardConfirm, guardDismiss;

        beforeEach(() => {
            window.showGuard = jasmine.createSpy("showGuard").and.callFake(
                (anchor, message, onConfirm, onDismiss) => {
                    guardAnchor  = anchor;
                    guardMessage = message;
                    guardConfirm = onConfirm;
                    guardDismiss = onDismiss;
                }
            );
            RoleSelect.openFan();
        });

        describe("clicking a card", () => {
            let card;

            beforeEach(() => {
                card = document.querySelector("#id_role_select .card");
                card.click();
            });

            it("calls window.showGuard", () => {
                expect(window.showGuard).toHaveBeenCalled();
            });

            it("passes the card element as the anchor", () => {
                expect(guardAnchor).toBe(card);
            });

            it("message contains the role name", () => {
                const roleName = card.querySelector(".card-role-name").textContent.trim();
                expect(guardMessage).toContain(roleName);
            });

            it("message contains the role code", () => {
                expect(guardMessage).toContain(card.dataset.role);
            });

            it("message contains a <br>", () => {
                expect(guardMessage).toContain("<br>");
            });

            it("does not immediately close the fan", () => {
                expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
            });

            it("does not immediately POST to the select_role URL", () => {
                expect(window.fetch).not.toHaveBeenCalled();
            });

            it("adds .flipped to the card", () => {
                expect(card.classList.contains("flipped")).toBe(true);
            });

            it("adds .guard-active to the card", () => {
                expect(card.classList.contains("guard-active")).toBe(true);
            });

            it("mouseleave does not remove .flipped while guard is active", () => {
                card.dispatchEvent(new MouseEvent("mouseleave"));
                expect(card.classList.contains("flipped")).toBe(true);
            });
        });

        describe("confirming the guard (OK)", () => {
            let card;

            beforeEach(() => {
                card = document.querySelector("#id_role_select .card");
                card.click();
                guardConfirm();
            });

            it("removes .guard-active from the card", () => {
                expect(card.classList.contains("guard-active")).toBe(false);
            });

            it("closes the fan", () => {
                expect(document.querySelector(".role-select-backdrop")).toBeNull();
            });

            it("POSTs to the select_role URL", () => {
                expect(window.fetch).toHaveBeenCalledWith(
                    "/epic/room/test-uuid/select-role",
                    jasmine.objectContaining({ method: "POST" })
                );
            });

        });

        describe("dismissing the guard (NVM or outside click)", () => {
            let card;

            beforeEach(() => {
                card = document.querySelector("#id_role_select .card");
                card.click();
                guardDismiss();
            });

            it("removes .guard-active from the card", () => {
                expect(card.classList.contains("guard-active")).toBe(false);
            });

            it("removes .flipped from the card", () => {
                expect(card.classList.contains("flipped")).toBe(false);
            });

            it("leaves the fan open", () => {
                expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
            });

            it("does not POST to the select_role URL", () => {
                expect(window.fetch).not.toHaveBeenCalled();
            });

            it("restores normal mouseleave behaviour on the card", () => {
                card.dispatchEvent(new MouseEvent("mouseenter"));
                card.dispatchEvent(new MouseEvent("mouseleave"));
                expect(card.classList.contains("flipped")).toBe(false);
            });
        });
    });

    // ------------------------------------------------------------------ //
    // No-deck guard                                                        //
    // data-equipped-deck="" → openFan() blocked; warning shown over stack //
    // ------------------------------------------------------------------ //

    describe("openFan() — no deck equipped (data-equipped-deck='')", () => {
        let stack;

        beforeEach(() => {
            stack = document.createElement("div");
            stack.className = "card-stack";
            stack.dataset.state = "eligible";
            stack.dataset.starterRoles = "";
            stack.dataset.userSlots = "1";
            stack.dataset.activeSlot = "1";
            stack.dataset.equippedDeck = "";   // explicitly empty = no deck
            testDiv.appendChild(stack);

            RoleSelect.openFan();  // intercepted — fan must NOT open
        });

        it("does not open the fan backdrop", () => {
            expect(document.querySelector(".role-select-backdrop")).toBeNull();
        });

        it("shows .role-no-deck-warning in the DOM", () => {
            expect(document.querySelector(".role-no-deck-warning")).not.toBeNull();
        });

        it("warning contains the deck prompt text", () => {
            const w = document.querySelector(".role-no-deck-warning");
            expect(w.textContent).toContain("Equip card deck before Role select");
        });

        it("warning has a .btn-info FYI link to gameboard", () => {
            const btn = document.querySelector(".role-no-deck-warning .guard-actions .btn-info");
            expect(btn).not.toBeNull();
            expect(btn.tagName).toBe("A");
            expect(btn.href).toContain("/gameboard/");
        });

        it("warning has a .btn-cancel NVM button", () => {
            expect(document.querySelector(".role-no-deck-warning .guard-actions .btn-cancel")).not.toBeNull();
        });

        it("NVM click removes the warning", () => {
            document.querySelector(".role-no-deck-warning .guard-actions .btn-cancel").click();
            expect(document.querySelector(".role-no-deck-warning")).toBeNull();
        });

        it("second openFan() call does not duplicate the warning", () => {
            RoleSelect.openFan();
            expect(document.querySelectorAll(".role-no-deck-warning").length).toBe(1);
        });

        it("does not POST to select_role", () => {
            expect(window.fetch).not.toHaveBeenCalled();
        });
    });

    describe("openFan() — deck is equipped (data-equipped-deck non-empty)", () => {
        beforeEach(() => {
            const stack = document.createElement("div");
            stack.className = "card-stack";
            stack.dataset.state = "eligible";
            stack.dataset.starterRoles = "";
            stack.dataset.userSlots = "1";
            stack.dataset.activeSlot = "1";
            stack.dataset.equippedDeck = "42";   // non-empty = deck present
            testDiv.appendChild(stack);

            RoleSelect.openFan();
        });

        it("opens the fan backdrop normally", () => {
            expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
        });

        it("does not show .role-no-deck-warning", () => {
            expect(document.querySelector(".role-no-deck-warning")).toBeNull();
        });
    });
});
