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

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

function withStat(value = 10, min?: number, max?: number) {
    const w = world();
    const e = w.createEntity();
    e.add(new Stat({ value, min, max }), 'str');
    return { w, e, stat: e.get(Stat, 'str')! };
}

describe('Effect — onAdd / onRemove', () => {
    it('applies delta to stat on add', () => {
        const { e, stat } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 5 }));
        expect(stat.value).toBe(15);
    });

    it('reverts delta on remove', () => {
        const { e, stat } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 5 }), 'fx');
        e.remove('fx');
        expect(stat.value).toBe(10);
    });

    it('applies delta to max field', () => {
        const { e } = withStat(10, undefined, 20);
        const s = e.get(Stat, 'str')!;
        e.add(new Effect({ targetKey: 'str', delta: 10, targetField: 'max' }), 'fx');
        expect(s.max).toBe(30);
        e.remove('fx');
        expect(s.max).toBe(20);
    });

    it('active is true after add', () => {
        const { e } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 1 }), 'fx');
        expect(e.get(Effect, 'fx')!.active).toBeTrue();
    });

    it('no-op if target stat is missing', () => {
        const w = world();
        const e = w.createEntity();
        expect(() => e.add(new Effect({ targetKey: 'str', delta: 5 }))).not.toThrow();
    });
});

describe('Effect — duration', () => {
    it('expires after duration ticks', () => {
        const { w, e, stat } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 5, duration: 2 }));
        expect(stat.value).toBe(15);

        w.update(1);
        expect(e.has(Effect)).toBeTrue();

        w.update(1);
        expect(e.has(Effect)).toBeFalse();
        expect(stat.value).toBe(10);
    });

    it('emits expired before removal', () => {
        const { w, e } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 1, duration: 1 }), 'fx');
        const events: string[] = [];
        e.on('Effect(fx).expired', () => events.push('expired'));
        w.update(1);
        expect(events).toEqual(['expired']);
    });

    it('reset() restarts timer', () => {
        const { w, e, stat } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 5, duration: 2 }), 'fx');
        w.update(1.5);
        e.get(Effect, 'fx')!.reset();
        w.update(1.5); // would have expired without reset
        expect(e.has(Effect)).toBeTrue();
        expect(stat.value).toBe(15);
    });

    it('reset(duration) changes duration', () => {
        const { w, e } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 5, duration: 1 }), 'fx');
        e.get(Effect, 'fx')!.reset(10);
        w.update(5);
        expect(e.has(Effect)).toBeTrue();
    });

    it('clear() immediately expires effect', () => {
        const { w, e, stat } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 5, duration: 100 }), 'fx');
        e.get(Effect, 'fx')!.clear();
        w.update(0.01);
        expect(e.has(Effect)).toBeFalse();
        expect(stat.value).toBe(10);
    });
});

describe('Effect — permanent', () => {
    it('permanent effect is never removed by EffectSystem', () => {
        const { w, e, stat } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 5 }));
        w.update(100);
        expect(e.has(Effect)).toBeTrue();
        expect(stat.value).toBe(15);
    });
});

describe('Effect — scope: onHit', () => {
    it('onAdd is a no-op for onHit scope', () => {
        const { e, stat } = withStat(10);
        e.add(new EffectTemplate({ targetKey: 'str', delta: 99 }));
        expect(stat.value).toBe(10);
    });

    it('onRemove is a no-op for onHit scope', () => {
        const { e, stat } = withStat(10);
        e.add(new EffectTemplate({ targetKey: 'str', delta: 99 }));
        e.remove('fx');
        expect(stat.value).toBe(10);
    });

    it('EffectSystem does not tick or remove onHit effects', () => {
        const { w, e } = withStat(10);
        e.add(new EffectTemplate({ targetKey: 'str', delta: 5, duration: 1 }));
        w.update(10);
        expect(e.has(EffectTemplate)).toBeTrue();
    });
});

describe('Effect — multiple effects on same stat', () => {
    it('multiple effects stack additively', () => {
        const { e, stat } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 3 }));
        e.add(new Effect({ targetKey: 'str', delta: 7 }));
        expect(stat.value).toBe(20);
    });

    it('removing one effect reverts only that delta', () => {
        const { e, stat } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 3 }), 'a');
        e.add(new Effect({ targetKey: 'str', delta: 7 }), 'b');
        e.remove('a');
        expect(stat.value).toBe(17);
    });
});

describe('Effect — targetKey resolution', () => {
    it('no targetKey finds a stat added without a key', () => {
        const w = world();
        const e = w.createEntity();
        const stat = e.add(new Stat({ value: 10 })); // Symbol key
        e.add(new Effect({ delta: 5 }));
        expect(stat.value).toBe(15);
    });

    it('no targetKey falls back to a string-keyed stat when no anonymous stat exists', () => {
        const { e, stat } = withStat(10);
        e.add(new Effect({ delta: 5 }));
        expect(stat.value).toBe(15);
    });

    it('no targetKey prefers the anonymous (symbol-keyed) stat over a named one', () => {
        const w = world();
        const e = w.createEntity();
        const named = e.add(new Stat({ value: 10 }), 'str');
        const anon  = e.add(new Stat({ value: 10 })); // Symbol key — should win
        e.add(new Effect({ delta: 5 }));
        expect(anon.value).toBe(15);
        expect(named.value).toBe(10);
    });

    it('targetKey selects the correct stat among multiple', () => {
        const w = world();
        const e = w.createEntity();
        const str = e.add(new Stat({ value: 10 }), 'str');
        const int = e.add(new Stat({ value: 5 }), 'int');
        e.add(new Effect({ targetKey: 'int', delta: 3 }));
        expect(int.value).toBe(8);
        expect(str.value).toBe(10);
    });

    it('wrong targetKey is a no-op and active stays false', () => {
        const { e, stat } = withStat(10);
        e.add(new Effect({ targetKey: 'dex', delta: 5 }), 'fx');
        expect(e.get(Effect, 'fx')!.active).toBeFalse();
        expect(stat.value).toBe(10);
    });
});

describe('Effect — target component class', () => {
    it('target: Health targets a Health component by key', () => {
        const w = world();
        const e = w.createEntity();
        const hp = e.add(new Health({ value: 100, min: 0 }), 'hp');
        e.add(new Effect({ target: Health, targetKey: 'hp', delta: 20 }));
        expect(hp.value).toBe(120);
    });

    it('removing the effect reverts the delta on the Health component', () => {
        const w = world();
        const e = w.createEntity();
        const hp = e.add(new Health({ value: 100, min: 0 }), 'hp');
        e.add(new Effect({ target: Health, targetKey: 'hp', delta: -30 }), 'fx');
        expect(hp.value).toBe(70);
        e.remove('fx');
        expect(hp.value).toBe(100);
    });

    it('target mismatch (wrong class for given key) is a no-op', () => {
        const { e, stat } = withStat(10);
        e.add(new Effect({ target: Health, targetKey: 'str', delta: 5 }), 'fx');
        expect(e.get(Effect, 'fx')!.active).toBeFalse();
        expect(stat.value).toBe(10);
    });

    it('target: Health without targetKey applies only to Health, not Stat', () => {
        const w = world();
        const e = w.createEntity();
        const str = e.add(new Stat({ value: 10 }), 'str');
        const hp  = e.add(new Health({ value: 100, min: 0 }), 'hp');
        e.add(new Effect({ target: Health, delta: 20 }));
        expect(hp.value).toBe(120);
        expect(str.value).toBe(10);
    });

    it('target: Health without targetKey applies to anonymous Health when Stat also present', () => {
        const w = world();
        const e = w.createEntity();
        const str = e.add(new Stat({ value: 10 }), 'str');
        const hp  = e.add(new Health({ value: 100, min: 0 })); // Symbol key
        e.add(new Effect({ target: Health, delta: -10 }));
        expect(hp.value).toBe(90);
        expect(str.value).toBe(10);
    });
});

describe('Effect — condition', () => {
    it('condition-based effect persists while condition is true', () => {
        const { w, e, stat } = withStat(10);
        w.globals.buffed = 1;
        e.add(new Effect({ targetKey: 'str', delta: 5, condition: '$.buffed > 0' }));
        w.update(10);
        expect(e.has(Effect)).toBeTrue();
        expect(stat.value).toBe(15);
    });

    it('condition-based effect is removed when condition becomes false', () => {
        const { w, e, stat } = withStat(10);
        w.globals.buffed = 1;
        e.add(new Effect({ targetKey: 'str', delta: 5, condition: '$.buffed > 0' }));
        w.globals.buffed = 0;
        w.update(0.01);
        expect(e.has(Effect)).toBeFalse();
        expect(stat.value).toBe(10);
    });
});

describe('Effect — clear() event count', () => {
    it('clear() emits expired exactly once even after subsequent update ticks', () => {
        const { w, e } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 1, duration: 100 }), 'fx');
        const count = { n: 0 };
        e.on('Effect(fx).expired', () => count.n++);
        e.get(Effect, 'fx')!.clear();
        w.update(0.01);
        w.update(0.01);
        expect(count.n).toBe(1);
    });

    it('natural duration expiry emits expired exactly once', () => {
        const { w, e } = withStat(10);
        e.add(new Effect({ targetKey: 'str', delta: 1, duration: 1 }), 'fx');
        const count = { n: 0 };
        e.on('Effect(fx).expired', () => count.n++);
        w.update(2);
        expect(count.n).toBe(1);
    });
});
