import { describe, expect, it } from 'vitest';
import { VoxelWorld } from '../../world/world';
import { makeBody, stepBody, collides, GRAVITY } from '../body';
import type { Body } from '../body';

/** Мир сцены теста: земля/платформы/стены/потолок. Высота мира — 16 (аргумент VoxelWorld). */
function testWorld(): VoxelWorld {
    const w = new VoxelWorld(16);
    // земля y=0..1 для x=0..9; уступ ниже (y=0) для x≥10
    for (let x = 0; x < 10; x++)
        for (let z = 0; z < 8; z++) {
            w.set(x, 0, z, 1);
            w.set(x, 1, z, 1);
        }
    for (let x = 10; x < 16; x++)
        for (let z = 0; z < 8; z++) w.set(x, 0, z, 1);
    // стена-колонна x=6 (толщина 1), y=2..4 — торчит над землёй
    for (let y = 2; y <= 4; y++)
        for (let z = 0; z < 8; z++) w.set(6, y, z, 1);
    // потолок-плита y=7 над x=0..3 (не задевает x=5, где падает тестовое тело)
    for (let x = 0; x <= 3; x++)
        for (let z = 0; z < 8; z++) w.set(x, 7, z, 1);
    // вторая стена z=0 (x=4..8, y=2..4) — вместе со стеной x=6 образует угол
    for (let x = 4; x <= 8; x++)
        for (let y = 2; y <= 4; y++) w.set(x, y, 0, 1);
    return w;
}

/** Прогнать шагов физики, пока не выполнится предикат (с лимитом). */
function runUntil(body: Body, world: VoxelWorld, pred: () => boolean, dt = 1 / 60, limit = 600): void {
    for (let i = 0; i < limit && !pred(); i++) stepBody(world, body, dt);
}

describe('collides (AABB против вокселей)', () => {
    it('в свободном пространстве — нет, с пересечением вокселя — да', () => {
        const w = testWorld();
        expect(collides(w, [5, 3, 5], [1, 2, 1])).toBe(false); // над землёй, вне стены
        expect(collides(w, [5, 1, 5], [1, 2, 1])).toBe(true);   // ноги в земле
    });

    it('касание грани снаружи — не столкновение', () => {
        const w = testWorld();
        expect(collides(w, [5, 2, 5], [1, 2, 1])).toBe(false); // ноги ровно на земле (y=2)
    });
});

describe('stepBody (падение и опора)', () => {
    it('тело падает и встаёт на землю ровно на грань', () => {
        const w = testWorld();
        const body = makeBody([5, 6, 5], [1, 2, 1]);
        runUntil(body, w, () => body.grounded);
        expect(body.pos[1]).toBeCloseTo(2);
        expect(body.vel[1]).toBe(0);
    });

    it('стоит на месте: позиция не плывёт, grounded держится', () => {
        const w = testWorld();
        const body = makeBody([5, 2, 5], [1, 2, 1]);
        body.grounded = true;
        for (let i = 0; i < 120; i++) stepBody(w, body, 1 / 60);
        expect(body.pos[1]).toBeCloseTo(2);
        expect(body.grounded).toBe(true);
        expect(body.pos[0]).toBeCloseTo(5);
    });

    it('сход с уступа: падает с верхней площадки на нижнюю', () => {
        const w = testWorld();
        const body = makeBody([9, 2, 5], [1, 2, 1]);
        runUntil(body, w, () => body.grounded); // сначала встать на верхнюю площадку
        body.vel[0] = 3; // идём на восток, за x=10 земля ниже
        runUntil(body, w, () => !body.grounded);
        expect(body.pos[0]).toBeGreaterThanOrEqual(9.5);
        runUntil(body, w, () => body.grounded && body.pos[0] > 10.5);
        expect(body.pos[1]).toBeCloseTo(1); // нижний уровень: грань y=1
    });
});

describe('stepBody (стены и зажимы)', () => {
    it('стена толщиной 1 останавливает, скорость обнуляется', () => {
        const w = testWorld();
        const body = makeBody([5, 2, 5], [1, 2, 1]); // запад от стены x=6
        body.vel[0] = 4;
        runUntil(body, w, () => body.vel[0] === 0);
        expect(body.pos[0]).toBeCloseTo(6 - 0.5 - 1e-4, 3); // упёрся в западную грань стены
        expect(body.vel[0]).toBe(0);
        expect(body.grounded).toBe(true);
    });

    it('быстрое движение не туннелирует сквозь тонкую стену', () => {
        const w = testWorld();
        const body = makeBody([5, 2, 5], [1, 2, 1]);
        body.vel[0] = 50; // 50 вокс/с при dt=0.1 — 5 вокселей за шаг
        stepBody(w, body, 0.1);
        expect(body.pos[0]).toBeLessThan(6);
        expect(body.pos[0]).toBeCloseTo(6 - 0.5 - 1e-4, 3);
    });

    it('угол: две стены по осям зажимают независимо', () => {
        const w = testWorld();
        const body = makeBody([5, 2, 2], [1, 2, 1]); // у угла стен x=6 и z=0
        body.vel[0] = 4;
        body.vel[2] = -4;
        runUntil(body, w, () => body.vel[0] === 0 && body.vel[2] === 0);
        expect(body.pos[0]).toBeCloseTo(6 - 0.5 - 1e-4, 3); // зажат западной гранью стены x=6
        expect(body.pos[2]).toBeCloseTo(1 + 0.5 + 1e-4, 3); // ...и северной гранью стены z=0
        expect(body.pos[1]).toBeCloseTo(2); // вертикаль не пострадала
    });

    it('потолок гасит подъём', () => {
        const w = testWorld();
        const body = makeBody([3, 2, 5], [1, 2, 1]); // под плитой y=7
        body.vel[1] = 20;
        runUntil(body, w, () => body.vel[1] === 0 && body.pos[1] > 3);
        expect(body.pos[1]).toBeCloseTo(7 - 2 - 1e-4, 2); // голова упёрлась в грань y=7
        expect(body.grounded).toBe(false);
    });
});

describe('гравитация (число)', () => {
    it('полунеявный Эйлер: за шаг dt скорость −G·dt, позиция −G·dt²', () => {
        const w = testWorld();
        const body = makeBody([8, 20, 5], [1, 2, 1]); // вдали от стен, высоко
        stepBody(w, body, 0.5);
        expect(body.vel[1]).toBeCloseTo(-GRAVITY * 0.5, 6);
        expect(body.pos[1]).toBeCloseTo(20 - GRAVITY * 0.5 * 0.5, 6);
    });
});