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

package com.tiedup.remake.v2.bondage.datadriven;

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.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.HashMap;
import java.util.Map;

import net.minecraft.resources.ResourceLocation;

import org.junit.jupiter.api.Test;

import com.tiedup.remake.rig.anim.LivingMotion;
import com.tiedup.remake.rig.anim.LivingMotions;

/**
 * Tests unitaires pour {@link AnimationBindings} :
 * <ul>
 *   <li>construction normale et cas nullables (P3-02),</li>
 *   <li>comportement du compact ctor (null -> empty map),</li>
 *   <li>immutabilite defensive de {@code livingMotions},</li>
 *   <li>contrat de {@link AnimationBindings#isEmpty()},</li>
 *   <li>constante {@link AnimationBindings#EMPTY}.</li>
 * </ul>
 *
 * Aucun MC runtime requis — {@link ResourceLocation} est utilisable hors
 * MinecraftServer, et {@link LivingMotion} est un interface pur.
 */
class AnimationBindingsTest {

    private static final ResourceLocation ANIM_IDLE =
        ResourceLocation.fromNamespaceAndPath("tiedup", "arms_cuffed_idle");
    private static final ResourceLocation ANIM_WALK =
        ResourceLocation.fromNamespaceAndPath("tiedup", "arms_cuffed_walk");
    private static final ResourceLocation ANIM_EQUIP =
        ResourceLocation.fromNamespaceAndPath("tiedup", "cuffs_equip");
    private static final ResourceLocation ANIM_UNEQUIP =
        ResourceLocation.fromNamespaceAndPath("tiedup", "cuffs_unequip");

    /** Construction nominale avec des donnees valides. */
    @Test
    void construction_withValidData_succeeds() {
        Map<LivingMotion, ResourceLocation> motions = Map.of(
            LivingMotions.IDLE, ANIM_IDLE,
            LivingMotions.WALK, ANIM_WALK
        );

        AnimationBindings bindings = new AnimationBindings(motions, ANIM_EQUIP, ANIM_UNEQUIP);

        assertEquals(2, bindings.livingMotions().size());
        assertEquals(ANIM_IDLE, bindings.livingMotions().get(LivingMotions.IDLE));
        assertEquals(ANIM_WALK, bindings.livingMotions().get(LivingMotions.WALK));
        assertEquals(ANIM_EQUIP, bindings.onEquip());
        assertEquals(ANIM_UNEQUIP, bindings.onUnequip());
    }

    /**
     * Le compact ctor doit remplacer un {@code livingMotions} null par
     * {@link java.util.Collections#emptyMap()} — jamais laisser le champ a null.
     */
    @Test
    void construction_withNullLivingMotions_usesEmptyMap() {
        AnimationBindings bindings = new AnimationBindings(null, null, null);

        assertNotNull(bindings.livingMotions(),
            "livingMotions ne doit jamais etre null (compact ctor garantit non-null)");
        assertTrue(bindings.livingMotions().isEmpty());
    }

    /** {@code onEquip} / {@code onUnequip} null sont legaux (optionnels). */
    @Test
    void construction_withNullEquip_isTolerated() {
        Map<LivingMotion, ResourceLocation> motions = Map.of(LivingMotions.IDLE, ANIM_IDLE);

        AnimationBindings bindings = new AnimationBindings(motions, null, null);

        assertEquals(1, bindings.livingMotions().size());
        assertNull(bindings.onEquip());
        assertNull(bindings.onUnequip());
    }

    /**
     * {@link AnimationBindings#isEmpty()} renvoie {@code true} quand tous les
     * champs sont vides/absents.
     */
    @Test
    void isEmpty_returnsTrueForFullyEmpty() {
        AnimationBindings bindings = new AnimationBindings(Map.of(), null, null);

        assertTrue(bindings.isEmpty());
    }

    /**
     * {@link AnimationBindings#isEmpty()} doit renvoyer {@code false} des qu'au
     * moins un champ est present (motion OU one-shot equip OU one-shot unequip).
     */
    @Test
    void isEmpty_returnsFalseIfAnyField() {
        AnimationBindings onlyMotion =
            new AnimationBindings(Map.of(LivingMotions.IDLE, ANIM_IDLE), null, null);
        AnimationBindings onlyEquip =
            new AnimationBindings(Map.of(), ANIM_EQUIP, null);
        AnimationBindings onlyUnequip =
            new AnimationBindings(Map.of(), null, ANIM_UNEQUIP);

        assertFalse(onlyMotion.isEmpty(), "isEmpty doit etre false si livingMotions non vide");
        assertFalse(onlyEquip.isEmpty(), "isEmpty doit etre false si onEquip non null");
        assertFalse(onlyUnequip.isEmpty(), "isEmpty doit etre false si onUnequip non null");
    }

    /**
     * Le compact ctor doit faire une copie defensive du Map passe — mutation de la
     * source apres construction ne doit PAS affecter le record.
     */
    @Test
    void immutability_livingMotionsCopyIsDefensive() {
        Map<LivingMotion, ResourceLocation> mutable = new HashMap<>();
        mutable.put(LivingMotions.IDLE, ANIM_IDLE);

        AnimationBindings bindings = new AnimationBindings(mutable, null, null);

        // Mutation de la map source apres construction
        mutable.put(LivingMotions.WALK, ANIM_WALK);
        mutable.remove(LivingMotions.IDLE);

        // Le record garde bien l'etat initial
        assertEquals(1, bindings.livingMotions().size(),
            "La map du record ne doit pas refleter les mutations de la source");
        assertEquals(ANIM_IDLE, bindings.livingMotions().get(LivingMotions.IDLE),
            "La map du record doit toujours contenir l'entry initiale");

        // Et la map retournee doit elle-meme etre immutable (Map.copyOf)
        assertThrows(UnsupportedOperationException.class,
            () -> bindings.livingMotions().put(LivingMotions.RUN, ANIM_WALK),
            "livingMotions() doit retourner un Map immutable");
    }

    /** La constante {@link AnimationBindings#EMPTY} est bien vide. */
    @Test
    void empty_constant_isEmpty() {
        assertTrue(AnimationBindings.EMPTY.isEmpty());
        assertTrue(AnimationBindings.EMPTY.livingMotions().isEmpty());
        assertNull(AnimationBindings.EMPTY.onEquip());
        assertNull(AnimationBindings.EMPTY.onUnequip());
    }
}
