import { describe, expect, it, vi } from 'vitest';
import { GameLoop } from '../GameLoop';

/** Заглушки rAF: frame() продвигает кадры вручную. */
function installRaf(): (advanceMs: number) => void {
    let now = 0;
    const callbacks: ((t: number) => void)[] = [];
    vi.stubGlobal('requestAnimationFrame', (cb: (t: number) => void) => {
        callbacks.push(cb);
        return callbacks.length;
    });
    vi.stubGlobal('cancelAnimationFrame', () => undefined);
    vi.stubGlobal('performance', { now: () => now });
    return (advanceMs: number) => {
        now += advanceMs;
        const list = [...callbacks];
        callbacks.length = 0;
        for (const cb of list) cb(now);
    };
}

describe('GameLoop — ручной режим', () => {
    it('setManual(true) гасит кадры, resetTiming не даёт догонять', () => {
        const frame = installRaf();
        const updates: number[] = [];
        const loop = new GameLoop(60, {
            update: (dt) => updates.push(dt),
            render: () => undefined
        });
        loop.start();
        frame(1000); // ~60 шагов сгущаются в максимум 5
        expect(updates.length).toBe(5);

        loop.setManual(true);
        frame(1000);
        expect(updates.length).toBe(5); // кадры в ручном режиме пустые

        loop.setManual(false);
        frame(20); // resetTiming в setManual(true) обнулил аккумулятор — без «догоняния»
        expect(updates.length).toBe(6); // ровно один шаг, а не 5+5
        loop.stop();
        vi.unstubAllGlobals();
    });
});

describe('GameLoop — лок кадров (maxFps)', () => {
    it('рендер не чаще лимита, шаги копятся и идут пачкой', () => {
        const frame = installRaf();
        const updates: number[] = [];
        let renders = 0;
        const loop = new GameLoop(
            60,
            {
                update: (dt) => updates.push(dt),
                render: () => renders++
            },
            { maxFps: 30 }
        );
        loop.start();

        frame(17); // 17 < 32-1: кадр срезан, ни рендера, ни шагов
        expect(renders).toBe(0);
        expect(updates.length).toBe(0);

        frame(17); // 34 мс с прошлого рендера: один кадр = 2 шага 60 Гц
        expect(renders).toBe(1);
        expect(updates.length).toBe(2);
        expect(updates[0]).toBeCloseTo(1 / 60);

        frame(17); // снова срезан
        frame(100); // крупный скачок: догоняет пачкой до maxStepsPerFrame
        expect(renders).toBe(2);
        expect(updates.length).toBe(7); // 2 + 5 (лимит за кадр)
        loop.stop();
        vi.unstubAllGlobals();
    });

    it('maxFps = 0 (по умолчанию) — без лока: рендер на каждом rAF', () => {
        const frame = installRaf();
        let renders = 0;
        const loop = new GameLoop(60, {
            update: () => undefined,
            render: () => renders++
        });
        loop.start();
        frame(16);
        frame(16);
        expect(renders).toBe(2);
        loop.stop();
        vi.unstubAllGlobals();
    });
});