/*
 * © 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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

import com.tiedup.remake.rig.armature.JointTransform;
import com.tiedup.remake.rig.math.Vec3f;

/**
 * Tests de {@link Pose} : interpolatePose, orElseEmpty, load (SET / OVERWRITE /
 * APPEND_ABSENT), disableAllJoints.
 *
 * Aucun MC runtime requis — Pose utilise uniquement des collections Java et
 * JointTransform (JOML + Vec3f, pas de Minecraft bootstrap).
 */
class PoseTest {

    // --- helpers ---

    private static JointTransform translation(float x, float y, float z) {
        return JointTransform.translation(new Vec3f(x, y, z));
    }

    // --- orElseEmpty ---

    /** Un joint absent retourne JointTransform.empty() (pas null). */
    @Test
    void orElseEmpty_unknownJoint_returnsEmpty() {
        Pose pose = new Pose();
        JointTransform result = pose.orElseEmpty("nonexistent");
        assertNotNull(result, "orElseEmpty ne doit pas retourner null");
        // Verifier que la translation est zeros (empty = identite)
        assertEquals(0.0f, result.translation().x, 1e-5f);
        assertEquals(0.0f, result.translation().y, 1e-5f);
        assertEquals(0.0f, result.translation().z, 1e-5f);
    }

    /** Un joint present retourne son transform, pas le fallback. */
    @Test
    void orElseEmpty_knownJoint_returnsTransform() {
        Pose pose = new Pose();
        JointTransform jt = translation(1.0f, 2.0f, 3.0f);
        pose.putJointData("spine", jt);
        assertEquals(jt, pose.orElseEmpty("spine"));
    }

    // --- interpolatePose ---

    /**
     * interpolatePose a progression 0.0 doit retourner les valeurs de pose1.
     */
    @Test
    void interpolatePose_progressionZero_returnsFirstPose() {
        Pose pose1 = new Pose();
        pose1.putJointData("arm", translation(0.0f, 0.0f, 0.0f));

        Pose pose2 = new Pose();
        pose2.putJointData("arm", translation(1.0f, 0.0f, 0.0f));

        Pose result = Pose.interpolatePose(pose1, pose2, 0.0f);
        assertTrue(result.hasTransform("arm"));
        assertEquals(0.0f, result.get("arm").translation().x, 1e-5f);
    }

    /**
     * interpolatePose a progression 1.0 doit retourner les valeurs de pose2.
     */
    @Test
    void interpolatePose_progressionOne_returnsSecondPose() {
        Pose pose1 = new Pose();
        pose1.putJointData("arm", translation(0.0f, 0.0f, 0.0f));

        Pose pose2 = new Pose();
        pose2.putJointData("arm", translation(10.0f, 0.0f, 0.0f));

        Pose result = Pose.interpolatePose(pose1, pose2, 1.0f);
        assertEquals(10.0f, result.get("arm").translation().x, 1e-4f);
    }

    /**
     * interpolatePose fusionne les joints des deux poses.
     * Un joint present dans pose2 seulement doit apparaitre dans le resultat
     * (interpole avec JointTransform.empty() comme pose1).
     */
    @Test
    void interpolatePose_mergesJointsFromBothPoses() {
        Pose pose1 = new Pose();
        pose1.putJointData("head", translation(0.0f, 1.0f, 0.0f));

        Pose pose2 = new Pose();
        pose2.putJointData("hand", translation(2.0f, 0.0f, 0.0f));

        Pose result = Pose.interpolatePose(pose1, pose2, 0.5f);
        // Les deux joints doivent etre presents
        assertTrue(result.hasTransform("head"), "head from pose1 doit etre dans le resultat");
        assertTrue(result.hasTransform("hand"), "hand from pose2 doit etre dans le resultat");
    }

    /**
     * interpolatePose a 0.5 doit etre a mi-chemin de la translation.
     */
    @Test
    void interpolatePose_halfProgression_midpointTranslation() {
        Pose pose1 = new Pose();
        pose1.putJointData("leg", translation(0.0f, 0.0f, 0.0f));

        Pose pose2 = new Pose();
        pose2.putJointData("leg", translation(4.0f, 0.0f, 0.0f));

        Pose result = Pose.interpolatePose(pose1, pose2, 0.5f);
        assertEquals(2.0f, result.get("leg").translation().x, 1e-4f);
    }

    // --- load(LoadOperation) ---

    /** SET efface les joints existants puis copie depuis la source. */
    @Test
    void load_set_replacesAllJoints() {
        Pose dest = new Pose();
        dest.putJointData("old_joint", JointTransform.empty());

        Pose src = new Pose();
        src.putJointData("new_joint", translation(1.0f, 0.0f, 0.0f));

        dest.load(src, Pose.LoadOperation.SET);

        assertFalse(dest.hasTransform("old_joint"),
            "SET doit supprimer les joints de la destination avant fusion");
        assertTrue(dest.hasTransform("new_joint"),
            "SET doit copier les joints source");
    }

    /** OVERWRITE ecrase les joints communs sans supprimer les joints uniquement dans dest. */
    @Test
    void load_overwrite_keepsExistingAndOverwritesCommon() {
        Pose dest = new Pose();
        dest.putJointData("kept", JointTransform.empty());
        dest.putJointData("shared", translation(0.0f, 0.0f, 0.0f));

        Pose src = new Pose();
        src.putJointData("shared", translation(5.0f, 0.0f, 0.0f));

        dest.load(src, Pose.LoadOperation.OVERWRITE);

        assertTrue(dest.hasTransform("kept"),
            "OVERWRITE doit conserver les joints de dest non presents dans src");
        assertEquals(5.0f, dest.get("shared").translation().x, 1e-4f,
            "OVERWRITE doit ecraser le joint commun avec la valeur src");
    }

    /** APPEND_ABSENT n'ajoute que les joints absents de dest. */
    @Test
    void load_appendAbsent_doesNotOverwriteExisting() {
        Pose dest = new Pose();
        dest.putJointData("existing", translation(9.0f, 0.0f, 0.0f));

        Pose src = new Pose();
        src.putJointData("existing", translation(1.0f, 0.0f, 0.0f));
        src.putJointData("added", translation(2.0f, 0.0f, 0.0f));

        dest.load(src, Pose.LoadOperation.APPEND_ABSENT);

        assertEquals(9.0f, dest.get("existing").translation().x, 1e-4f,
            "APPEND_ABSENT ne doit pas ecraser un joint existant");
        assertTrue(dest.hasTransform("added"),
            "APPEND_ABSENT doit ajouter les joints absents");
    }

    // --- disableAllJoints ---

    /** disableAllJoints vide la map de transforms. */
    @Test
    void disableAllJoints_clearsTransforms() {
        Pose pose = new Pose();
        pose.putJointData("head", JointTransform.empty());
        pose.putJointData("arm", JointTransform.empty());
        pose.disableAllJoints();
        assertTrue(pose.getJointTransformData().isEmpty(),
            "disableAllJoints doit vider la map de transforms");
    }
}
