import { PlayerMotionModel } from '@logic/PlayerMotionModel';
import { JumpController, PARABOLIC_HORIZONTAL_SPEED, heightToImpulse } from '@logic/JumpController';
import {
    JUMP_HEIGHT_STANDARD,
    JUMP_HEIGHT_CHARGED,
    JUMP_HEIGHT_YELLOW,
    JUMP_PREPARE_DELAY_MS,
    PlayerColorState,
} from '@common/Constants';

function newPair(color: PlayerColorState = PlayerColorState.Red) {
    const motion = new PlayerMotionModel({
        aabb: { x: 0, y: 16, w: 16, h: 32 },
        platforms: [{ topY: 0, leftX: -500, rightX: 500 }],
        initialColorState: color,
    });
    motion.update(0.016); // settle on ground
    const jump = new JumpController(motion);
    return { motion, jump };
}

describe('JumpController — press / release lifecycle (req 2.2, 2.3, 2.4, 2.8)', () => {
    it('refuses to press when airborne (req 2.4)', () => {
        const { motion, jump } = newPair();
        motion.applyJumpImpulse(500); // lift off manually
        const res = jump.pressJump(0);
        expect(res.reason).toBe('airborne');
        expect(jump.isButtonEnabled()).toBe(false);
    });

    it('standard-press + quick release → standard jump height', () => {
        const { jump } = newPair();
        jump.pressJump(0);
        const res = jump.releaseJump(100, 'horizontal');
        expect(res.height).toBe(JUMP_HEIGHT_STANDARD);
        expect(res.phase).toBe('crouching');
    });

    it('holding ≥500ms produces the charged jump (req 2.3)', () => {
        const { jump } = newPair();
        jump.pressJump(0);
        const res = jump.releaseJump(600, 'horizontal');
        expect(res.height).toBe(JUMP_HEIGHT_CHARGED);
    });

    it('yellow color state uses 300px baseline jump (req 2.2)', () => {
        const { jump } = newPair(PlayerColorState.Yellow);
        jump.pressJump(0);
        const res = jump.releaseJump(50, 'horizontal', PlayerColorState.Yellow);
        expect(res.height).toBe(JUMP_HEIGHT_YELLOW);
    });
});

describe('JumpController — parabolic impulse (req 2.5)', () => {
    it('parabolic_right imparts +PARABOLIC_HORIZONTAL_SPEED', () => {
        const { jump } = newPair();
        jump.pressJump(0);
        const res = jump.releaseJump(100, 'parabolic_right');
        expect(res.horizontalImpulse).toBe(PARABOLIC_HORIZONTAL_SPEED);
    });

    it('parabolic_left imparts −PARABOLIC_HORIZONTAL_SPEED', () => {
        const { jump } = newPair();
        jump.pressJump(0);
        const res = jump.releaseJump(100, 'parabolic_left');
        expect(res.horizontalImpulse).toBe(-PARABOLIC_HORIZONTAL_SPEED);
    });
});

describe('JumpController — crouch delay + launch + re-enable (req 2.8, 2.4)', () => {
    it('does not apply impulse until after 150ms crouch delay', () => {
        const { motion, jump } = newPair();
        jump.pressJump(0);
        jump.releaseJump(100, 'horizontal');
        // Inside the crouch window — still grounded because impulse not applied.
        jump.tick(200);
        expect(motion.vy).toBe(0);
        expect(motion.isGrounded).toBe(true);
        jump.tick(100 + JUMP_PREPARE_DELAY_MS + 1);
        expect(motion.vy).toBeGreaterThan(0);
        expect(motion.isGrounded).toBe(false);
    });

    it('re-enables the jump button once the player lands again', () => {
        const { motion, jump } = newPair();
        jump.pressJump(0);
        jump.releaseJump(50, 'horizontal');
        jump.tick(50 + JUMP_PREPARE_DELAY_MS + 1);
        // Let gravity bring the player back down.
        for (let i = 0; i < 120; i++) motion.update(1 / 60);
        jump.tick(1_000);
        expect(motion.isGrounded).toBe(true);
        expect(jump.isButtonEnabled()).toBe(true);
    });
});

describe('heightToImpulse — physics math', () => {
    it('computes impulse such that peak equals the requested height', () => {
        const g = 2500;
        const h = 250;
        const v0 = heightToImpulse(h, g);
        // At apex v=0 ⇒ t = v0/g ⇒ peak = v0 * t - 0.5 * g * t^2 ⇒ v0^2 / (2g)
        expect((v0 * v0) / (2 * g)).toBeCloseTo(h, 3);
    });
});
