// ── BurgerSpec.js ────────────────────────────────────────────────────────────
//
// Unit specs for burger-btn.js — the room.html corner-menu btn + its 5-btn
// fanning arc (sky, earth, sea, voice, text).
//
// DOM contract assumed by the module:
//   #id_burger_btn   — the burger btn
//   #id_burger_fan   — the fan container (sibling)
//   .burger-fan-btn  — each sub-btn (5 of them)
//   #id_kit_btn      — kit btn (optional)
//   #id_kit_bag_dialog — kit dialog (optional)
//   #id_bud_btn      — bud btn (optional)
//
// Public API under test (window.bindBurger):
//   var ac = bindBurger();  // returns an AbortController for cleanup
//
// Behaviours:
//   • Click toggles .active class on #id_burger_btn.
//   • Escape closes when open.
//   • Click outside closes when open.
//   • Opening burger closes the kit dialog + the bud panel if either is open.
//
// ─────────────────────────────────────────────────────────────────────────────

describe("Burger", () => {
    let burgerBtn, fan, ac;

    beforeEach(() => {
        burgerBtn = document.createElement("button");
        burgerBtn.id = "id_burger_btn";
        document.body.appendChild(burgerBtn);

        fan = document.createElement("div");
        fan.id = "id_burger_fan";
        document.body.appendChild(fan);

        ac = bindBurger();
    });

    afterEach(() => {
        if (ac) ac.abort();
        burgerBtn.remove();
        fan.remove();
        // Clean up any test-added kit/bud elements + classes.
        ["id_kit_btn", "id_kit_bag_dialog", "id_bud_btn"].forEach(id => {
            const el = document.getElementById(id);
            if (el) el.remove();
        });
        document.documentElement.classList.remove("bud-open");
    });

    describe("bindBurger()", () => {
        it("returns an AbortController when DOM is present", () => {
            expect(ac).not.toBeNull();
            expect(ac.abort).toBeDefined();
        });

        it("returns null when #id_burger_btn is absent", () => {
            burgerBtn.remove();
            const ac2 = bindBurger();
            expect(ac2).toBeNull();
        });

        it("returns null when #id_burger_fan is absent", () => {
            fan.remove();
            const ac2 = bindBurger();
            expect(ac2).toBeNull();
        });
    });

    describe("click toggle", () => {
        it("first click adds .active to #id_burger_btn", () => {
            burgerBtn.click();
            expect(burgerBtn.classList.contains("active")).toBe(true);
        });

        it("first click sets aria-expanded='true'", () => {
            burgerBtn.click();
            expect(burgerBtn.getAttribute("aria-expanded")).toBe("true");
        });

        it("first click sets fan aria-hidden='false'", () => {
            burgerBtn.click();
            expect(fan.getAttribute("aria-hidden")).toBe("false");
        });

        it("second click removes .active (closes)", () => {
            burgerBtn.click();
            burgerBtn.click();
            expect(burgerBtn.classList.contains("active")).toBe(false);
        });

        it("second click resets aria-expanded='false'", () => {
            burgerBtn.click();
            burgerBtn.click();
            expect(burgerBtn.getAttribute("aria-expanded")).toBe("false");
        });
    });

    describe("Escape key", () => {
        it("closes burger when open", () => {
            burgerBtn.click();
            const evt = new KeyboardEvent("keydown", { key: "Escape" });
            document.dispatchEvent(evt);
            expect(burgerBtn.classList.contains("active")).toBe(false);
        });

        it("no-op when burger already closed", () => {
            const evt = new KeyboardEvent("keydown", { key: "Escape" });
            document.dispatchEvent(evt);
            expect(burgerBtn.classList.contains("active")).toBe(false);
        });
    });

    describe("click-outside", () => {
        it("closes burger on click outside btn + fan", () => {
            burgerBtn.click();
            const outsider = document.createElement("div");
            document.body.appendChild(outsider);
            outsider.click();
            expect(burgerBtn.classList.contains("active")).toBe(false);
            outsider.remove();
        });

        it("does NOT close on click inside fan", () => {
            burgerBtn.click();
            const subBtn = document.createElement("button");
            subBtn.className = "burger-fan-btn";
            fan.appendChild(subBtn);
            subBtn.click();
            expect(burgerBtn.classList.contains("active")).toBe(true);
        });
    });

    describe("opening burger closes other corner panels", () => {
        it("clicks #id_kit_btn when kit dialog is open", () => {
            const kitBtn = document.createElement("button");
            kitBtn.id = "id_kit_btn";
            const kitClickSpy = jasmine.createSpy("kitClick");
            kitBtn.addEventListener("click", kitClickSpy);
            document.body.appendChild(kitBtn);

            const dialog = document.createElement("dialog");
            dialog.id = "id_kit_bag_dialog";
            dialog.setAttribute("open", "");
            document.body.appendChild(dialog);

            burgerBtn.click();
            expect(kitClickSpy).toHaveBeenCalled();
        });

        it("does NOT click #id_kit_btn when kit dialog is closed", () => {
            const kitBtn = document.createElement("button");
            kitBtn.id = "id_kit_btn";
            const kitClickSpy = jasmine.createSpy("kitClick");
            kitBtn.addEventListener("click", kitClickSpy);
            document.body.appendChild(kitBtn);

            const dialog = document.createElement("dialog");
            dialog.id = "id_kit_bag_dialog";
            // no open attribute
            document.body.appendChild(dialog);

            burgerBtn.click();
            expect(kitClickSpy).not.toHaveBeenCalled();
        });

        it("clicks #id_bud_btn when html.bud-open is set", () => {
            const budBtn = document.createElement("button");
            budBtn.id = "id_bud_btn";
            const budClickSpy = jasmine.createSpy("budClick");
            budBtn.addEventListener("click", budClickSpy);
            document.body.appendChild(budBtn);

            document.documentElement.classList.add("bud-open");

            burgerBtn.click();
            expect(budClickSpy).toHaveBeenCalled();
        });

        it("does NOT click #id_bud_btn when html.bud-open is absent", () => {
            const budBtn = document.createElement("button");
            budBtn.id = "id_bud_btn";
            const budClickSpy = jasmine.createSpy("budClick");
            budBtn.addEventListener("click", budClickSpy);
            document.body.appendChild(budBtn);

            burgerBtn.click();
            expect(budClickSpy).not.toHaveBeenCalled();
        });

        it("ignores missing kit/bud btns w.o. error", () => {
            // No kit, no bud — opening should still succeed.
            expect(() => burgerBtn.click()).not.toThrow();
            expect(burgerBtn.classList.contains("active")).toBe(true);
        });
    });

    describe("inactive sub-btn flash", () => {
        let subBtn;

        beforeEach(() => {
            subBtn = document.createElement("button");
            subBtn.className = "burger-fan-btn";
            fan.appendChild(subBtn);
            jasmine.clock().install();
        });

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

        it("adds .flash-inactive to inactive sub-btn on click", () => {
            subBtn.click();
            expect(subBtn.classList.contains("flash-inactive")).toBe(true);
        });

        it("removes .flash-inactive after the ON window (~180ms)", () => {
            subBtn.click();
            jasmine.clock().tick(200);
            expect(subBtn.classList.contains("flash-inactive")).toBe(false);
        });

        it("pulses twice — second ON window arrives after off+on cycle (~480ms)", () => {
            subBtn.click();
            // First pulse cycle: 180ms ON + 120ms OFF = 300ms. Second pulse ON starts.
            jasmine.clock().tick(310);
            expect(subBtn.classList.contains("flash-inactive")).toBe(true);
        });

        it("settles back to default after 2 pulses (~700ms total)", () => {
            subBtn.click();
            jasmine.clock().tick(800);
            expect(subBtn.classList.contains("flash-inactive")).toBe(false);
        });

        it("does NOT flash when sub-btn carries .active class", () => {
            subBtn.classList.add("active");
            subBtn.click();
            expect(subBtn.classList.contains("flash-inactive")).toBe(false);
        });
    });

    describe("AbortController teardown", () => {
        it("detaches click handler when ac.abort() is called", () => {
            ac.abort();
            burgerBtn.click();
            expect(burgerBtn.classList.contains("active")).toBe(false);
        });
    });
});

// Voice persistence across my_sea reloads — bindVoiceBtn remembers the active
// room in sessionStorage + silently re-joins it on the next page if voice is
// still available (2026-05-29).
describe("voice auto-rejoin", () => {
    let vbtn, origVR, joinSpy;

    beforeEach(() => {
        try { sessionStorage.removeItem("mysea-voice-room"); } catch (e) {}
        vbtn = document.createElement("button");
        vbtn.id = "id_voice_btn";
        vbtn.classList.add("active");
        vbtn.setAttribute("data-room-id", "mysea-abc");
        document.body.appendChild(vbtn);
        origVR = window.VoiceRoom;
        joinSpy = jasmine.createSpy("join").and.returnValue(Promise.resolve());
        window.VoiceRoom = { join: joinSpy };
    });

    afterEach(() => {
        window.VoiceRoom = origVR;
        vbtn.remove();
        try { sessionStorage.removeItem("mysea-voice-room"); } catch (e) {}
    });

    it("rejoins when the remembered room matches the active voice btn", () => {
        sessionStorage.setItem("mysea-voice-room", "mysea-abc");
        bindVoiceBtn();
        expect(joinSpy).toHaveBeenCalledWith("mysea-abc");
    });

    it("does NOT rejoin when the remembered room is a different sea", () => {
        sessionStorage.setItem("mysea-voice-room", "mysea-other");
        bindVoiceBtn();
        expect(joinSpy).not.toHaveBeenCalled();
    });

    it("does NOT rejoin when voice is unavailable (btn inactive)", () => {
        vbtn.classList.remove("active");
        sessionStorage.setItem("mysea-voice-room", "mysea-abc");
        bindVoiceBtn();
        expect(joinSpy).not.toHaveBeenCalled();
    });

    it("remembers the room on a manual join; mySeaVoiceForget clears it", () => {
        bindVoiceBtn();
        expect(joinSpy).not.toHaveBeenCalled();   // nothing remembered yet
        vbtn.click();
        expect(joinSpy).toHaveBeenCalledWith("mysea-abc");
        expect(sessionStorage.getItem("mysea-voice-room")).toBe("mysea-abc");
        window.mySeaVoiceForget();
        expect(sessionStorage.getItem("mysea-voice-room")).toBeNull();
    });
});

// Voice mute persistence + 3-min auto-disconnect (2026-05-30) — the mute is
// stored server-side (User.voice_muted_at, POSTed to /voice/mute) so it survives
// in-sea nav/refresh: the auto-rejoin re-applies it, and a mute held 3 min
// auto-disconnects. Clock + Date are faked so timer behaviour is deterministic.
describe("voice mute persistence", () => {
    let vbtn, origVR, origFetch, joinSpy, setMutedSpy, leaveSpy, fetchSpy;
    const BASE = new Date(2026, 0, 1, 12, 0, 0);

    beforeEach(() => {
        jasmine.clock().install();
        jasmine.clock().mockDate(BASE);
        try { sessionStorage.removeItem("mysea-voice-room"); } catch (e) {}
        vbtn = document.createElement("button");
        vbtn.id = "id_voice_btn";
        vbtn.classList.add("active");
        vbtn.setAttribute("data-room-id", "mysea-abc");
        document.body.appendChild(vbtn);
        origVR = window.VoiceRoom;
        joinSpy = jasmine.createSpy("join").and.returnValue({ catch: () => {} });
        setMutedSpy = jasmine.createSpy("setMuted");
        leaveSpy = jasmine.createSpy("leave");
        window.VoiceRoom = {
            join: joinSpy, setMuted: setMutedSpy, leave: leaveSpy,
            toggleMute: () => true,
        };
        origFetch = window.fetch;
        fetchSpy = jasmine.createSpy("fetch").and.returnValue({ catch: () => {} });
        window.fetch = fetchSpy;
    });

    afterEach(() => {
        window.VoiceRoom = origVR;
        window.fetch = origFetch;
        vbtn.remove();
        try { sessionStorage.removeItem("mysea-voice-room"); } catch (e) {}
        jasmine.clock().uninstall();
    });

    describe("muteRemainingMs()", () => {
        it("returns the full 3-min window at t=0", () => {
            expect(window._muteRemainingMs(1000, 1000)).toBe(180000);
        });
        it("counts down as time elapses", () => {
            expect(window._muteRemainingMs(1000, 61000)).toBe(120000);
        });
        it("goes non-positive once 3 min elapse", () => {
            expect(window._muteRemainingMs(1000, 201000)).toBeLessThanOrEqual(0);
        });
    });

    it("re-applies the persisted mute on auto-rejoin within the window", () => {
        sessionStorage.setItem("mysea-voice-room", "mysea-abc");
        vbtn.setAttribute("data-voice-muted-at",
            new Date(BASE.getTime() - 60000).toISOString());
        bindVoiceBtn();
        expect(setMutedSpy).toHaveBeenCalledWith(true);
        expect(vbtn.classList.contains("muted")).toBe(true);
        expect(joinSpy).toHaveBeenCalledWith("mysea-abc");
    });

    it("does NOT re-mute on rejoin when no mute was persisted", () => {
        sessionStorage.setItem("mysea-voice-room", "mysea-abc");
        bindVoiceBtn();
        expect(setMutedSpy).not.toHaveBeenCalled();
        expect(vbtn.classList.contains("muted")).toBe(false);
        expect(joinSpy).toHaveBeenCalledWith("mysea-abc");
    });

    it("auto-disconnects (no rejoin + clears) when the persisted mute already exceeded 3 min", () => {
        sessionStorage.setItem("mysea-voice-room", "mysea-abc");
        vbtn.setAttribute("data-voice-muted-at",
            new Date(BASE.getTime() - 200000).toISOString());
        bindVoiceBtn();
        expect(joinSpy).not.toHaveBeenCalled();
        expect(vbtn.classList.contains("muted")).toBe(false);
        expect(fetchSpy).toHaveBeenCalled();
        const opts = fetchSpy.calls.mostRecent().args[1];
        expect(JSON.parse(opts.body).muted).toBe(false);
    });

    it("persists muted=true on a toggle, then auto-disconnects after 3 min", () => {
        bindVoiceBtn();
        vbtn.click();                 // manual join → in-call (clears any stale)
        fetchSpy.calls.reset();
        vbtn.click();                 // in-call → toggle mute
        expect(vbtn.classList.contains("muted")).toBe(true);
        expect(fetchSpy.calls.mostRecent().args[0]).toBe("/voice/mute");
        expect(JSON.parse(fetchSpy.calls.mostRecent().args[1].body).muted).toBe(true);
        jasmine.clock().tick(180001);
        expect(leaveSpy).toHaveBeenCalled();
        expect(vbtn.classList.contains("muted")).toBe(false);
        expect(vbtn.dataset.inCall).toBeUndefined();
    });

    it("clears stale mute (POST muted=false) on a fresh manual join", () => {
        bindVoiceBtn();
        vbtn.click();                 // fresh manual join
        expect(fetchSpy).toHaveBeenCalled();
        expect(JSON.parse(fetchSpy.calls.mostRecent().args[1].body).muted).toBe(false);
    });
});
