import { describe, expect, it } from 'vitest';
import { Camera } from '../../render/Camera';
import { EventBus } from '../../core/EventBus';
import { EngineAgent } from '../EngineAgent';
import type { AgentHost, SceneAgent, SnapshotLayer } from '../types';

/**
 * Фейковый хост: без Pixi/канваса. stepTick двигает «время» и вызывает
 * sceneAgent; renderFrame только помечается.
 */
function fakeHost(): AgentHost & { renders: number } {
    let ticks = 0;
    const events = new EventBus();
    return {
        events,
        scenes: { current: undefined, transitioning: false } as never,
        input: {
            getPointer: () => ({ x: 0, y: 0, down: false, justPressed: false, downTicks: 0 })
        } as never,
        camera: new Camera(480, 270),
        virtualWidth: 480,
        virtualHeight: 270,
        fixedStep: 1 / 60,
        fps: 0,
        get tickCount() {
            return ticks;
        },
        stepTick() {
            return ++ticks;
        },
        stepTicks(n = 1) {
            for (let i = 0; i < n; i++) this.stepTick();
            return ticks;
        },
        renderFrame() {
            this.renders++;
        },
        renders: 0
    };
}

function fakeScene(): SceneAgent {
    return {
        agentSnapshot: () => ({ area: 'test', hp: 5 }),
        agentInvariants: () => [{ id: 'warn-x', severity: 'warn', message: 'подозрительно' }],
        agentCommand: (name) => (name === 'ping' ? 'pong' : null)
    };
}

describe('EngineAgent — снапшот и шаги', () => {
    it('снапшот содержит движковый слой + слой сцены', () => {
        const host = fakeHost();
        const agent = new EngineAgent(host, () => fakeScene());
        const s = agent.snapshot() as SnapshotLayer;
        expect(s.tick).toBe(0);
        expect(s.fixedStep).toBeCloseTo(1 / 60, 6);
        expect(s.transitioning).toBe(false);
        expect(s.camera).toBeTruthy();
        expect(s.pointer).toBeTruthy();
        expect(s.area).toBe('test');
        expect(s.hp).toBe(5);
    });

    it('без сцены снапшот всё равно валиден', () => {
        const agent = new EngineAgent(fakeHost(), () => null);
        const s = agent.snapshot();
        expect(s.tick).toBe(0);
        expect(s.area).toBeUndefined();
    });
});

describe('EngineAgent — инварианты', () => {
    it('мержит движковые и сценические инварианты', () => {
        const host = fakeHost();
        host.camera.x = NaN; // движковый инвариант pos-nan
        const agent = new EngineAgent(host, () => fakeScene());
        const invs = agent.invariants();
        expect(invs.map((i) => i.id)).toEqual(['pos-nan', 'warn-x']);
    });
});

describe('EngineAgent — шаги и команды', () => {
    it('step крутит ровно n тиков', () => {
        const host = fakeHost();
        const agent = new EngineAgent(host, () => fakeScene());
        expect(agent.step(10).tick).toBe(10);
        expect(host.tickCount).toBe(10);
    });

    it('waitFor завершается по предикату и по лимиту', async () => {
        const host = fakeHost();
        const agent = new EngineAgent(host, () => fakeScene());
        const ok = await agent.waitFor((s) => (s.tick as number) >= 5, { timeoutTicks: 100 });
        expect(ok.ok).toBe(true);
        expect(ok.ticks).toBe(5);
        const fail = await agent.waitFor(() => false, { timeoutTicks: 3 });
        expect(fail.ok).toBe(false);
        expect(fail.ticks).toBe(3);
    });

    it('command проходит в сцену, неизвестная — null', () => {
        const agent = new EngineAgent(fakeHost(), () => fakeScene());
        expect(agent.command('ping')).toBe('pong');
        expect(agent.command('unknown')).toBeNull();
    });

    it('ошибка в сцене не вылетает, а попадает в слой { error }', () => {
        const bad: SceneAgent = {
            agentSnapshot: () => {
                throw new Error('бум');
            },
            agentInvariants: () => {
                throw new Error('бум');
            }
        };
        const agent = new EngineAgent(fakeHost(), () => bad);
        const s = agent.snapshot() as SnapshotLayer;
        expect(String(s.error)).toContain('бум');
        expect(agent.invariants()).toEqual([]);
    });
});