import { describe, expect, it } from 'vitest';
import { samplePose, validateClip, footMinY } from '../clip';
import type { AnimClip } from '../clip';
import { makeRig } from '../rig';

/** Простой клип-ходьба: кость «leg» качается, кость «root» приседает. */
function walkClip(): AnimClip {
    const bone = (rotX: number, rootY: number) => ({
        bones: { leg: { rot: [rotX, 0, 0] as const }, root: { pos: [0, rootY, 0] as const } },
    });
    return {
        name: 'walk',
        duration: 1,
        loop: true,
        keys: [
            { t: 0, ...bone(0, 0) },
            { t: 0.25, ...bone(0.5, -0.3) },
            { t: 0.5, ...bone(0, 0) },
            { t: 0.75, ...bone(-0.5, -0.3) },
            { t: 1, ...bone(0, 0) },
        ],
    };
}

describe('samplePose (семплинг позы)', () => {
    it('в ключах — точные значения', () => {
        const pose = samplePose(walkClip(), 0.25);
        expect(pose.leg?.rot).toEqual([0.5, 0, 0]);
        expect(pose.root?.pos).toEqual([0, -0.3, 0]);
    });

    it('между ключами — линейная интерполяция по каналам', () => {
        const pose = samplePose(walkClip(), 0.375); // середина между 0.25 и 0.5
        expect(pose.leg?.rot?.[0]).toBeCloseTo(0.25);
        expect(pose.root?.pos?.[1]).toBeCloseTo(-0.15);
    });

    it('зацикленный клип: t за пределами длительности заворачивается', () => {
        const a = samplePose(walkClip(), 1.25);
        const b = samplePose(walkClip(), 0.25);
        expect(a.leg?.rot?.[0]).toBeCloseTo(b.leg?.rot?.[0] ?? NaN);
        const c = samplePose(walkClip(), -0.75);
        expect(c.leg?.rot?.[0]).toBeCloseTo(b.leg?.rot?.[0] ?? NaN);
    });

    it('незацикленный клип: за концом держит последний ключ', () => {
        const clip = { ...walkClip(), loop: false };
        expect(samplePose(clip, 5).leg?.rot?.[0]).toBeCloseTo(0);
    });

    it('кость только в одном из соседних ключей — берётся из ближнего целиком', () => {
        const clip: AnimClip = {
            name: 'gaps', duration: 1, loop: false,
            keys: [
                { t: 0, bones: { arm: { rot: [1, 0, 0] } } },
                { t: 1, bones: {} },
            ],
        };
        const mid = samplePose(clip, 0.9);
        expect(mid.arm?.rot).toEqual([1, 0, 0]); // ближний ключ 0 несёт канал целиком
    });
});

describe('validateClip (числовая валидация)', () => {
    it('корректный цикл — пустой список проблем', () => {
        expect(validateClip(walkClip())).toEqual([]);
    });

    it('ловит разрыв цикла на стыке', () => {
        const clip = walkClip();
        clip.keys[clip.keys.length - 1] = { t: 1, bones: { leg: { rot: [0.3, 0, 0] } } };
        const problems = validateClip(clip);
        expect(problems.some((p) => p.includes('рвётся'))).toBe(true);
    });

    it('ловит немонотонность и выход t за границы', () => {
        const clip = walkClip();
        clip.keys[2].t = 0.1;
        const problems = validateClip(clip);
        expect(problems.some((p) => p.includes('не больше предыдущего'))).toBe(true);
        clip.keys[2].t = 2;
        expect(validateClip(clip).some((p) => p.includes('вне'))).toBe(true);
    });

    it('ловит пустой клип и нулевую длительность', () => {
        expect(validateClip({ name: 'e', duration: 0, loop: false, keys: [] }).length).toBe(2);
    });
});

describe('footMinY (контакт стопы)', () => {
    it('кость, поднятая позой, поднимает свои воксели', () => {
        // модель 1×2×2: воксели (0,0,0) кость foot, (0,1,0) кость body
        const model = {
            size: [1, 2, 2] as [number, number, number],
            palette: { 1: '#101010', 2: '#202020' },
            data: btoa(String.fromCharCode(1, 2, 0, 0)),
        };
        const rig = makeRig(
            [
                { name: 'root', parent: -1, pivot: [0.5, 1, 0.5] },
                { name: 'foot', parent: 0, pivot: [0.5, 0.5, 0.5] },
            ],
            model,
            [1, 0, -1, -1],
        );
        const clip: AnimClip = {
            name: 'hop', duration: 1, loop: true,
            keys: [
                { t: 0, bones: { foot: { pos: [0, 0, 0] } } },
                { t: 0.5, bones: { foot: { pos: [0, 2, 0] } } },
                { t: 1, bones: { foot: { pos: [0, 0, 0] } } },
            ],
        };
        expect(footMinY(rig, model, clip, 'foot', 0)).toBeCloseTo(0.5); // центр вокселя на y=0.5
        expect(footMinY(rig, model, clip, 'foot', 0.25)).toBeCloseTo(1.5); // интерполяция +1
    });
});