import { describe, it, expect } from 'bun:test';
import { World } from '@common/rpg/core/world';
import { Experience } from '@common/rpg/components/experience';

function withXp(spec: ConstructorParameters<typeof Experience>[0]) {
    const w = new World();
    const e = w.createEntity();
    e.add(new Experience(spec), 'xp');
    return { e, xp: e.get(Experience, 'xp')! };
}

describe('Experience — initial state', () => {
    it('starts at level 1, xp 0', () => {
        const { xp } = withXp([100, 200, 400]);
        expect(xp.level).toBe(1);
        expect(xp.xp).toBe(0);
        expect(xp.xpInLevel).toBe(0);
    });

    it('progress is 0 at start', () => {
        const { xp } = withXp([100]);
        expect(xp.progress).toBe(0);
    });

    it('xpToNext equals first threshold', () => {
        const { xp } = withXp([100, 300]);
        expect(xp.xpToNext).toBe(100);
    });
});

describe('Experience — award XP (array spec)', () => {
    it('accumulates xp without leveling up', () => {
        const { xp } = withXp([100]);
        xp.award(50);
        expect(xp.xp).toBe(50);
        expect(xp.level).toBe(1);
        expect(xp.xpInLevel).toBe(50);
    });

    it('levels up when xp meets threshold', () => {
        const { xp } = withXp([100]);
        xp.award(100);
        expect(xp.level).toBe(2);
        expect(xp.xpInLevel).toBe(0);
    });

    it("emits 'levelup' with prev and new level", () => {
        const { e, xp } = withXp([100]);
        const events: unknown[] = [];
        e.on('Experience(xp).levelup', ({ data }) => events.push(data));
        xp.award(100);
        expect(events).toEqual([{ level: 2, prev: 1 }]);
    });

    it('handles multiple level-ups in a single award', () => {
        const { xp } = withXp([100, 200, 400]);
        xp.award(350); // 100 (L1→2) + 200 (L2→3) + 50 leftover
        expect(xp.level).toBe(3);
        expect(xp.xpInLevel).toBe(50);
    });

    it("emits 'levelup' for each level gained", () => {
        const { e, xp } = withXp([100, 200]);
        const events: unknown[] = [];
        e.on('Experience(xp).levelup', ({ data }) => events.push(data));
        xp.award(300); // gains 2 levels
        expect(events.length).toBe(2);
        expect((events[0] as any).level).toBe(2);
        expect((events[1] as any).level).toBe(3);
    });

    it('stops leveling at max level (array spec)', () => {
        const { xp } = withXp([100]); // max level = 2
        xp.award(9999);
        expect(xp.level).toBe(2);
    });

    it('xpToNext is null at max level', () => {
        const { xp } = withXp([100]);
        xp.award(9999);
        expect(xp.xpToNext).toBeNull();
    });

    it('progress is 1 at max level', () => {
        const { xp } = withXp([100]);
        xp.award(9999);
        expect(xp.progress).toBe(1);
    });

    it('xp.total never resets across levels', () => {
        const { xp } = withXp([100, 200]);
        xp.award(400);
        expect(xp.xp).toBe(400);
    });
});

describe('Experience — award XP (geometric spec)', () => {
    it('levels up using geometric thresholds', () => {
        // base=100, factor=2 → L1→L2: 100xp, L2→L3: 200xp
        const { xp } = withXp({ base: 100, factor: 2 });
        xp.award(100);
        expect(xp.level).toBe(2);
        xp.award(200);
        expect(xp.level).toBe(3);
    });

    it('has no max level with geometric spec', () => {
        const { xp } = withXp({ base: 10, factor: 1 }); // 10xp per level, forever
        xp.award(10000);
        expect(xp.level).toBeGreaterThan(100);
        expect(xp.xpToNext).not.toBeNull();
    });

    it('progress tracks correctly within a level', () => {
        const { xp } = withXp({ base: 100, factor: 2 }); // 100xp for L1→2
        xp.award(40);
        expect(xp.progress).toBeCloseTo(0.4);
    });
});

describe('Experience — xpInLevel / xpToNext', () => {
    it('xpInLevel resets after level-up', () => {
        const { xp } = withXp([100, 200]);
        xp.award(150); // levels up at 100, 50 left
        expect(xp.xpInLevel).toBe(50);
        expect(xp.xpToNext).toBe(150); // 200 - 50
    });

    it('xpToNext decreases as xp accumulates', () => {
        const { xp } = withXp([100]);
        expect(xp.xpToNext).toBe(100);
        xp.award(30);
        expect(xp.xpToNext).toBe(70);
    });
});
