import { describe, it, expect } from 'vitest';
import { Container } from 'pixi.js';
import { Camera } from '../Camera';
import { screenToWorld, worldToScreen } from '../../math/iso';

/** Смещение цели от центра камеры в экранных пикселях: мировая точка. */
function targetAtScreenOffset(cam: Camera, dxPx: number, dyPx: number): { x: number; y: number } {
    const c = worldToScreen(cam.x, cam.y);
    return screenToWorld(c.x + dxPx, c.y + dyPx);
}

describe('Camera dead zone', () => {
    it('без dead zone — камера центрируется на цели', () => {
        const cam = new Camera(480, 270);
        cam.follow(3, 5);
        expect(cam.x).toBe(3);
        expect(cam.y).toBe(5);
    });

    it('цель внутри окна — камера стоит', () => {
        const cam = new Camera(480, 270);
        cam.snap(3, 5);
        cam.deadZonePx = { width: 160, height: 100 };
        // Цель на 60 px правее и 30 px ниже центра экрана — внутри окна.
        const t = targetAtScreenOffset(cam, 60, 30);
        cam.follow(t.x, t.y);
        expect(cam.x).toBe(3);
        expect(cam.y).toBe(5);
    });

    it('цель толкает камеру ровно на величину выхода', () => {
        const cam = new Camera(480, 270);
        cam.snap(3, 5);
        cam.deadZonePx = { width: 160, height: 100 };
        // Выход за окно: 20 px вправо и 20 px вверх.
        const t = targetAtScreenOffset(cam, 100, -70);
        cam.follow(t.x, t.y);
        // Центр проекции сместился ровно на (20, −20) экранных пикселей.
        const c0 = worldToScreen(3, 5);
        const c = worldToScreen(cam.x, cam.y);
        expect(c.x - c0.x).toBeCloseTo(20, 6);
        expect(c.y - c0.y).toBeCloseTo(-20, 6);
    });

    it('snap ставит камеру на точку, минуя окно', () => {
        const cam = new Camera(480, 270);
        cam.deadZonePx = { width: 160, height: 100 };
        cam.snap(5, 2);
        expect(cam.x).toBe(5);
        expect(cam.y).toBe(2);
        // Окно сохранено: цель чуть правее центра — камера не сдвигается.
        const t = targetAtScreenOffset(cam, 50, 0);
        cam.follow(t.x, t.y);
        expect(cam.x).toBe(5);
    });
});

describe('Camera bounds (мировые юниты)', () => {
    it('границы мира держат камеру прижатой', () => {
        const cam = new Camera(480, 270);
        cam.bounds = { x: 0, y: 0, width: 64, height: 64 };
        cam.snap(-1000, 500); // за границей по обеим осям
        // bbox карты: x = (0 - 64)*16, y = 0; клэмп по проекции центра.
        const c = worldToScreen(cam.x, cam.y);
        expect(c.x).toBeCloseTo(-1024 + 240, 6);
        expect(c.y).toBeCloseTo(135, 6);
    });

    it('apply ставит worldRoot по округлённой проекции', () => {
        const cam = new Camera(480, 270);
        cam.snap(3, 5);
        const view = new Container();
        cam.apply(view);
        const applied = worldToScreen(cam.x, cam.y);
        expect(view.x).toBe(240 - Math.round(applied.x));
        expect(view.y).toBe(135 - Math.round(applied.y));
    });

    it('toScreen согласована с apply: точка мира проецируется в то же место', () => {
        const cam = new Camera(480, 270);
        cam.snap(3, 5);
        const view = new Container();
        cam.apply(view);
        // Точка мира в worldRoot: worldToScreen(p) + позиция worldRoot.
        const wx = 3.5, wy = 5.5;
        const p = worldToScreen(wx, wy);
        expect(view.x + p.x).toBeCloseTo(cam.toScreen(wx, wy).x, 6);
        expect(view.y + p.y).toBeCloseTo(cam.toScreen(wx, wy).y, 6);
    });

    it('toScreen учитывает тряску так же, как apply', () => {
        const cam = new Camera(480, 270);
        cam.snap(3, 5);
        cam.addShake(8, 1); // тряска активна, offset фиксирован до update
        const view = new Container();
        cam.apply(view);
        const p = worldToScreen(3.5, 5.5);
        expect(view.x + p.x).toBeCloseTo(cam.toScreen(3.5, 5.5).x, 6);
        expect(view.y + p.y).toBeCloseTo(cam.toScreen(3.5, 5.5).y, 6);
    });
});

describe('Camera zoom', () => {
    it('apply ставит scale и позицию при z=2, точка мира проецируется на место', () => {
        const cam = new Camera(480, 270);
        cam.snap(3, 5);
        cam.setZoom(2);
        const view = new Container();
        cam.apply(view);
        expect(view.scale.x).toBe(2);
        // Точка мира в worldRoot: p*zoom + позиция worldRoot (формула toScreen).
        const wx = 3.5, wy = 5.5;
        const p = worldToScreen(wx, wy);
        expect(view.x + p.x * 2).toBeCloseTo(cam.toScreen(wx, wy).x, 6);
        expect(view.y + p.y * 2).toBeCloseTo(cam.toScreen(wx, wy).y, 6);
        // Центр взгляда — в центре экрана.
        const c = worldToScreen(3, 5);
        expect(view.x + c.x * 2).toBeCloseTo(240, 6);
        expect(view.y + c.y * 2).toBeCloseTo(135, 6);
    });

    it('тряска не масштабируется зумом', () => {
        const cam = new Camera(480, 270);
        cam.snap(3, 5);
        cam.setZoom(2);
        cam.addShake(4, 1); // offset фиксирован до update
        const view = new Container();
        cam.apply(view);
        // Та же камера без тряски — разница позиций и есть offset.
        const calm = new Camera(480, 270);
        calm.snap(3, 5);
        calm.setZoom(2);
        const calmView = new Container();
        calm.apply(calmView);
        // Сдвиг от тряски одинаков в apply и toScreen (добавлен один раз).
        expect(view.x - calmView.x).toBeCloseTo(cam.toScreen(3.5, 5.5).x - calm.toScreen(3.5, 5.5).x, 6);
        expect(view.y - calmView.y).toBeCloseTo(cam.toScreen(3.5, 5.5).y - calm.toScreen(3.5, 5.5).y, 6);
    });

    it('clamp при z=2 держит прижатие к границе мира', () => {
        const cam = new Camera(480, 270);
        cam.bounds = { x: 0, y: 0, width: 64, height: 64 };
        cam.setZoom(2);
        cam.snap(-1000, 500);
        // bbox карты ×2: x = (0-64)*16*2 = -2048; центр прижат: -2048 + 480/2.
        const c = worldToScreen(cam.x, cam.y);
        expect(c.x * 2).toBeCloseTo(-2048 + 240, 6);
        expect(c.y * 2).toBeCloseTo(135, 6);
    });

    it('zoomTo анимируется и квантуется, финал — ровно целевой зум', () => {
        const cam = new Camera(480, 270);
        cam.zoomTo(2, 1);
        cam.update(0.5);
        const mid = cam.zoomLevel;
        expect(mid).toBeGreaterThan(1);
        expect(mid).toBeLessThan(2);
        expect(mid * 4).toBeCloseTo(Math.round(mid * 4), 6); // кратно 0.25
        cam.update(0.5);
        expect(cam.zoomLevel).toBe(2);
        expect(cam.zoomLevel * 4).toBeCloseTo(Math.round(cam.zoomLevel * 4), 6);
    });

    it('zoomTo с seconds=0 — мгновенно; setZoom сбрасывает анимацию', () => {
        const cam = new Camera(480, 270);
        cam.zoomTo(2, 0);
        expect(cam.zoomLevel).toBe(2);
        cam.zoomTo(1, 10);
        cam.setZoom(1.5);
        cam.update(1);
        expect(cam.zoomLevel).toBe(1.5); // анимация отменена
    });

    it('dead zone при зуме: выход цели считается в экранных пикселях', () => {
        const cam = new Camera(480, 270);
        cam.snap(3, 5);
        cam.setZoom(2);
        cam.deadZonePx = { width: 160, height: 100 };
        // Экранный разнос цели 60px при зуме 2 — внутри окна: камера стоит.
        const c = worldToScreen(3, 5);
        const t = screenToWorld(c.x + 30, c.y + 15); // 30 мировых px = 60 экранных
        cam.follow(t.x, t.y);
        expect(cam.x).toBe(3);
        expect(cam.y).toBe(5);
    });
});