import { describe, it, expect } from 'bun:test';
import { World } from '@common/rpg/core/world';
import { Inventory } from '@common/rpg/components/inventory';
import { Items, Item, Stackable, Usable } from '@common/rpg/components/item';
import { Equipment, Equippable } from '@common/rpg/components/equipment';
import { Stat } from '@common/rpg/components/stat';

function world() { return new World(); }

describe('Items.register', () => {
    it('creates entity with Item component', () => {
        const w = world();
        const e = Items.register(w, 'sword', 'Iron Sword');
        expect(e.get(Item)!.name).toBe('Iron Sword');
    });

    it('adds Stackable when maxStack is provided', () => {
        const w = world();
        const e = Items.register(w, 'coin', 'Gold Coin', { maxStack: 99 });
        expect(e.get(Stackable)!.maxStack).toBe(99);
    });

    it('does not add Stackable when maxStack is omitted', () => {
        const w = world();
        const e = Items.register(w, 'key', 'Old Key');
        expect(e.has(Stackable)).toBeFalse();
    });

    it('adds Usable when usable options are provided', () => {
        const w = world();
        const e = Items.register(w, 'potion', 'Health Potion', {
            usable: { actions: [{ type: 'health.update', arg: 50 }] },
        });
        expect(e.has(Usable)).toBeTrue();
        expect(e.get(Usable)!.consumeOnUse).toBeTrue(); // default
    });

    it('respects consumeOnUse: false', () => {
        const w = world();
        const e = Items.register(w, 'ring', 'Magic Ring', {
            usable: { actions: [], consumeOnUse: false },
        });
        expect(e.get(Usable)!.consumeOnUse).toBeFalse();
    });

    it('sets description', () => {
        const w = world();
        const e = Items.register(w, 'rune', 'Ancient Rune', { description: 'A glowing rune.' });
        expect(e.get(Item)!.description).toBe('A glowing rune.');
    });

    it('registers entity under the given id', () => {
        const w = world();
        Items.register(w, 'gem', 'Ruby');
        expect(w.getEntity('gem')).toBeDefined();
    });
});

describe('Inventory — infinite mode', () => {
    it('adds items without limit', () => {
        const w = world();
        Items.register(w, 'coin', 'Coin', { maxStack: 99 });
        const e = w.createEntity('player');
        e.add(new Inventory());
        e.get(Inventory)!.add({ itemId: 'coin', amount: 500 });
        expect(e.get(Inventory)!.getAmount('coin')).toBe(500);
    });

    it('grows slot list automatically', () => {
        const w = world();
        Items.register(w, 'coin', 'Coin', { maxStack: 10 });
        const e = w.createEntity('player');
        e.add(new Inventory());
        e.get(Inventory)!.add({ itemId: 'coin', amount: 25 });
        // 10 + 10 + 5 = 3 slots
        expect(e.get(Inventory)!.getItems().get('coin')).toBe(25);
    });

    it('add returns true', () => {
        const w = world();
        Items.register(w, 'gem', 'Gem');
        const e = w.createEntity('player');
        e.add(new Inventory());
        expect(e.get(Inventory)!.add({ itemId: 'gem', amount: 1 })).toBeTrue();
    });

    it('warns and returns false for unknown item', () => {
        const w = world();
        const e = w.createEntity('player');
        e.add(new Inventory());
        expect(e.get(Inventory)!.add({ itemId: 'ghost', amount: 1 })).toBeFalse();
    });
});

describe('Inventory — finite mode (count)', () => {
    it('accepts items up to slot capacity', () => {
        const w = world();
        Items.register(w, 'potion', 'Potion', { maxStack: 5 });
        const e = w.createEntity('player');
        e.add(new Inventory(2)); // 2 slots × 5 stack = 10 max
        expect(e.get(Inventory)!.add({ itemId: 'potion', amount: 10 })).toBeTrue();
        expect(e.get(Inventory)!.getAmount('potion')).toBe(10);
    });

    it('rejects add when capacity exceeded', () => {
        const w = world();
        Items.register(w, 'potion', 'Potion', { maxStack: 5 });
        const e = w.createEntity('player');
        e.add(new Inventory(1)); // 1 slot × 5 = 5 max
        expect(e.get(Inventory)!.add({ itemId: 'potion', amount: 6 })).toBeFalse();
        expect(e.get(Inventory)!.getAmount('potion')).toBe(0);
    });

    it('fills partial stacks before opening new slots', () => {
        const w = world();
        Items.register(w, 'stone', 'Stone', { maxStack: 10 });
        const e = w.createEntity('player');
        e.add(new Inventory(2));
        const inv = e.get(Inventory)!;
        inv.add({ itemId: 'stone', amount: 8 });
        inv.add({ itemId: 'stone', amount: 5 }); // fills slot 0 to 10, slot 1 to 3
        expect(inv.getAmount('stone')).toBe(13);
    });
});

describe('Inventory — named slots', () => {
    it('add to specific named slotId', () => {
        const w = world();
        Items.register(w, 'herb', 'Herb', { maxStack: 10 });
        const e = w.createEntity('player');
        e.add(new Inventory([{ slotId: 'pocket', limit: 10 }]));
        const inv = e.get(Inventory)!;
        expect(inv.add({ itemId: 'herb', amount: 3, slotId: 'pocket' })).toBeTrue();
        expect(inv.getSlotContents('pocket')).toEqual({ itemId: 'herb', amount: 3 });
    });

    it('rejects add to unknown slotId', () => {
        const w = world();
        Items.register(w, 'herb', 'Herb');
        const e = w.createEntity('player');
        e.add(new Inventory([{ slotId: 'pocket', limit: 10 }]));
        expect(e.get(Inventory)!.add({ itemId: 'herb', amount: 1, slotId: 'wallet' })).toBeFalse();
    });

    it('rejects add exceeding slot limit', () => {
        const w = world();
        Items.register(w, 'herb', 'Herb', { maxStack: 99 });
        const e = w.createEntity('player');
        e.add(new Inventory([{ slotId: 'slot', limit: 5 }]));
        expect(e.get(Inventory)!.add({ itemId: 'herb', amount: 6, slotId: 'slot' })).toBeFalse();
    });
});

describe('Inventory — remove', () => {
    it('removes partial amount', () => {
        const w = world();
        Items.register(w, 'coin', 'Coin', { maxStack: 99 });
        const e = w.createEntity('player');
        e.add(new Inventory());
        const inv = e.get(Inventory)!;
        inv.add({ itemId: 'coin', amount: 50 });
        inv.remove({ itemId: 'coin', amount: 20 });
        expect(inv.getAmount('coin')).toBe(30);
    });

    it('returns false when removing more than available', () => {
        const w = world();
        Items.register(w, 'coin', 'Coin');
        const e = w.createEntity('player');
        e.add(new Inventory());
        const inv = e.get(Inventory)!;
        inv.add({ itemId: 'coin', amount: 1 });
        expect(inv.remove({ itemId: 'coin', amount: 10 })).toBeFalse();
        expect(inv.getAmount('coin')).toBe(1);
    });

    it('removes from a specific slot', () => {
        const w = world();
        Items.register(w, 'gem', 'Gem');
        const e = w.createEntity('player');
        e.add(new Inventory([{ slotId: 'a' }, { slotId: 'b' }]));
        const inv = e.get(Inventory)!;
        inv.add({ itemId: 'gem', amount: 1, slotId: 'a' });
        inv.add({ itemId: 'gem', amount: 1, slotId: 'b' });
        inv.remove({ itemId: 'gem', amount: 1, slotId: 'a' });
        expect(inv.getSlotContents('a')).toBeNull();
        expect(inv.getSlotContents('b')).toEqual({ itemId: 'gem', amount: 1 });
    });

    it('slot contents becomes null after full removal', () => {
        const w = world();
        Items.register(w, 'herb', 'Herb');
        const e = w.createEntity('player');
        e.add(new Inventory([{ slotId: 's' }]));
        const inv = e.get(Inventory)!;
        inv.add({ itemId: 'herb', amount: 1, slotId: 's' });
        inv.remove({ itemId: 'herb', amount: 1, slotId: 's' });
        expect(inv.getSlotContents('s')).toBeNull();
    });
});

describe('Inventory — getItems / getAmount', () => {
    it('getItems aggregates across all slots', () => {
        const w = world();
        Items.register(w, 'coin', 'Coin', { maxStack: 5 });
        const e = w.createEntity('player');
        e.add(new Inventory(3));
        const inv = e.get(Inventory)!;
        inv.add({ itemId: 'coin', amount: 12 });
        expect(inv.getItems().get('coin')).toBe(12);
    });

    it('getAmount returns 0 for absent item', () => {
        const w = world();
        const e = w.createEntity('player');
        e.add(new Inventory());
        expect(e.get(Inventory)!.getAmount('missing')).toBe(0);
    });
});

describe('Inventory — equip', () => {
    it('delegates to Equipment.equip', () => {
        const w = world();
        const sword = Items.register(w, 'sword', 'Sword');
        sword.add(new Equippable('weapon'));
        const player = w.createEntity('player');
        player.add(new Stat({ value: 10 }));
        player.add(new Equipment({ slotName: 'weapon', type: 'weapon' }));
        player.add(new Inventory());
        const inv = player.get(Inventory)!;
        inv.add({ itemId: 'sword', amount: 1 });
        expect(inv.equip({ itemId: 'sword' })).toBeTrue();
        expect(player.get(Equipment)!.getItem('weapon')).toBe('sword');
    });
});

describe('Inventory — use', () => {
    it('executes Usable actions', () => {
        const w = world();
        const player = w.createEntity('player');
        player.add(new Stat({ value: 10 }), 'str');
        player.add(new Inventory());
        Items.register(w, 'potion', 'Health Potion', {
            usable: { actions: [{ type: 'Stat(str).update', arg: 5 }], consumeOnUse: false },
        });
        player.get(Inventory)!.add({ itemId: 'potion', amount: 1 });
        player.get(Inventory)!.use({ itemId: 'potion' });
        expect(player.get(Stat, 'str')!.value).toBe(15);
    });

    it('consumes item when consumeOnUse is true', () => {
        const w = world();
        const player = w.createEntity('player');
        player.add(new Inventory());
        Items.register(w, 'herb', 'Herb', {
            maxStack: 99,
            usable: { actions: [], consumeOnUse: true },
        });
        player.get(Inventory)!.add({ itemId: 'herb', amount: 3 });
        player.get(Inventory)!.use({ itemId: 'herb' });
        expect(player.get(Inventory)!.getAmount('herb')).toBe(2);
    });
});
