describe("FanStage", () => {
    let testDiv, dialog, fanContent, stageBlock, stageCard;

    function makeCardEl({ id, suit_icon = '', corner_rank = 'I', name_group = '',
                          name_title = 'The Magician', arcana = 'Major Arcana',
                          correspondence = '', keywords_upright = 'will,focus,manifestation',
                          keywords_reversed = 'manipulation,illusion',
                          energies = '[]', operations = '[]',
                          levity_qualifier = '', gravity_qualifier = '',
                          reversal_qualifier = '' } = {}) {
        return `<div class="fan-card"
             data-index="${id}"
             data-suit-icon="${suit_icon}"
             data-corner-rank="${corner_rank}"
             data-name-group="${name_group}"
             data-name-title="${name_title}"
             data-arcana="${arcana}"
             data-correspondence="${correspondence}"
             data-keywords-upright="${keywords_upright}"
             data-keywords-reversed="${keywords_reversed}"
             data-energies='${energies}'
             data-operations='${operations}'
             data-levity-qualifier="${levity_qualifier}"
             data-gravity-qualifier="${gravity_qualifier}"
             data-reversal-qualifier="${reversal_qualifier}">
            <div class="fan-card-corner fan-card-corner--tl">
                <span class="fan-corner-rank">${corner_rank}</span>
            </div>
            <div class="fan-card-face">
                <div class="fan-card-face-upright">
                    <p class="fan-card-name-group"></p>
                    <p class="sig-qualifier-above"></p>
                    <h3 class="fan-card-name">${name_title}</h3>
                    <p class="sig-qualifier-below"></p>
                </div>
                <p class="fan-card-arcana">${arcana}</p>
                <div class="fan-card-face-reversal">
                    <p class="fan-card-reversal-qualifier"></p>
                    <p class="fan-card-reversal-name"></p>
                </div>
            </div>
            <div class="fan-card-corner fan-card-corner--br">
                <span class="fan-corner-rank">${corner_rank}</span>
            </div>
        </div>`;
    }

    function makeFixture() {
        testDiv = document.createElement("div");
        testDiv.innerHTML = `
            <dialog id="id_tarot_fan_dialog">
                <div class="tarot-fan-wrap">
                    <button id="id_fan_prev" class="fan-nav fan-nav--prev">&#8249;</button>
                    <div id="id_fan_content" class="tarot-fan"></div>
                    <div class="fan-stage-block sig-stat-block" id="id_fan_stage_block">
                        <button class="btn btn-reverse spin-btn" type="button">SPIN</button>
                        <button class="btn btn-info fyi-btn" type="button">FYI</button>
                        <div class="stat-face stat-face--upright">
                            <p class="stat-face-label">Emanation</p>
                            <ul class="stat-keywords" id="id_fan_stat_upright"></ul>
                        </div>
                        <div class="stat-face stat-face--reversed">
                            <p class="stat-face-label">Reversal</p>
                            <ul class="stat-keywords" id="id_fan_stat_reversed"></ul>
                        </div>
                        <div class="sig-info" id="id_fan_fyi_panel" style="display:none">
                            <div class="sig-info-header">
                                <h4 class="sig-info-title"></h4>
                                <p class="sig-info-type"></p>
                            </div>
                            <p class="sig-info-effect"></p>
                            <span class="sig-info-index"></span>
                        </div>
                        <button class="btn btn-nav-left fyi-prev" type="button">PRV</button>
                        <button class="btn btn-nav-right fyi-next" type="button">NXT</button>
                    </div>
                    <button id="id_fan_flip" class="btn btn-reveal fan-flip-btn" type="button">FLIP</button>
                    <button id="id_fan_next" class="fan-nav fan-nav--next">&#8250;</button>
                </div>
            </dialog>
            <button class="gk-deck-card" data-deck-id="1">Earthman</button>
        `;
        document.body.appendChild(testDiv);
        dialog       = testDiv.querySelector("#id_tarot_fan_dialog");
        fanContent   = testDiv.querySelector("#id_fan_content");
        stageBlock   = testDiv.querySelector("#id_fan_stage_block");

        // Pre-render two cards into fanContent so openFan's fetch can be skipped via _testOpen
        fanContent.innerHTML =
            makeCardEl({ id: 0, name_title: 'The Magician', corner_rank: 'I',
                         levity_qualifier: 'Enlightened',
                         gravity_qualifier: 'Engraven',
                         keywords_upright: 'will,focus',
                         keywords_reversed: 'manipulation',
                         energies: '[{"type":"LIBIDO","effect":"Drive."}]',
                         operations: '[{"type":"COVER","effect":"Shield."}]' }) +
            makeCardEl({ id: 1, name_title: 'The High Priestess', corner_rank: 'II',
                         levity_qualifier: 'Enlightened',
                         gravity_qualifier: 'Engraven',
                         keywords_upright: 'intuition,subconscious',
                         keywords_reversed: 'secrets,disconnect' });

        jasmine.clock().install();
        GameKit._testInit();
    }

    afterEach(() => {
        jasmine.clock().uninstall();
        if (testDiv) testDiv.remove();
    });

    // ── Stage block reveal / hide ────────────────────────────────────────── //

    describe("idle reveal", () => {
        beforeEach(() => makeFixture());

        it("starts with the stage block hidden (no .is-revealed)", () => {
            GameKit._testOpen();
            expect(stageBlock.classList.contains("is-revealed")).toBe(false);
        });

        it("adds .is-revealed after 500ms of idle", () => {
            GameKit._testOpen();
            jasmine.clock().tick(500);
            expect(stageBlock.classList.contains("is-revealed")).toBe(true);
        });

        it("does NOT reveal before 500ms have elapsed", () => {
            GameKit._testOpen();
            jasmine.clock().tick(400);
            expect(stageBlock.classList.contains("is-revealed")).toBe(false);
        });

        it("re-hides immediately on navigation, even after reveal", () => {
            GameKit._testOpen();
            jasmine.clock().tick(500);
            expect(stageBlock.classList.contains("is-revealed")).toBe(true);
            GameKit._testNavigate(1);
            expect(stageBlock.classList.contains("is-revealed")).toBe(false);
        });

        it("restarts the idle timer after navigation", () => {
            GameKit._testOpen();
            jasmine.clock().tick(500);
            GameKit._testNavigate(1);
            jasmine.clock().tick(400);
            expect(stageBlock.classList.contains("is-revealed")).toBe(false);
            jasmine.clock().tick(100);
            expect(stageBlock.classList.contains("is-revealed")).toBe(true);
        });
    });

    // ── Stat block population from focused fan-card ──────────────────────── //

    describe("stat block population", () => {
        beforeEach(() => makeFixture());

        it("populates upright keywords from data-keywords-upright of focused card", () => {
            GameKit._testOpen();
            const list = stageBlock.querySelector("#id_fan_stat_upright");
            const items = list.querySelectorAll("li");
            expect(items.length).toBe(2);
            expect(items[0].textContent).toBe("will");
            expect(items[1].textContent).toBe("focus");
        });

        it("populates reversed keywords from data-keywords-reversed of focused card", () => {
            GameKit._testOpen();
            const list = stageBlock.querySelector("#id_fan_stat_reversed");
            const items = list.querySelectorAll("li");
            expect(items.length).toBe(1);
            expect(items[0].textContent).toBe("manipulation");
        });

        it("re-populates from the new focused card on navigation", () => {
            GameKit._testOpen();
            GameKit._testNavigate(1);
            const items = stageBlock.querySelectorAll("#id_fan_stat_upright li");
            expect(items.length).toBe(2);
            expect(items[0].textContent).toBe("intuition");
            expect(items[1].textContent).toBe("subconscious");
        });
    });

    // ── SPIN button (.btn-reverse) ──────────────────────────────────────── //

    describe("SPIN", () => {
        beforeEach(() => makeFixture());

        it("toggles .is-reversed on the stat block", () => {
            GameKit._testOpen();
            stageBlock.querySelector(".spin-btn").click();
            expect(stageBlock.classList.contains("is-reversed")).toBe(true);
        });

        it("toggles back when clicked twice", () => {
            GameKit._testOpen();
            const spin = stageBlock.querySelector(".spin-btn");
            spin.click(); spin.click();
            expect(stageBlock.classList.contains("is-reversed")).toBe(false);
        });

        it("resets .is-reversed when navigating to a new card", () => {
            GameKit._testOpen();
            stageBlock.querySelector(".spin-btn").click();
            expect(stageBlock.classList.contains("is-reversed")).toBe(true);
            GameKit._testNavigate(1);
            expect(stageBlock.classList.contains("is-reversed")).toBe(false);
        });
    });

    // ── FYI button (.btn-info) ──────────────────────────────────────────── //

    describe("FYI", () => {
        beforeEach(() => makeFixture());

        it("opens the FYI panel and disables SPIN+FYI buttons", () => {
            GameKit._testOpen();
            stageBlock.querySelector(".fyi-btn").click();
            const panel = stageBlock.querySelector("#id_fan_fyi_panel");
            expect(panel.style.display).toBe("flex");
            expect(stageBlock.querySelector(".spin-btn").classList.contains("btn-disabled")).toBe(true);
            expect(stageBlock.querySelector(".fyi-btn").classList.contains("btn-disabled")).toBe(true);
        });

        it("populates the panel with the first energy entry from data-energies", () => {
            GameKit._testOpen();
            stageBlock.querySelector(".fyi-btn").click();
            const panel = stageBlock.querySelector("#id_fan_fyi_panel");
            expect(panel.querySelector(".sig-info-type").textContent).toBe("LIBIDO");
            expect(panel.querySelector(".sig-info-effect").innerHTML).toBe("Drive.");
        });

        it("PRV/NXT cycle through energies + operations", () => {
            GameKit._testOpen();
            stageBlock.querySelector(".fyi-btn").click();
            stageBlock.querySelector(".fyi-next").click();
            const panel = stageBlock.querySelector("#id_fan_fyi_panel");
            expect(panel.querySelector(".sig-info-type").textContent).toBe("COVER");
            expect(panel.querySelector(".sig-info-effect").innerHTML).toBe("Shield.");
        });
    });

    // ── FLIP — polarity toggle (Levity ↔ Gravity) ──────────────────────────── //
    //
    // FLIP swaps polarity on the focused card with a perspective rotateY animation.
    // The repaint fires at the 250ms midpoint via setTimeout (jasmine.clock fakes
    // it). Element.animate() is called too — but tests assert side effects (the
    // dataset.polarity attr + the qualifier text content), not the animation.

    describe("FLIP", () => {
        beforeEach(() => makeFixture());

        function flipBtn() { return testDiv.querySelector("#id_fan_flip"); }
        function activeCard() { return testDiv.querySelector(".fan-card--active"); }

        it("starts with polarity = levity (the server-rendered default)", () => {
            GameKit._testOpen();
            jasmine.clock().tick(250);
            expect(activeCard().dataset.polarity).toBe("levity");
        });

        it("toggles dataset.polarity to gravity at the midpoint after click", () => {
            GameKit._testOpen();
            flipBtn().click();
            jasmine.clock().tick(250);
            expect(activeCard().dataset.polarity).toBe("gravity");
        });

        it("repaints the upright qualifier slot with the gravity qualifier", () => {
            GameKit._testOpen();
            flipBtn().click();
            jasmine.clock().tick(250);
            const card = activeCard();
            // Major arcana places qualifier in the BELOW slot.
            expect(card.querySelector(".sig-qualifier-below").textContent).toBe("Engraven");
        });

        it("FLIPs back to levity on a second click", () => {
            GameKit._testOpen();
            flipBtn().click();
            jasmine.clock().tick(500);
            flipBtn().click();
            jasmine.clock().tick(250);
            expect(activeCard().dataset.polarity).toBe("levity");
        });

        it("retains SPIN state across a FLIP (.stage-card--reversed survives)", () => {
            GameKit._testOpen();
            stageBlock.querySelector(".spin-btn").click();   // SPIN first
            expect(activeCard().classList.contains("stage-card--reversed")).toBe(true);
            flipBtn().click();
            jasmine.clock().tick(500);
            expect(activeCard().classList.contains("stage-card--reversed")).toBe(true);
        });

        it("ignores a second click while a flip is in flight", () => {
            GameKit._testOpen();
            flipBtn().click();
            // Mid-animation second click — should be ignored
            jasmine.clock().tick(100);
            flipBtn().click();
            jasmine.clock().tick(150);  // total = 250ms (one repaint window)
            // Only one polarity swap happened (levity → gravity), not two.
            expect(activeCard().dataset.polarity).toBe("gravity");
        });
    });

    // ── Bookmark vs polarity — sessionStorage stores card index only ─────── //
    //
    // The bookmark must NOT encode FLIP state: closing a modal in gravity and
    // reopening it should always land on the levity-painted face (the SSR
    // default + the FLIP cue). _testOpen mirrors openFan's polarity reset so
    // a re-call simulates a real close + reopen.

    describe("openFan polarity reset", () => {
        beforeEach(() => makeFixture());

        function flipBtn() { return testDiv.querySelector("#id_fan_flip"); }
        function activeCard() { return testDiv.querySelector(".fan-card--active"); }

        it("resets polarity to levity on reopen even after FLIP-to-gravity", () => {
            GameKit._testOpen();
            flipBtn().click();
            jasmine.clock().tick(500);
            expect(activeCard().dataset.polarity).toBe("gravity");

            // Re-open: bookmark would restore currentIndex, polarity must reset.
            GameKit._testOpen();
            jasmine.clock().tick(250);
            expect(activeCard().dataset.polarity).toBe("levity");
        });

        it("does not persist polarity in sessionStorage", () => {
            GameKit._testOpen();
            flipBtn().click();
            jasmine.clock().tick(500);
            // Bookmark stores currentIndex only — no polarity key anywhere.
            for (let i = 0; i < sessionStorage.length; i++) {
                const key = sessionStorage.key(i);
                const val = sessionStorage.getItem(key);
                expect(val).not.toMatch(/levity|gravity/);
            }
        });
    });
});
