import { describe, it, expect, beforeEach } from 'vitest';
import { InputManager, type PointerState } from '../InputManager';

/**
 * Фейковые canvas/window: достаточно, чтобы InputManager работал без браузера.
 * События диспатчим напрямую в сохранённые обработчики.
 */
type Handler = (e: unknown) => void;

function makeFakeEnv() {
    const canvasListeners = new Map<string, Handler>();
    const windowListeners = new Map<string, Handler>();
    const canvas = {
        addEventListener: (type: string, fn: Handler) => canvasListeners.set(type, fn),
        removeEventListener: (type: string) => canvasListeners.delete(type),
        getBoundingClientRect: () => ({ left: 0, top: 0, width: 480, height: 270 })
    };
    (globalThis as Record<string, unknown>).window = {
        addEventListener: (type: string, fn: Handler) => windowListeners.set(type, fn),
        removeEventListener: (type: string) => windowListeners.delete(type)
    };
    return { canvas, canvasListeners, windowListeners } as const;
}

function keyEvent(code: string) {
    return { code, pointerType: 'mouse', clientX: 0, clientY: 0 };
}

function pointerEvent(x: number, y: number, type = 'mouse') {
    return { clientX: x, clientY: y, pointerType: type };
}

describe('InputManager (фейковый canvas/window)', () => {
    let input: InputManager;
    let env: ReturnType<typeof makeFakeEnv>;

    beforeEach(() => {
        env = makeFakeEnv();
        input = new InputManager(env.canvas as unknown as HTMLCanvasElement, 480, 270);
        input.bindActions({ attack: ['Space'], advance: ['Enter'] });
    });

    it('justPressed/justReleased по клавиатуре, endTick очищает', () => {
        const kd = env.windowListeners.get('keydown')!;
        const ku = env.windowListeners.get('keyup')!;

        kd(keyEvent('Space'));
        expect(input.isActionJustPressed('attack')).toBe(true);
        expect(input.isActionActive('attack')).toBe(true);
        input.endTick();

        expect(input.isActionJustPressed('attack')).toBe(false);
        expect(input.isActionActive('attack')).toBe(true);

        ku(keyEvent('Space'));
        expect(input.wasKeyReleased('Space')).toBe(true);
        expect(input.isActionJustReleased('attack')).toBe(true);
        expect(input.isActionActive('attack')).toBe(false);
        input.endTick();
        expect(input.isActionJustReleased('attack')).toBe(false);
    });

    it('isActionJustReleased не путает действия', () => {
        const kd = env.windowListeners.get('keydown')!;
        const ku = env.windowListeners.get('keyup')!;
        kd(keyEvent('Space'));
        input.endTick();
        ku(keyEvent('Space'));
        expect(input.isActionJustReleased('attack')).toBe(true);
        expect(input.isActionJustReleased('advance')).toBe(false);
    });

    it('одна клавиша маппится на несколько действий', () => {
        const kd = env.windowListeners.get('keydown')!;
        input.bindActions({ advance: ['Space'] }); // Space уже занят attack
        kd(keyEvent('Space'));
        expect(input.isActionJustPressed('attack')).toBe(true);
        expect(input.isActionJustPressed('advance')).toBe(true);
        expect(input.isActionActive('attack')).toBe(true);
        expect(input.isActionActive('advance')).toBe(true);
    });

    it('указатель: downTicks копится, justPressed сбрасывается в endTick', () => {
        const pd = env.canvasListeners.get('pointerdown')!;
        const pu = env.canvasListeners.get('pointerup')!;

        pd(pointerEvent(240, 135));
        const p = input.getPointer() as PointerState;
        expect(p.justPressed).toBe(true);
        expect(p.downTicks).toBe(0);
        expect(p.x).toBe(240);
        input.endTick();

        expect(p.justPressed).toBe(false);
        expect(p.down).toBe(true);
        input.endTick();
        expect(p.downTicks).toBe(2); // накопился за два тика удержания

        pu(pointerEvent(240, 135));
        expect(p.justReleased).toBe(true);
        expect(p.down).toBe(false);
        input.endTick();
        expect(p.downTicks).toBe(0);
    });

    it('координаты переводятся в виртуальное разрешение', () => {
        const pm = env.canvasListeners.get('pointermove')!;
        // фейковый rect: 0..480x270 1:1, проверяем смещение
        pm(pointerEvent(96, 54));
        const p = input.getPointer();
        expect(p.x).toBe(96);
        expect(p.y).toBe(54);
    });

    it('isTouch помечает тач-события', () => {
        const pd = env.canvasListeners.get('pointerdown')!;
        pd(pointerEvent(0, 0, 'touch'));
        expect(input.getPointer().isTouch).toBe(true);
    });

    // --- инъекция ввода (агентный мост): та же семантика, что у реальных событий ---

    it('injectPointerDown/Up видны как justPressed ровно один тик', () => {
        input.injectPointerDown(96, 54);
        const p = input.getPointer();
        expect(p.down).toBe(true);
        expect(p.justPressed).toBe(true);
        expect(p.x).toBe(96);
        input.endTick();
        expect(input.getPointer().justPressed).toBe(false);
        input.injectPointerUp(96, 54);
        expect(input.getPointer().justReleased).toBe(true);
    });

    it('injectAction срабатывает в isActionJustPressed один тик', () => {
        input.injectAction('attack');
        expect(input.isActionJustPressed('attack')).toBe(true);
        expect(input.isActionActive('attack')).toBe(true);
        input.endTick();
        expect(input.isActionJustPressed('attack')).toBe(false);
        expect(input.isActionActive('attack')).toBe(true); // всё ещё зажато
        input.injectActionRelease('attack');
        expect(input.isActionActive('attack')).toBe(false);
        expect(input.isActionJustReleased('attack')).toBe(true);
    });

    it('injectKeyCode виден в wasKeyPressed/в действие-маппинге', () => {
        input.injectKeyCode('Space');
        expect(input.wasKeyPressed('Space')).toBe(true);
        expect(input.isActionJustPressed('attack')).toBe(true);
        input.injectKeyCodeUp('Space');
        expect(input.isKeyDown('Space')).toBe(false);
    });
});