import { describe, expect, it } from 'vitest';
import { GameState } from '../../core/GameState';
import { linear } from '../../core/easing';
import { Camera } from '../../render/Camera';
import { CutsceneRunner } from '../CutsceneRunner';
import { applyEase } from '../tween';

/** Продвинуть раннер: dt N раз. */
function tick(runner: CutsceneRunner, dt: number, times: number): void {
    for (let i = 0; i < times; i++) runner.update(dt);
}

/** Фиктивный «взгляд»: камера с заданным центром. */
function cameraAt(x: number, y: number): Camera {
    const cam = new Camera(480, 270);
    cam.x = x;
    cam.y = y;
    return cam;
}

describe('CutsceneRunner — последовательность шагов', () => {
    it('шаги выполняются по порядку, wait держит паузу', () => {
        const runner = new CutsceneRunner();
        const order: string[] = [];
        runner.play([
            { kind: 'call', fn: () => order.push('a') },
            { kind: 'wait', seconds: 0.5 },
            { kind: 'call', fn: () => order.push('b') }
        ]);
        // Первый шаг стартовал сразу, второй ещё ждёт.
        expect(order).toEqual(['a']);
        expect(runner.active).toBe(true);
        tick(runner, 0.1, 4); // 0.4с — всё ещё wait
        expect(order).toEqual(['a']);
        tick(runner, 0.2, 1); // 0.6с — wait кончился
        expect(order).toEqual(['a', 'b']);
        expect(runner.active).toBe(false);
    });

    it('шаг без seconds параллелен: следующий стартует сразу', () => {
        const runner = new CutsceneRunner();
        const order: string[] = [];
        runner.play([
            { kind: 'burst', fn: () => order.push('burst') },
            { kind: 'call', fn: () => order.push('call') }
        ]);
        // Шаги без длительности не блокируют: оба стартовали в том же тике.
        expect(order).toEqual(['burst', 'call']);
        expect(runner.active).toBe(false);
    });

    it('cameraMove плавно ведёт камеру к точке', () => {
        const runner = new CutsceneRunner();
        const cam = cameraAt(0, 0);
        runner.play([{ kind: 'cameraMove', x: 10, y: -20, seconds: 1, camera: cam }]);
        tick(runner, 0.25, 2); // половина пути
        expect(cam.x).toBeGreaterThan(0);
        expect(cam.x).toBeLessThan(10);
        expect(cam.y).toBeLessThan(0);
        tick(runner, 0.25, 3); // время вышло — точно в точке
        expect(cam.x).toBe(10);
        expect(cam.y).toBe(-20);
        expect(runner.active).toBe(false);
    });

    it('setFlag поднимает флаги, onComplete вызывается в конце', () => {
        const runner = new CutsceneRunner();
        const state = new GameState();
        let done = 0;
        runner.onComplete = () => done++;
        runner.play([
            { kind: 'setFlag', flag: 'flag1', state },
            { kind: 'setFlag', flag: 'flag2', state }
        ]);
        expect(state.hasFlag('flag1')).toBe(true);
        expect(state.hasFlag('flag2')).toBe(true);
        expect(done).toBe(1);
    });

    it('onStep наблюдает порядок шагов', () => {
        const seen: string[] = [];
        const runner = new CutsceneRunner((s) => seen.push(s.kind));
        runner.play([
            { kind: 'call', fn: () => undefined, seconds: 0.1 },
            { kind: 'wait', seconds: 0.1 }
        ]);
        // Первый шаг стартовал; второй — после его длительности.
        expect(seen).toEqual(['call']);
        tick(runner, 0.2, 1);
        expect(seen).toEqual(['call', 'wait']);
    });

    it('until ждёт внешнее событие и завершается по предикату', () => {
        const runner = new CutsceneRunner();
        const order: string[] = [];
        let ready = false;
        runner.play([
            { kind: 'until', predicate: () => ready, label: 'событие' },
            { kind: 'call', fn: () => order.push('after') }
        ]);
        tick(runner, 0.1, 5); // событие не наступило — стоим на месте
        expect(order).toEqual([]);
        expect(runner.active).toBe(true);
        ready = true;
        runner.update(0.1);
        expect(order).toEqual(['after']);
        expect(runner.active).toBe(false);
    });

    it('until с уже выполненным предикатом не блокирует', () => {
        const runner = new CutsceneRunner();
        const order: string[] = [];
        runner.play([
            { kind: 'until', predicate: () => true },
            { kind: 'call', fn: () => order.push('next') }
        ]);
        expect(order).toEqual(['next']);
        expect(runner.active).toBe(false);
    });

    it('until с timeout завершается принудительно по истечении', () => {
        const runner = new CutsceneRunner();
        let done = 0;
        runner.onComplete = () => done++;
        runner.play([{ kind: 'until', predicate: () => false, timeout: 0.3 }]);
        tick(runner, 0.1, 2); // 0.2с — ещё ждём
        expect(runner.active).toBe(true);
        tick(runner, 0.1, 1); // 0.3с — timeout
        expect(runner.active).toBe(false);
        expect(done).toBe(1);
    });

    it('moveView плавно ведёт вьюху в локальных координатах родителя', () => {
        const runner = new CutsceneRunner();
        const view = { position: { x: 0, y: 0 } };
        runner.play([{ kind: 'moveView', view, x: 40, y: -10, seconds: 1 }]);
        tick(runner, 0.25, 2); // половина пути
        expect(view.position.x).toBeGreaterThan(0);
        expect(view.position.x).toBeLessThan(40);
        expect(view.position.y).toBeLessThan(0);
        tick(runner, 0.25, 3); // время вышло — точно в точке
        expect(view.position.x).toBe(40);
        expect(view.position.y).toBe(-10);
        expect(runner.active).toBe(false);
    });

    it('moveView с кастомным ease', () => {
        const runner = new CutsceneRunner();
        const view = { position: { x: 0, y: 0 } };
        runner.play([{ kind: 'moveView', view, x: 10, y: 0, seconds: 1, ease: (t) => t * t }]);
        runner.update(0.5);
        expect(view.position.x).toBeCloseTo(2.5); // quadIn(0.5) = 0.25
    });

    it('skip дожимает сцену до конца: движение — в конечные точки', () => {
        const runner = new CutsceneRunner();
        const cam = cameraAt(0, 0);
        const view = { position: { x: 0, y: 0 } };
        const order: string[] = [];
        runner.onComplete = () => order.push('done');
        runner.play([
            { kind: 'moveView', view, x: 12, y: 6, seconds: 5 },
            { kind: 'cameraMove', x: 3, y: 3, seconds: 5, camera: cam },
            { kind: 'call', fn: () => order.push('call') }
        ]);
        runner.skip();
        expect(order).toEqual(['call', 'done']);
        expect(view.position.x).toBe(12);
        expect(view.position.y).toBe(6);
        expect(cam.x).toBe(3);
        expect(cam.y).toBe(3);
        expect(runner.active).toBe(false);
    });

    it('skip форсирует until (иначе скип висел бы навсегда)', () => {
        const runner = new CutsceneRunner();
        runner.play([
            { kind: 'call', fn: () => undefined },
            { kind: 'until', predicate: () => false }, // событие никогда не наступит
            { kind: 'call', fn: () => undefined, seconds: 0.2 }
        ]);
        runner.skip();
        expect(runner.active).toBe(false);
    });

    it('cameraZoom ведёт зум камеры; seconds: 0 — мгновенно', () => {
        const runner = new CutsceneRunner();
        const cam = cameraAt(0, 0);
        const order: string[] = [];
        runner.play([
            { kind: 'cameraZoom', camera: cam, z: 2, seconds: 1 },
            { kind: 'call', fn: () => order.push('after') }
        ]);
        runner.update(0.5);
        cam.update(0.5); // анимацию зума тикает камера (движок — каждый шаг)
        expect(cam.zoomLevel).toBeGreaterThan(1);
        expect(cam.zoomLevel).toBeLessThan(2);
        expect(order).toEqual([]); // зум ещё идёт — сцена не двинулась
        runner.update(0.5);
        cam.update(0.5);
        expect(cam.zoomLevel).toBe(2);
        expect(order).toEqual(['after']);
    });

    it('cameraZoom без seconds — параллельный шаг (не блокирует)', () => {
        const runner = new CutsceneRunner();
        const cam = cameraAt(0, 0);
        runner.play([{ kind: 'cameraZoom', camera: cam, z: 2 }]);
        expect(cam.zoomLevel).toBe(2);
        expect(runner.active).toBe(false);
    });
});

describe('tween — applyEase', () => {
    it('интерполирует точку по кривой easing', () => {
        const from = { x: 0, y: 0 };
        const to = { x: 10, y: -20 };
        const mid = applyEase(from, to, 0.5, linear);
        expect(mid.x).toBeCloseTo(5);
        expect(mid.y).toBeCloseTo(-10);
    });

    it('клэмпит k за пределы 0..1', () => {
        const end = applyEase({ x: 0, y: 0 }, { x: 8, y: 4 }, 2, linear);
        expect(end.x).toBe(8);
        expect(end.y).toBe(4);
        const start = applyEase({ x: 0, y: 0 }, { x: 8, y: 4 }, -1, linear);
        expect(start.x).toBe(0);
    });
});