import { describe, it, expect } from 'vitest';
import { TweenManager } from '../Tween';
import { linear, quadOut, cubicIn, sineInOut, quadIn, cubicOut } from '../easing';

describe('easing', () => {
    it('края области определения всегда 0 и 1', () => {
        for (const e of [linear, quadIn, quadOut, cubicIn, cubicOut, sineInOut]) {
            expect(e(0)).toBeCloseTo(0);
            expect(e(1)).toBeCloseTo(1);
        }
    });

    it('quadOut в середине быстрее linear', () => {
        expect(quadOut(0.5)).toBeGreaterThan(0.5);
        expect(cubicIn(0.5)).toBeLessThan(0.5);
    });
});

describe('TweenManager', () => {
    it('линейный твин доходит до цели за duration', () => {
        const tw = new TweenManager();
        const obj = { x: 0 };
        tw.to(obj, { x: 10 }, { duration: 2 });

        tw.update(1);
        expect(obj.x).toBeCloseTo(5);
        tw.update(1);
        expect(obj.x).toBeCloseTo(10);
        expect(tw.activeCount).toBe(0);
    });

    it('не проходит цель и вызывает onDone один раз', () => {
        const tw = new TweenManager();
        const obj = { y: 0 };
        let done = 0;
        tw.to(obj, { y: 8 }, { duration: 1, onDone: () => done++ });

        tw.update(0.7);
        expect(obj.y).toBeCloseTo(5.6);
        tw.update(0.7); // переполнение — зажимается
        expect(obj.y).toBeCloseTo(8);
        expect(done).toBe(1);

        tw.update(5);
        expect(done).toBe(1);
    });

    it('ease применяется к прогрессу', () => {
        const tw = new TweenManager();
        const obj = { v: 0 };
        tw.to(obj, { v: 100 }, { duration: 2, ease: cubicIn });
        tw.update(1);
        expect(obj.v).toBeCloseTo(100 * cubicIn(0.5));
        expect(obj.v).toBeLessThan(50);
    });

    it('delay откладывает старт: значения не трогаются до истечения', () => {
        const tw = new TweenManager();
        const obj = { x: 1 };
        tw.to(obj, { x: 5 }, { duration: 1, delay: 0.5 });

        tw.update(0.4);
        expect(obj.x).toBe(1); // ещё в delay
        tw.update(0.6); // delay закончился, первый кадр движения
        expect(obj.x).toBeGreaterThan(1);
        tw.update(1);
        expect(obj.x).toBeCloseTo(5);
    });

    it('delay(seconds, fn) вызывает fn один раз', () => {
        const tw = new TweenManager();
        let calls = 0;
        tw.delay(0.5, () => calls++);
        tw.update(0.3);
        expect(calls).toBe(0);
        tw.update(0.3);
        expect(calls).toBe(1);
        tw.update(1);
        expect(calls).toBe(1);
    });

    it('cancel останавливает твин', () => {
        const tw = new TweenManager();
        const obj = { x: 0 };
        const h = tw.to(obj, { x: 10 }, { duration: 1 });
        tw.update(0.5);
        h.cancel();
        tw.update(0.5);
        expect(obj.x).toBeCloseTo(5);
        expect(tw.activeCount).toBe(0);
    });

    it('cancelFor отменяет только твины целевого объекта', () => {
        const tw = new TweenManager();
        const a = { x: 0 };
        const b = { x: 0 };
        tw.to(a, { x: 10 }, { duration: 1 });
        tw.to(b, { x: 10 }, { duration: 1 });
        tw.cancelFor(a);
        tw.update(1);
        expect(a.x).toBe(0);
        expect(b.x).toBeCloseTo(10);
    });

    it('начальные значения снимаются при старте (после delay), а не при создании', () => {
        const tw = new TweenManager();
        const obj = { x: 0 };
        tw.to(obj, { x: 10 }, { duration: 1, delay: 1 });
        obj.x = 4; // изменили до старта
        tw.update(1.5); // delay прошёл, половина движения
        expect(obj.x).toBeCloseTo(7);
    });
});