import { AttackController, IJumpStateProvider } from '@logic/AttackController';
import { WeaponType } from '@data/Interfaces';
import { PlayerColorState } from '@common/Constants';

function makeJumpState(ts?: number): IJumpStateProvider {
    return {
        lastJumpPressTs: () => ts,
        isGrounded: () => ts === undefined,
    };
}

describe('AttackController — mutual exclusion (req 3.1-3.3)', () => {
    it('first-pressed weapon wins when both buttons go down together', () => {
        const ac = new AttackController();
        ac.press(WeaponType.Shuriken, 10);
        ac.press(WeaponType.NinjaSword, 11);
        expect(ac.getActive()).toBe(WeaponType.Shuriken);
    });

    it('releasing the active weapon transfers activation to the still-held one', () => {
        const ac = new AttackController();
        ac.press(WeaponType.Shuriken, 0);
        ac.press(WeaponType.NinjaSword, 10);
        ac.release(WeaponType.Shuriken);
        expect(ac.getActive()).toBe(WeaponType.NinjaSword);
    });

    it('releasing the only pressed weapon deactivates everything', () => {
        const ac = new AttackController();
        ac.press(WeaponType.Shuriken, 0);
        ac.release(WeaponType.Shuriken);
        expect(ac.getActive()).toBe('none');
    });
});

describe('AttackController — firing intervals (req 3.4, 3.6)', () => {
    it('shuriken fires every 300ms for red/green, 250ms for yellow', () => {
        const ac = new AttackController();
        ac.press(WeaponType.Shuriken, 0);
        expect(ac.tick(0, PlayerColorState.Red).length).toBe(1);
        expect(ac.tick(299, PlayerColorState.Red).length).toBe(0);
        expect(ac.tick(300, PlayerColorState.Red).length).toBe(1);

        const fast = new AttackController();
        fast.press(WeaponType.Shuriken, 0);
        fast.tick(0, PlayerColorState.Yellow);
        expect(fast.tick(249, PlayerColorState.Yellow).length).toBe(0);
        expect(fast.tick(250, PlayerColorState.Yellow).length).toBe(1);
    });

    it('sword fires every 500ms', () => {
        const ac = new AttackController();
        ac.press(WeaponType.NinjaSword, 0);
        ac.tick(0, PlayerColorState.Red);
        expect(ac.tick(499, PlayerColorState.Red).length).toBe(0);
        expect(ac.tick(500, PlayerColorState.Red).length).toBe(1);
    });

    it('shuriken burst index caps at SHURIKEN_BURST_MAX', () => {
        const ac = new AttackController();
        ac.press(WeaponType.Shuriken, 0);
        const indexes: number[] = [];
        for (let i = 0; i <= 600; i += 300) {
            const fires = ac.tick(i, PlayerColorState.Red);
            if (fires.length) indexes.push(fires[0].burstIndex);
        }
        expect(indexes).toEqual([1, 2, 3]);
        // One more attempt must still cap at 3 not 4.
        const more = ac.tick(900, PlayerColorState.Red);
        expect(more[0].burstIndex).toBe(3);
    });
});

describe('AttackController — combo window (req 4.1)', () => {
    it('comboWithJump is true when jump timestamp is within 100ms', () => {
        const ac = new AttackController(makeJumpState(95));
        ac.press(WeaponType.Shuriken, 100);
        const fires = ac.tick(100, PlayerColorState.Red);
        expect(fires[0].comboWithJump).toBe(true);
    });

    it('comboWithJump is false when jump was pressed >100ms ago', () => {
        const ac = new AttackController(makeJumpState(0));
        ac.press(WeaponType.Shuriken, 200);
        const fires = ac.tick(200, PlayerColorState.Red);
        expect(fires[0].comboWithJump).toBe(false);
    });

    it('comboWithJump is false when the player has not jumped yet', () => {
        const ac = new AttackController(makeJumpState(undefined));
        ac.press(WeaponType.Shuriken, 0);
        const fires = ac.tick(0, PlayerColorState.Red);
        expect(fires[0].comboWithJump).toBe(false);
    });
});

describe('AttackController — reset()', () => {
    it('clears active / pressed / cooldowns', () => {
        const ac = new AttackController();
        ac.press(WeaponType.Shuriken, 0);
        ac.tick(0, PlayerColorState.Red);
        ac.reset();
        expect(ac.getActive()).toBe('none');
        expect(ac.isPressed(WeaponType.Shuriken)).toBe(false);
    });
});
