import { Attacked, Damage, Defense } from '@common/rpg/components/combat';
import { Effect, EffectTemplate } from '@common/rpg/components/effect';
import { Health, Stat } from '@common/rpg/components/stat';
import { World } from '@common/rpg/core/world';
import { CombatSystem } from '@common/rpg/systems/combat';
import { EffectSystem } from '@common/rpg/systems/effect';
import { describe, expect, it } from 'bun:test';

function world() {
    const w = new World();
    w.addSystem(new CombatSystem());
    w.addSystem(new EffectSystem());
    return w;
}

describe('CombatSystem — damage', () => {
    it('reduces target health by damage value', () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 20, damageType: 'physical' }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'sword' }));
        w.update(1);
        expect(target.get(Health)!.value).toBe(80);
    });

    it('defense on target reduces damage', () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 20, damageType: 'physical' }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        target.add(new Defense({ value: 8, damageType: 'physical' }));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'sword' }));
        w.update(1);
        expect(target.get(Health)!.value).toBe(88);
    });

    it('defense only applies to matching damage type', () => {
        const w = world();
        const spell = w.createEntity('spell');
        spell.add(new Damage({ value: 20, damageType: 'fire' }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        target.add(new Defense({ value: 8, damageType: 'physical' }));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'spell' }));
        w.update(1);
        expect(target.get(Health)!.value).toBe(80);
    });

    it('minDamage is enforced when defense exceeds damage', () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 5, damageType: 'physical', minDamage: 3 }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        target.add(new Defense({ value: 10, damageType: 'physical' }));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'sword' }));
        w.update(1);
        expect(target.get(Health)!.value).toBe(97);
    });

    it('null sourceId falls back to Damage on attacker', () => {
        const w = world();
        const attacker = w.createEntity('attacker');
        attacker.add(new Damage({ value: 15, damageType: 'physical' }));
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: null }));
        w.update(1);
        expect(target.get(Health)!.value).toBe(85);
    });

    it('multiple attacks in one tick accumulate', () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 10, damageType: 'physical' }));
        w.createEntity('a');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        target.add(new Attacked({ attackerId: 'a', sourceId: 'sword' }));
        target.add(new Attacked({ attackerId: 'a', sourceId: 'sword' }));
        w.update(1);
        expect(target.get(Health)!.value).toBe(80);
    });

    it('Attacked components are removed after processing', () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 5, damageType: 'physical' }));
        w.createEntity('a');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        target.add(new Attacked({ attackerId: 'a', sourceId: 'sword' }));
        w.update(1);
        expect(target.has(Attacked)).toBeFalse();
    });

    it('missing attacker entity skips attack gracefully', () => {
        const w = world();
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        target.add(new Attacked({ attackerId: 'ghost', sourceId: null }));
        w.update(1);
        expect(target.get(Health)!.value).toBe(100);
    });

    it('missing source entity skips attack gracefully', () => {
        const w = world();
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'gone' }));
        w.update(1);
        expect(target.get(Health)!.value).toBe(100);
    });

    it('source with no Damage component skips attack gracefully', () => {
        const w = world();
        w.createEntity('attacker');
        w.createEntity('empty_source');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'empty_source' }));
        w.update(1);
        expect(target.get(Health)!.value).toBe(100);
    });

    it('target with no Health component is skipped gracefully', () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 10, damageType: 'physical' }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'sword' }));
        w.update(1); // should not throw
    });
});

describe('CombatSystem — on-hit effects', () => {
    it('onHit effect is applied to target on hit', () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 10, damageType: 'physical' }));
        sword.add(new EffectTemplate({ targetKey: 'health', delta: -5, targetField: 'value', duration: 10 }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }), 'health');
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'sword' }));
        w.update(1);
        expect(target.get(Health)!.value).toBe(85); // 100 - 10 dmg - 5 burn modifier
        expect(target.getAll(Effect).length).toBe(1);
    });

    it('onHit effect on weapon does not affect weapon itself', () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 10, damageType: 'physical' }));
        sword.add(new Stat({ value: 50 }), 'str');
        sword.add(new EffectTemplate({ targetKey: 'str', delta: -99, targetField: 'value' }));
        expect(sword.get(Stat, 'str')!.value).toBe(50);
    });

    it('onHit effect expires on target after duration', () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 5, damageType: 'physical' }));
        sword.add(new EffectTemplate({ targetKey: 'health', delta: -10, targetField: 'value', duration: 2 }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }), 'health');
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'sword' }));
        w.update(1);
        const afterHit = target.get(Health)!.value; // 85
        w.update(2); // burn expires
        expect(target.getAll(Effect).length).toBe(0);
        expect(target.get(Health)!.value).toBe(afterHit + 10); // modifier reverted
    });
});

describe('Effect stacking', () => {
    it('stack: allows multiple effects with the same tag', () => {
        const w = world();
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }), 'health');
        target.add(new Effect({ targetKey: 'health', delta: -5, stacking: 'stack', tag: 'poison' }));
        target.add(new Effect({ targetKey: 'health', delta: -5, stacking: 'stack', tag: 'poison' }));
        expect(target.getAll(Effect).length).toBe(2);
        expect(target.get(Health)!.value).toBe(90);
    });

    it('unique: second effect with same tag is discarded', () => {
        const w = world();
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }), 'health');
        target.add(new Effect({ targetKey: 'health', delta: -5, stacking: 'unique', tag: 'poison' }));
        target.add(new Effect({ targetKey: 'health', delta: -5, stacking: 'unique', tag: 'poison' }));
        expect(target.getAll(Effect).length).toBe(1);
        expect(target.get(Health)!.value).toBe(95);
    });

    it('unique: effects with different tags both apply', () => {
        const w = world();
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }), 'health');
        target.add(new Effect({ targetKey: 'health', delta: -5, stacking: 'unique', tag: 'poison' }));
        target.add(new Effect({ targetKey: 'health', delta: -3, stacking: 'unique', tag: 'burn' }));
        expect(target.getAll(Effect).length).toBe(2);
        expect(target.get(Health)!.value).toBe(92);
    });

    it('replace: removes existing effect and applies new one', () => {
        const w = world();
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }), 'health');
        target.add(new Effect({ targetKey: 'health', delta: -5, stacking: 'replace', tag: 'poison' }));
        expect(target.get(Health)!.value).toBe(95);
        target.add(new Effect({ targetKey: 'health', delta: -10, stacking: 'replace', tag: 'poison' }));
        expect(target.getAll(Effect).length).toBe(1);
        expect(target.get(Health)!.value).toBe(90); // old -5 reversed, new -10 applied
    });

    it('no tag: unique mode does not enforce limits', () => {
        const w = world();
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }), 'health');
        target.add(new Effect({ targetKey: 'health', delta: -5, stacking: 'unique' }));
        target.add(new Effect({ targetKey: 'health', delta: -5, stacking: 'unique' }));
        expect(target.getAll(Effect).length).toBe(2);
        expect(target.get(Health)!.value).toBe(90);
    });

    it('unique onHit effect is discarded when same tag already on target', () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 0, damageType: 'physical' }));
        sword.add(new EffectTemplate({ targetKey: 'health', delta: -5, duration: 10, stacking: 'unique', tag: 'burn' }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }), 'health');
        target.add(new Effect({ targetKey: 'health', delta: -5, stacking: 'unique', tag: 'burn' }));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'sword' }));
        w.update(1);
        expect(target.getAll(Effect).length).toBe(1); // onHit copy discarded
        expect(target.get(Health)!.value).toBe(95);
    });
});

describe('CombatSystem — events', () => {
    it("emits 'hit' on target with attack info", () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 10, damageType: 'fire' }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        const hits: unknown[] = [];
        target.on('hit', ({ data }) => hits.push(data));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'sword' }));
        w.update(1);
        expect(hits.length).toBe(1);
        expect((hits[0] as any).damageType).toBe('fire');
        expect((hits[0] as any).amount).toBe(10);
        expect((hits[0] as any).attackerId).toBe('attacker');
        expect((hits[0] as any).sourceId).toBe('sword');
    });

    it("emits 'kill' when target health reaches zero", () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 999, damageType: 'physical' }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 50, min: 0 }));
        const kills: unknown[] = [];
        target.on('kill', ({ data }) => kills.push(data));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'sword' }));
        w.update(1);
        expect(kills.length).toBe(1);
    });

    it("does not emit 'kill' when target survives", () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 5, damageType: 'physical' }));
        w.createEntity('attacker');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        const kills: unknown[] = [];
        target.on('kill', ({ data }) => kills.push(data));
        target.add(new Attacked({ attackerId: 'attacker', sourceId: 'sword' }));
        w.update(1);
        expect(kills.length).toBe(0);
    });

    it("emits 'hit' per attack when multiple attacks land", () => {
        const w = world();
        const sword = w.createEntity('sword');
        sword.add(new Damage({ value: 5, damageType: 'physical' }));
        w.createEntity('a');
        const target = w.createEntity('target');
        target.add(new Health({ value: 100, min: 0 }));
        const hits: unknown[] = [];
        target.on('hit', ({ data }) => hits.push(data));
        target.add(new Attacked({ attackerId: 'a', sourceId: 'sword' }));
        target.add(new Attacked({ attackerId: 'a', sourceId: 'sword' }));
        w.update(1);
        expect(hits.length).toBe(2);
    });
});
