import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { installMenuRepositionFix } from '../../resources/js/utils/menuRepositionFix';

// Ручной requestAnimationFrame: кадры прогоняем детерминированно.
let rafQueue: FrameRequestCallback[] = [];
function flushFrames(n: number): void {
    for (let i = 0; i < n; i++) {
        const batch = rafQueue;
        rafQueue = [];
        batch.forEach((cb) => cb(0));
    }
}

// Дать MutationObserver (микротаска jsdom) сработать.
const tick = (): Promise<void> => new Promise((r) => setTimeout(r, 0));

function makeMenu(): HTMLElement {
    const menu = document.createElement('div');
    menu.className = 'v-overlay v-menu';
    const content = document.createElement('div');
    content.className = 'v-overlay__content';
    menu.appendChild(content);
    return menu;
}

let teardown: (() => void) | undefined;
let rectWidth = 200;
let rectLeft = 100;
let origRect: typeof HTMLElement.prototype.getBoundingClientRect;
let resizeSpy: ReturnType<typeof vi.fn>;

beforeEach(() => {
    rafQueue = [];
    rectWidth = 200;
    rectLeft = 100;
    vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
        rafQueue.push(cb);
        return rafQueue.length;
    });
    origRect = HTMLElement.prototype.getBoundingClientRect;
    HTMLElement.prototype.getBoundingClientRect = function (): DOMRect {
        return {
            width: rectWidth,
            height: 10,
            left: rectLeft,
            top: 0,
            right: rectLeft + rectWidth,
            bottom: 10,
            x: rectLeft,
            y: 0,
            toJSON: () => ({}),
        } as DOMRect;
    };
    resizeSpy = vi.fn();
    window.addEventListener('resize', resizeSpy as unknown as EventListener);
});

afterEach(() => {
    teardown?.();
    teardown = undefined;
    HTMLElement.prototype.getBoundingClientRect = origRect;
    window.removeEventListener('resize', resizeSpy as unknown as EventListener);
    vi.unstubAllGlobals();
    document.body.innerHTML = '';
});

describe('installMenuRepositionFix', () => {
    it('при появлении меню стабилизирует позицию и шлёт один resize', async () => {
        teardown = installMenuRepositionFix();
        document.body.appendChild(makeMenu());
        await tick();
        flushFrames(6);
        expect(resizeSpy).toHaveBeenCalledTimes(1);
    });

    it('на посторонний узел не реагирует', async () => {
        teardown = installMenuRepositionFix();
        const other = document.createElement('div');
        other.className = 'some-card';
        document.body.appendChild(other);
        await tick();
        flushFrames(6);
        expect(resizeSpy).not.toHaveBeenCalled();
    });

    it('идемпотентна: двойная установка не даёт двойной resize', async () => {
        teardown = installMenuRepositionFix();
        installMenuRepositionFix(); // второй вызов — noop
        document.body.appendChild(makeMenu());
        await tick();
        flushFrames(6);
        expect(resizeSpy).toHaveBeenCalledTimes(1);
    });

    it('предохранитель: если геометрия не устаканилась — не виснет и не шлёт resize', async () => {
        rectWidth = 0; // контент «нулевой» → условие стабилизации не выполняется
        teardown = installMenuRepositionFix();
        document.body.appendChild(makeMenu());
        await tick();
        flushFrames(120); // больше предохранителя (90 кадров)
        expect(resizeSpy).not.toHaveBeenCalled();
        expect(rafQueue.length).toBe(0); // цикл остановился, не зациклился
    });
});
