/*
 * © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
 */

package com.tiedup.remake.rig.anim;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;

import org.junit.jupiter.api.Test;

import com.tiedup.remake.rig.anim.TransformSheet.InterpolationInfo;
import com.tiedup.remake.rig.armature.JointTransform;

/**
 * Tests de {@link TransformSheet} : interpolation (binary search),
 * maxFrameTime, copy/copyAll.
 *
 * Aucun MC runtime requis — TransformSheet n'utilise que
 * net.minecraft.util.Mth (pure math, pas de bootstrap) et des types
 * RIG internes (Vec3f, JointTransform).
 */
class TransformSheetTest {

    private static Keyframe kf(float time) {
        return new Keyframe(time, JointTransform.empty());
    }

    private static TransformSheet sheet(float... times) {
        Keyframe[] frames = new Keyframe[times.length];
        for (int i = 0; i < times.length; i++) {
            frames[i] = kf(times[i]);
        }
        return new TransformSheet(frames);
    }

    // --- getInterpolationInfo ---

    /** Sheet vide doit retourner INVALID, pas NPE. */
    @Test
    void interpolationInfo_emptySheet_returnsInvalid() {
        TransformSheet ts = new TransformSheet(new Keyframe[0]);
        assertSame(InterpolationInfo.INVALID, ts.getInterpolationInfo(0.5f),
            "Sheet vide doit retourner INVALID");
    }

    /** Sur [0.0, 1.0] au temps 0.5, delta = 0.5. */
    @Test
    void interpolationInfo_twoKeyframes_midpointDelta() {
        TransformSheet ts = sheet(0.0f, 1.0f);
        InterpolationInfo info = ts.getInterpolationInfo(0.5f);
        assertEquals(0, info.prev());
        assertEquals(1, info.next());
        assertEquals(0.5f, info.delta(), 1e-5f);
    }

    /**
     * Temps negatif est converti en maxTime + currentTime.
     * Sur [0.0, 1.0], temps=-0.25 -> effectif=0.75 -> delta=0.75.
     */
    @Test
    void interpolationInfo_negativeTime_wrapsToEnd() {
        TransformSheet ts = sheet(0.0f, 1.0f);
        InterpolationInfo info = ts.getInterpolationInfo(-0.25f);
        assertEquals(0.75f, info.delta(), 1e-5f,
            "Temps negatif doit wrapper : maxTime + t");
    }

    /** Binary search sur 5 keyframes irreguliers — segment correct. */
    @Test
    void interpolationInfo_multipleKeyframes_correctSegment() {
        TransformSheet ts = sheet(0.0f, 0.1f, 0.5f, 0.9f, 1.0f);
        InterpolationInfo info = ts.getInterpolationInfo(0.7f);
        assertEquals(2, info.prev(), "0.7 entre index 2 (0.5) et 3 (0.9)");
        assertEquals(3, info.next());
        float expected = (0.7f - 0.5f) / (0.9f - 0.5f);
        assertEquals(expected, info.delta(), 1e-5f);
    }

    /** Temps exactement sur un keyframe interne : delta = 0. */
    @Test
    void interpolationInfo_exactKeyframeTime_deltaZero() {
        TransformSheet ts = sheet(0.0f, 0.5f, 1.0f);
        InterpolationInfo info = ts.getInterpolationInfo(0.5f);
        assertEquals(0.0f, info.delta(), 1e-5f,
            "Temps exact sur keyframe => delta 0");
    }

    /** Temps au-dela du max : delta clamp a 1.0. */
    @Test
    void interpolationInfo_timeExceedsMax_deltaClampedToOne() {
        TransformSheet ts = sheet(0.0f, 1.0f);
        InterpolationInfo info = ts.getInterpolationInfo(2.0f);
        assertEquals(1.0f, info.delta(), 1e-5f,
            "Temps > max => delta clamp a 1.0");
    }

    // --- maxFrameTime ---

    /** Sheet vide : maxFrameTime retourne la sentinelle -1. */
    @Test
    void maxFrameTime_emptySheet_returnsMinusOne() {
        TransformSheet ts = new TransformSheet(new Keyframe[0]);
        assertEquals(-1.0f, ts.maxFrameTime(), 1e-5f);
    }

    /** maxFrameTime retourne le max quel que soit l'ordre des keyframes. */
    @Test
    void maxFrameTime_multipleKeyframes_returnsMax() {
        TransformSheet ts = sheet(0.0f, 0.3f, 1.5f, 0.7f);
        assertEquals(1.5f, ts.maxFrameTime(), 1e-5f);
    }

    // --- copy / copyAll ---

    /** copyAll produit une instance distincte de meme taille. */
    @Test
    void copyAll_producesIndependentCopy() {
        TransformSheet ts = sheet(0.0f, 0.5f, 1.0f);
        TransformSheet copy = ts.copyAll();
        assertNotSame(ts, copy);
        assertEquals(ts.getKeyframes().length, copy.getKeyframes().length);
    }

    /** copy(start, end) extrait le bon sous-intervalle. */
    @Test
    void copy_subrange_correctLength() {
        TransformSheet ts = sheet(0.0f, 0.25f, 0.5f, 0.75f, 1.0f);
        TransformSheet sub = ts.copy(1, 4);
        assertEquals(3, sub.getKeyframes().length,
            "copy(1,4) extrait 3 keyframes");
        assertEquals(0.25f, sub.getKeyframes()[0].time(), 1e-5f,
            "Premier keyframe de la sous-copie = index 1 de l'original");
    }

    /**
     * BUG FLAG (pas de correctif demande) : getFirstFrame() appelle copy(0, 2)
     * sans verifier que la sheet a au moins 2 keyframes. Sur une sheet de 1
     * keyframe, ArrayIndexOutOfBoundsException. Ce test documente le cas sain
     * (2 keyframes) pour catcher toute regression.
     */
    @Test
    void getFirstFrame_twoKeyframes_doesNotThrow() {
        TransformSheet ts = sheet(0.0f, 1.0f);
        TransformSheet first = ts.getFirstFrame();
        assertEquals(2, first.getKeyframes().length);
    }
}
