import { describe, it, expect, mock } from 'bun:test';
import { World, Entity, Component } from '@common/rpg/core/world';

class Counter extends Component<{ n: number }> {
    constructor(n = 0) { super({ n }); }
    get n() { return this.state.n; }
    inc() { this.state.n++; }
}

class Tag extends Component<{}> {
    constructor() { super({}); }
}

describe('World — entity management', () => {
    it('creates entity with auto id', () => {
        const world = new World();
        const e = world.createEntity();
        expect(e.id).toMatch(/^entity_\d+$/);
    });

    it('creates entity with explicit id', () => {
        const world = new World();
        const e = world.createEntity('player');
        expect(e.id).toBe('player');
    });

    it('creates entity with template id', () => {
        const world = new World();
        const a = world.createEntity('enemy_*');
        const b = world.createEntity('enemy_*');
        expect(a.id).not.toBe(b.id);
        expect(a.id).toMatch(/^enemy_\d+$/);
    });

    it('throws on duplicate entity id', () => {
        const world = new World();
        world.createEntity('dup');
        expect(() => world.createEntity('dup')).toThrow();
    });

    it('getEntity returns entity or undefined', () => {
        const world = new World();
        world.createEntity('e');
        expect(world.getEntity('e')).toBeInstanceOf(Entity);
        expect(world.getEntity('missing')).toBeUndefined();
    });

    it('destroyEntity removes entity and its components', () => {
        const world = new World();
        const e = world.createEntity('e');
        const removed: string[] = [];
        class Tracked extends Component<{}> {
            constructor() { super({}); }
            override onRemove() { removed.push('ok'); }
        }
        e.add(new Tracked());
        world.destroyEntity(e);
        expect(world.getEntity('e')).toBeUndefined();
        expect(removed).toEqual(['ok']);
    });

    it('iterates all entities', () => {
        const world = new World();
        world.createEntity('a');
        world.createEntity('b');
        const ids = [...world].map(e => e.id);
        expect(ids).toContain('a');
        expect(ids).toContain('b');
    });
});

describe('Entity — components', () => {
    it('add / get by key', () => {
        const world = new World();
        const e = world.createEntity();
        e.add(new Counter(5), 'counter');
        expect(e.get<Counter>('counter')?.n).toBe(5);
    });

    it('get by class', () => {
        const world = new World();
        const e = world.createEntity();
        e.add(new Counter(3));
        expect(e.get(Counter)?.n).toBe(3);
    });

    it('get by class and key', () => {
        const world = new World();
        const e = world.createEntity();
        e.add(new Counter(1), 'a');
        e.add(new Counter(2), 'b');
        expect(e.get(Counter, 'b')?.n).toBe(2);
    });

    it('get by class and filter', () => {
        const world = new World();
        const e = world.createEntity();
        e.add(new Counter(10));
        e.add(new Counter(20));
        expect(e.get(Counter, c => c.n === 20)?.n).toBe(20);
    });

    it('getAll returns all matching components', () => {
        const world = new World();
        const e = world.createEntity();
        e.add(new Counter(1));
        e.add(new Counter(2));
        e.add(new Tag());
        const all = e.getAll(Counter);
        expect(all.length).toBe(2);
        expect(all.map(c => c.n).sort()).toEqual([1, 2]);
    });

    it('has by key / by class / by class+key', () => {
        const world = new World();
        const e = world.createEntity();
        e.add(new Counter(), 'c');
        expect(e.has('c')).toBeTrue();
        expect(e.has('missing')).toBeFalse();
        expect(e.has(Counter)).toBeTrue();
        expect(e.has(Tag)).toBeFalse();
        expect(e.has(Counter, 'c')).toBeTrue();
        expect(e.has(Counter, 'missing')).toBeFalse();
    });

    it('remove by key calls onRemove', () => {
        const world = new World();
        const e = world.createEntity();
        const removed: boolean[] = [];
        class R extends Component<{}> {
            constructor() { super({}); }
            override onRemove() { removed.push(true); }
        }
        e.add(new R(), 'r');
        e.remove('r');
        expect(removed).toEqual([true]);
        expect(e.has('r')).toBeFalse();
    });

    it('remove by component instance', () => {
        const world = new World();
        const e = world.createEntity();
        const c = new Counter();
        e.add(c, 'c');
        e.remove(c);
        expect(e.has('c')).toBeFalse();
    });

    it('add over existing key calls onRemove on old', () => {
        const world = new World();
        const e = world.createEntity();
        const events: string[] = [];
        class Ev extends Component<{ id: string }> {
            constructor(id: string) { super({ id }); }
            override onAdd() { events.push(`add:${this.state.id}`); }
            override onRemove() { events.push(`remove:${this.state.id}`); }
        }
        e.add(new Ev('a'), 'k');
        e.add(new Ev('b'), 'k');
        expect(events).toEqual(['add:a', 'remove:a', 'add:b']);
    });

    it('component.entity and component.key are set on add', () => {
        const world = new World();
        const e = world.createEntity('me');
        const c = new Counter();
        e.add(c, 'mykey');
        expect(c.entity).toBe(e);
        expect(c.key).toBe('mykey');
    });
});

describe('Entity — clone', () => {
    it('clones component state deeply', () => {
        const world = new World();
        const e = world.createEntity();
        const orig = new Counter(7);
        e.add(orig, 'c');
        const clone = e.clone(orig, 'd');
        clone.inc();
        expect(orig.n).toBe(7);
        expect(clone.n).toBe(8);
    });

    it('clone fires onAdd', () => {
        const world = new World();
        const e = world.createEntity();
        const added: boolean[] = [];
        class A extends Component<{}> {
            constructor() { super({}); }
            override onAdd() { added.push(true); }
        }
        const a = new A();
        e.add(a, 'a');
        added.length = 0;
        e.clone(a, 'b');
        expect(added).toEqual([true]);
    });
});

describe('World — cloneEntity', () => {
    it('produces independent deep copy', () => {
        const world = new World();
        const src = world.createEntity('src');
        src.add(new Counter(5));
        const copy = world.cloneEntity(src, 'copy');
        copy.get(Counter)!.inc();
        expect(src.get(Counter)!.n).toBe(5);
        expect(copy.get(Counter)!.n).toBe(6);
    });
});

describe('World — query', () => {
    it('single-component query yields matching entities', () => {
        const world = new World();
        const a = world.createEntity('a');
        const b = world.createEntity('b');
        world.createEntity('c');
        a.add(new Counter());
        b.add(new Counter());
        const found = [...world.query(Counter)].map(([e]) => e.id);
        expect(found.sort()).toEqual(['a', 'b']);
    });

    it('multi-component query requires all', () => {
        const world = new World();
        const a = world.createEntity('a');
        const b = world.createEntity('b');
        a.add(new Counter());
        a.add(new Tag());
        b.add(new Counter());
        const found = [...world.query(Counter, Tag)].map(([e]) => e.id);
        expect(found).toEqual(['a']);
    });
});

describe('Entity — events', () => {
    it('emit / on fires handler', () => {
        const world = new World();
        const e = world.createEntity();
        const received: unknown[] = [];
        e.on('boom', ({ data }) => received.push(data));
        e.emit('boom', 42);
        expect(received).toEqual([42]);
    });

    it('off unsubscribes handler', () => {
        const world = new World();
        const e = world.createEntity();
        const received: unknown[] = [];
        const handler = ({ data }: { data?: unknown }) => received.push(data);
        e.on('x', handler);
        e.off('x', handler);
        e.emit('x', 1);
        expect(received).toEqual([]);
    });

    it('once fires exactly once', () => {
        const world = new World();
        const e = world.createEntity();
        const received: unknown[] = [];
        e.once('x', ({ data }) => received.push(data));
        e.emit('x', 1);
        e.emit('x', 2);
        expect(received).toEqual([1]);
    });

    it('on returns unsubscribe function', () => {
        const world = new World();
        const e = world.createEntity();
        const received: unknown[] = [];
        const unsub = e.on('x', ({ data }) => received.push(data));
        unsub();
        e.emit('x', 1);
        expect(received).toEqual([]);
    });

    it('destroying entity removes all event handlers', () => {
        const world = new World();
        const e = world.createEntity('e');
        const received: unknown[] = [];
        e.on('x', ({ data }) => received.push(data));
        world.destroyEntity(e);
        // no error, handler just never fires
        expect(received).toEqual([]);
    });
});
