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

describe('SceneRegistry', () => {
    it('add/get/remove и byKind по роду', () => {
        const r = new SceneRegistry();
        const a = r.add({ id: 'npc:1', kind: 'npc', pos: { x: 3.5, y: 4.5 } });
        r.add({ id: 'enemy:1', kind: 'enemy', pos: { x: 8.5, y: 2.5 } });
        expect(r.get('npc:1')).toBe(a);
        expect(a.tile).toEqual({ x: 3, y: 4 });
        expect(r.byKind('npc').map((o) => o.id)).toEqual(['npc:1']);
        expect(r.size).toBe(2);
        r.remove('npc:1');
        expect(r.get('npc:1')).toBeUndefined();
        expect(r.at(3, 4)).toBeNull();
    });

    it('дубликат id — throw', () => {
        const r = new SceneRegistry();
        r.add({ id: 'x', kind: 'npc', pos: { x: 1, y: 1 } });
        expect(() => r.add({ id: 'x', kind: 'npc', pos: { x: 2, y: 2 } })).toThrow('дубликат');
    });

    it('move переиндексирует тайл (и O(1) внутри тайла)', () => {
        const r = new SceneRegistry();
        r.add({ id: 'e', kind: 'enemy', pos: { x: 2.5, y: 2.5 }, radius: 0.3 });
        r.move('e', { x: 2.9, y: 2.9 }); // тот же тайл
        expect(r.at(2, 2, 'enemy')).not.toBeNull();
        expect(r.at(3, 3, 'enemy')).toBeNull();
        r.move('e', { x: 5.2, y: 7.8 });
        expect(r.at(2, 2, 'enemy')).toBeNull();
        expect(r.at(5, 7, 'enemy')).not.toBeNull();
        expect(r.get('e')!.pos).toEqual({ x: 5.2, y: 7.8 });
        r.remove('e'); // remove после move не оставляет «призраков» в старом тайле
        expect(r.at(5, 7)).toBeNull();
    });

    it('footprint-объект находится во всех покрытых тайлах', () => {
        const r = new SceneRegistry();
        r.add({
            id: 'prop:1', kind: 'prop', pos: { x: 10.5, y: 10.5 },
            footprint: { w: 2, h: 2 }
        });
        expect(r.at(10, 10, 'prop')).not.toBeNull();
        expect(r.at(11, 10, 'prop')).not.toBeNull();
        expect(r.at(10, 11, 'prop')).not.toBeNull();
        expect(r.at(11, 11, 'prop')).not.toBeNull();
        expect(r.at(12, 11, 'prop')).toBeNull();
        r.remove('prop:1');
        expect(r.at(11, 11)).toBeNull();
    });

    it('atWorld находит тело, выступающее из тайла привязки', () => {
        const r = new SceneRegistry();
        r.add({ id: 'npc:1', kind: 'npc', pos: { x: 10.9, y: 10.5 }, radius: 0.4 });
        // Точка в соседнем тайле, но внутри радиуса тела.
        expect(r.atWorld({ x: 11.2, y: 10.5 }, 'npc')).not.toBeNull();
        // Вне радиуса — промах.
        expect(r.atWorld({ x: 12.2, y: 10.5 }, 'npc')).toBeNull();
    });

    it('near: по радиусу, с фильтром рода и без дублей на границе', () => {
        const r = new SceneRegistry();
        r.add({ id: 'a', kind: 'npc', pos: { x: 5, y: 5 } });
        r.add({ id: 'b', kind: 'enemy', pos: { x: 5.5, y: 5.5 } });
        r.add({ id: 'c', kind: 'npc', pos: { x: 7.5, y: 7.5 } });
        const ids = (list: { id: string }[]) => list.map((o) => o.id).sort();
        expect(ids(r.near({ x: 5, y: 5 }, 1))).toEqual(['a', 'b']);
        expect(ids(r.near({ x: 5, y: 5 }, 1, 'npc'))).toEqual(['a']);
        expect(ids(r.near({ x: 5, y: 5 }, 10))).toEqual(['a', 'b', 'c']);
        expect(r.near({ x: 20, y: 20 }, 2)).toEqual([]);
    });
});