// ── WalletShopSpec.js ────────────────────────────────────────────────────────
//
// Unit specs for wallet-shop.js — the BUY-ITEM click handler that drives
// the shop applet's purchase flow (Chunk 4 of the wallet-expansion sprint).
//
// DOM contract assumed by `initWalletShop()`:
//
//   Each shop tile carries `data-shop-item-slug`, `data-price-cents`, and
//   `data-item-name`. Inside the tile, a `.tt-buy-btn` element is the
//   click target. Disabled tiles have `.btn-disabled` on the BUY btn +
//   should NOT trigger the buy flow.
//
//   `window.showGuard(anchor, message, onConfirm)` is the project's
//   guard-portal API (from base.html). We spy on it to assert message
//   + callback wiring.
//
//   `window.Stripe` and `window.fetch` are stubbed out per-test so we
//   never hit the real Stripe.js or backend.
//
// API under test:
//   `initWalletShop()` — attaches click listeners to every `.tt-buy-btn`
//                        inside `.wallet-shop`. Idempotent (calling twice
//                        doesn't double-fire).
// ─────────────────────────────────────────────────────────────────────────────

function _seedShopFixture() {
    const root = document.createElement('section');
    root.className = 'wallet-shop';
    root.id = 'id_wallet_shop';
    // `_doBuy` reads these from `.wallet-shop` dataset; without them the
    // buy flow short-circuits at the no-PM alert before reaching fetch.
    root.dataset.defaultPaymentMethodId = 'pm_test_4242';
    root.dataset.stripePublishableKey   = 'pk_test_fixture';
    root.innerHTML = `
        <div id="id_shop_tithe-1" class="shop-tile">
            <i class="fa-solid fa-piggy-bank"></i>
            <div class="tt">
                <h4 class="tt-title">Tithe Token</h4>
                <p class="tt-description">1 Tithe + 144 Writs</p>
                <p class="tt-price">$1</p>
            </div>
            <div class="tt-micro">
                <button class="btn btn-primary tt-buy-btn"
                        data-shop-item-slug="tithe-1"
                        data-item-name="Tithe Token"
                        data-price-cents="100">BUY ITEM</button>
            </div>
        </div>
        <div id="id_shop_band-1" class="shop-tile">
            <i class="fa-solid fa-ring"></i>
            <div class="tt">
                <h4 class="tt-title">Wristband</h4>
                <p class="tt-price">$20</p>
            </div>
            <div class="tt-micro">
                <span class="tt-already-owned">Already owned</span>
            </div>
        </div>
        <div id="id_shop_tarot-rider-waite-smith"
             class="shop-tile shop-tile-deck"
             data-deck-slug="tarot-rider-waite-smith">
            <div class="tt">
                <h4 class="tt-title"><span>Tarot (Rider-Waite-Smith)</span><span class="tt-price">$0</span></h4>
                <p class="tt-description">78-card Tarot deck</p>
            </div>
            <div class="tt-micro">
                <button class="btn btn-primary tt-free-btn"
                        data-deck-slug="tarot-rider-waite-smith"
                        data-item-name="Tarot (Rider-Waite-Smith)">FREE ITEM</button>
            </div>
        </div>
    `;
    document.body.appendChild(root);
    return root;
}


describe('WalletShop.initWalletShop', () => {
    let fixture;
    let _origFetch;
    let _origStripe;
    let _origShowGuard;
    let _origWalletTooltips;

    beforeEach(() => {
        fixture = _seedShopFixture();
        // Save+stub instead of spyOn — another spec elsewhere may already
        // have a fetch spy active, and Jasmine refuses to double-spy on
        // the same method ("fetch has already been spied upon"). Manual
        // save+restore is double-spy-safe + scoped per spec block.
        _origFetch = window.fetch;
        window.fetch = jasmine.createSpy('fetch').and.returnValue(
            Promise.resolve({ ok: true, json: () => Promise.resolve({}) })
        );
        _origStripe = window.Stripe;
        window.Stripe = jasmine.createSpy('Stripe').and.returnValue({
            confirmCardPayment: jasmine.createSpy('confirmCardPayment')
                .and.returnValue(Promise.resolve({ paymentIntent: { status: 'succeeded' } })),
        });
        _origShowGuard = window.showGuard;
        window.showGuard = jasmine.createSpy('showGuard');
        // Stub WalletTooltips.pin/unpin so we can assert the buy flow
        // pins the tooltip on guard-open + unpins on confirm/dismiss.
        _origWalletTooltips = window.WalletTooltips;
        window.WalletTooltips = {
            pin: jasmine.createSpy('pin'),
            unpin: jasmine.createSpy('unpin'),
        };
    });

    afterEach(() => {
        fixture.remove();
        window.fetch = _origFetch;
        window.Stripe = _origStripe;
        window.showGuard = _origShowGuard;
        window.WalletTooltips = _origWalletTooltips;
    });

    // ── T1 ── click on enabled BUY opens guard portal w. price prompt ───────
    it('T1: clicking BUY ITEM on tithe-1 opens guard with the item name + price', () => {
        WalletShop.initWalletShop();
        const btn = fixture.querySelector('#id_shop_tithe-1 .tt-buy-btn');
        btn.click();
        expect(window.showGuard).toHaveBeenCalled();
        const args = window.showGuard.calls.mostRecent().args;
        expect(args[0]).toBe(btn);  // anchor
        expect(args[1]).toContain('Tithe Token');
        expect(args[1]).toContain('$1');
        expect(typeof args[2]).toBe('function');  // onConfirm callback
    });

    // ── T2 ── capped items render no BUY btn at all (status-only microtext)
    it('T2: capped band-1 tile renders no BUY btn (only the "Already owned" pill)', () => {
        WalletShop.initWalletShop();
        // No `.tt-buy-btn` inside the band tile — capped items follow the
        // Game Kit "Equipped" / "In-Use: X" pattern: status text only, no
        // disabled action btn. Clicking anywhere on the tile is a no-op.
        const btns = fixture.querySelectorAll('#id_shop_band-1 .tt-buy-btn');
        expect(btns.length).toBe(0);
        // The microtext is rendered (cloned into the mini portal on hover).
        const ownedText = fixture.querySelector('#id_shop_band-1 .tt-already-owned');
        expect(ownedText).not.toBeNull();
        expect(ownedText.textContent.trim()).toBe('Already owned');
    });

    // ── T3 ── onConfirm callback POSTs to /shop/buy w. the right slug ───────
    it('T3: invoking the onConfirm callback POSTs shop_item_slug to /shop/buy', () => {
        // T3-local fetch override: never-resolving promise so the async
        // chain pauses at the FIRST await + doesn't progress into the
        // Stripe / confirm-endpoint dance that gets torn down by
        // afterEach. Without this, the unawaited continuation fires
        // `window.Stripe(pubKey)` after afterEach has restored
        // window.Stripe = undefined → unhandled-promise-rejection error.
        window.fetch = jasmine.createSpy('fetch').and.returnValue(new Promise(() => {}));
        WalletShop.initWalletShop();
        const btn = fixture.querySelector('#id_shop_tithe-1 .tt-buy-btn');
        btn.click();
        const onConfirm = window.showGuard.calls.mostRecent().args[2];
        onConfirm();
        expect(window.fetch).toHaveBeenCalled();
        const [url, opts] = window.fetch.calls.mostRecent().args;
        expect(url).toContain('/dashboard/wallet/shop/buy');
        expect(opts.method).toBe('POST');
        expect(opts.body).toContain('shop_item_slug=tithe-1');
    });

    // ── T4 ── initWalletShop is idempotent (calling twice doesn't double-wire) ─
    it('T4: calling initWalletShop twice does not double-fire the guard portal', () => {
        WalletShop.initWalletShop();
        WalletShop.initWalletShop();
        const btn = fixture.querySelector('#id_shop_tithe-1 .tt-buy-btn');
        btn.click();
        expect(window.showGuard.calls.count()).toBe(1);
    });

    // ── T5 ── no `.wallet-shop` in DOM → init is a quiet no-op ──────────────
    it('T5: initWalletShop with no `.wallet-shop` root does not throw', () => {
        fixture.remove();
        expect(() => WalletShop.initWalletShop()).not.toThrow();
    });

    // ── T6 ── clicking BUY pins the tooltip BEFORE opening the guard ──────
    it('T6: clicking BUY ITEM pins the WalletTooltips (so the item ' +
       'tooltip stays visible while the guard portal is open)', () => {
        WalletShop.initWalletShop();
        const btn = fixture.querySelector('#id_shop_tithe-1 .tt-buy-btn');
        btn.click();
        // pin() called BEFORE showGuard so the tooltip never has a
        // chance to hide between the click + the guard render.
        expect(window.WalletTooltips.pin).toHaveBeenCalled();
        expect(window.showGuard).toHaveBeenCalled();
    });

    // ── T7 ── guard onConfirm unpins the tooltip ───────────────────────────
    it('T7: invoking the onConfirm callback unpins the WalletTooltips', () => {
        // Block fetch like in T3 to avoid Stripe-chain async cleanup pollution.
        window.fetch = jasmine.createSpy('fetch').and.returnValue(new Promise(() => {}));
        WalletShop.initWalletShop();
        const btn = fixture.querySelector('#id_shop_tithe-1 .tt-buy-btn');
        btn.click();
        const onConfirm = window.showGuard.calls.mostRecent().args[2];
        onConfirm();
        expect(window.WalletTooltips.unpin).toHaveBeenCalled();
    });

    // ── T8 ── guard onDismiss unpins the tooltip ───────────────────────────
    it('T8: invoking the onDismiss callback unpins the WalletTooltips', () => {
        WalletShop.initWalletShop();
        const btn = fixture.querySelector('#id_shop_tithe-1 .tt-buy-btn');
        btn.click();
        const onDismiss = window.showGuard.calls.mostRecent().args[3];
        expect(typeof onDismiss).toBe('function');
        onDismiss();
        expect(window.WalletTooltips.unpin).toHaveBeenCalled();
    });

    // ── F1 ── FREE ITEM click POSTs deck_slug to /shop/claim ────────────────
    it('F1: clicking FREE ITEM POSTs deck_slug to /dashboard/wallet/shop/claim', () => {
        // Never-resolving fetch so `_doClaimFree` pauses at the await + never
        // reaches `window.location.reload()` (which would nav the spec page).
        window.fetch = jasmine.createSpy('fetch').and.returnValue(new Promise(() => {}));
        WalletShop.initWalletShop();
        const btn = fixture.querySelector('#id_shop_tarot-rider-waite-smith .tt-free-btn');
        btn.click();
        expect(window.fetch).toHaveBeenCalled();
        const [url, opts] = window.fetch.calls.mostRecent().args;
        expect(url).toContain('/dashboard/wallet/shop/claim');
        expect(opts.method).toBe('POST');
        expect(opts.body).toContain('deck_slug=tarot-rider-waite-smith');
    });

    // ── F2 ── FREE ITEM is a direct claim — no guard portal (it's free) ─────
    it('F2: clicking FREE ITEM does NOT open the guard portal (free = no confirm)', () => {
        window.fetch = jasmine.createSpy('fetch').and.returnValue(new Promise(() => {}));
        WalletShop.initWalletShop();
        const btn = fixture.querySelector('#id_shop_tarot-rider-waite-smith .tt-free-btn');
        btn.click();
        expect(window.fetch).toHaveBeenCalled();
        expect(window.showGuard).not.toHaveBeenCalled();
    });

    // ── F3 ── free-claim wiring is idempotent (init twice → one POST) ───────
    it('F3: calling initWalletShop twice does not double-fire the free claim', () => {
        window.fetch = jasmine.createSpy('fetch').and.returnValue(new Promise(() => {}));
        WalletShop.initWalletShop();
        WalletShop.initWalletShop();
        const btn = fixture.querySelector('#id_shop_tarot-rider-waite-smith .tt-free-btn');
        btn.click();
        expect(window.fetch.calls.count()).toBe(1);
    });
});
