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

describe('StateMachine', () => {
    it('change вызывает exit предыдущего и enter нового', () => {
        const log: string[] = [];
        const sm = new StateMachine();
        sm.add('idle', {
            enter: () => log.push('enter:idle'),
            exit: () => log.push('exit:idle')
        });
        sm.add('walk', {
            enter: () => log.push('enter:walk')
        });

        sm.change('idle');
        sm.change('walk');
        expect(log).toEqual(['enter:idle', 'exit:idle', 'enter:walk']);
        expect(sm.current).toBe('walk');
    });

    it('повторный вход в то же состояние игнорируется', () => {
        let enters = 0;
        const sm = new StateMachine();
        sm.add('a', { enter: () => enters++ });
        sm.change('a');
        sm.change('a');
        expect(enters).toBe(1);
    });

    it('handleEvent выполняет переход по таблице', () => {
        const sm = new StateMachine();
        sm.add('patrol', {});
        sm.add('chase', {});
        sm.transition('patrol', 'seePlayer', 'chase');
        sm.transition('chase', 'losePlayer', 'patrol');

        sm.change('patrol');
        sm.handleEvent('seePlayer');
        expect(sm.current).toBe('chase');
        sm.handleEvent('losePlayer');
        expect(sm.current).toBe('patrol');
    });

    it('неизвестное событие попадает в onUnhandled', () => {
        let handled: string | undefined;
        const sm = new StateMachine((state, event) => (handled = `${state}:${event}`));
        sm.add('idle', {});
        sm.change('idle');
        sm.handleEvent('boom');
        expect(handled).toBe('idle:boom');
        expect(sm.current).toBe('idle');
    });

    it('update тикает текущее состояние и считает время в нём', () => {
        let t = 0;
        const sm = new StateMachine();
        sm.add('a', { update: (dt) => (t += dt) });
        sm.add('b', {});
        sm.change('a');
        sm.update(0.5);
        sm.update(0.5);
        expect(t).toBeCloseTo(1);
        expect(sm.time).toBeCloseTo(1);
        sm.change('b');
        expect(sm.time).toBe(0);
    });

    it('change в неизвестное состояние бросает', () => {
        const sm = new StateMachine();
        expect(() => sm.change('nope')).toThrow();
    });

    it('transitionAny срабатывает из любого состояния', () => {
        const sm = new StateMachine();
        sm.add('idle', {});
        sm.add('chase', {});
        sm.add('windup', {});
        sm.add('dead', { enter: () => deadEntered++ });
        sm.transition('idle', 'wake', 'chase');
        sm.transitionAny('die', 'dead');
        let deadEntered = 0;

        sm.change('idle');
        sm.handleEvent('die');
        expect(sm.current).toBe('dead');
        expect(deadEntered).toBe(1);

        // и после смены состояния переход «из любого» продолжает работать
        sm.change('windup');
        sm.handleEvent('die');
        expect(sm.current).toBe('dead');
        expect(deadEntered).toBe(2);
    });

    it('обычный переход приоритетнее transitionAny', () => {
        const sm = new StateMachine();
        sm.add('a', {});
        sm.add('b', {});
        sm.add('c', {});
        sm.transitionAny('go', 'b');
        sm.transition('a', 'go', 'c');
        sm.change('a');
        sm.handleEvent('go');
        expect(sm.current).toBe('c');
    });

    it('transitionAny в неизвестное состояние бросает', () => {
        const sm = new StateMachine();
        sm.add('a', {});
        expect(() => sm.transitionAny('go', 'nope')).toThrow();
    });

    it('transitionAny не даёт повторного входа в то же состояние', () => {
        let enters = 0;
        const sm = new StateMachine();
        sm.add('loop', { enter: () => enters++ });
        sm.transitionAny('again', 'loop');
        sm.change('loop');
        sm.handleEvent('again');
        expect(enters).toBe(1);
    });
});