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

package com.tiedup.remake.rig;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;

import net.minecraft.resources.ResourceLocation;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.tiedup.remake.rig.anim.AnimationManager;
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
import com.tiedup.remake.rig.anim.types.StaticAnimation;

/**
 * Tests de {@link TiedUpAnimationRegistry#resolveWithFallback(ResourceLocation)}
 * — helper consommé par {@code ClientRigEquipmentHandler.rebuildBondageAnimations}
 * (P3-05) et {@code PacketPlayRigAnim.handleOnClient} (P3-12) pour résoudre
 * un anim ID avec fallback safe si miss.
 *
 * <p>Ces tests verrouillent trois invariants critiques :</p>
 * <ul>
 *   <li><b>Happy path</b> — un ID enregistré via {@link AnimationManager.AnimationBuilder}
 *       est résolu directement (pas de fallback).</li>
 *   <li><b>Fallback safe</b> — un ID inconnu retourne
 *       {@link TiedUpRigRegistry#EMPTY_ANIMATION} (pas null, pas de throw).
 *       Garantit la robustesse du pipeline rendu client (pas de crash sur
 *       typo modder JSON).</li>
 *   <li><b>Dedup WARN</b> — deux calls consécutifs avec le même ID invalide
 *       n'ajoutent qu'une entrée au set interne. Le set s'exprime via
 *       {@link TiedUpAnimationRegistry#resetWarnedMissing()} qui doit être
 *       appelable sans throw (idempotence).</li>
 * </ul>
 *
 * <p><b>Isolation de test</b> : chaque test commence par
 * {@link TiedUpAnimationRegistry#resetWarnedMissing()} pour ne pas hériter du
 * set dedup d'un test précédent (le set est statique, partagé entre tests).
 * L'{@link AfterEach} reset aussi, au cas où un test ferait son propre setup
 * conditionnel.</p>
 *
 * <p><b>Pas de MC bootstrap</b> : {@link AnimationManager.AnimationBuilder}
 * peut instancier un accessor sans toucher au ResourceManager (le
 * {@code onLoad} lambda n'est invoqué qu'à {@code .get()}, et on ne call pas
 * get() dans ces tests). Donc pas besoin de {@code Bootstrap.bootStrap()}
 * ou autre init MC.</p>
 */
class TiedUpAnimationRegistryResolveTest {

    /** Namespace arbitraire pour les IDs de test — isolé des vraies registrations. */
    private static final String TEST_NS = "tiedup_test_resolve";

    @BeforeEach
    void resetDedupSet() {
        TiedUpAnimationRegistry.resetWarnedMissing();
    }

    @AfterEach
    void cleanUp() {
        TiedUpAnimationRegistry.resetWarnedMissing();
    }

    /**
     * ID enregistré via {@link AnimationManager.AnimationBuilder#nextAccessor}
     * doit être résolu directement (pas de fallback).
     *
     * <p>Pattern : injecte un accessor fake dans {@code AnimationManager}
     * via le builder public, puis vérifie que
     * {@link TiedUpAnimationRegistry#resolveWithFallback} retourne exactement
     * la même instance via {@code assertSame}. Si {@code resolveWithFallback}
     * ratait le happy path (ex. bug qui retournerait EMPTY_ANIMATION sur tous
     * les IDs), {@code assertSame} détecterait immédiatement la régression.</p>
     */
    @Test
    void resolveWithFallback_existingId_returnsRegisteredAnim() {
        String idPath = "registered_anim_" + System.nanoTime();
        ResourceLocation id = ResourceLocation.fromNamespaceAndPath(TEST_NS, idPath);

        // Injecte un accessor fake via le builder public. onLoad retourne null
        // mais ce n'est pas invoqué tant qu'on ne call pas .get() dessus.
        AnimationAccessor<StaticAnimation> registered =
            new AnimationManager.AnimationBuilder(TEST_NS, b -> {})
                .nextAccessor(idPath, acc -> null);

        AnimationAccessor<? extends StaticAnimation> resolved =
            TiedUpAnimationRegistry.resolveWithFallback(id);

        assertSame(registered, resolved,
            "Un ID enregistré dans AnimationManager doit être retourné tel quel "
            + "(pas de fallback sur EMPTY_ANIMATION). Régression si "
            + "resolveWithFallback contourne AnimationManager.byKey.");
    }

    /**
     * ID inconnu (jamais enregistré) doit retourner
     * {@link TiedUpRigRegistry#EMPTY_ANIMATION} — le singleton canonique
     * utilisé partout comme fallback.
     *
     * <p>Important de retourner LE singleton (pas une autre instance d'empty) :
     * plusieurs sites du runtime ({@code Layer#off}, {@code AnimationPlayer#isEmpty},
     * {@code LayerOffAnimation#getNextAnimation}) testent l'identité via
     * {@code == EMPTY_ANIMATION}. Si on retournait une instance différente,
     * ces checks donneraient des false-negatives.</p>
     */
    @Test
    void resolveWithFallback_unknownId_returnsEmptyFallback() {
        ResourceLocation unknown = ResourceLocation.fromNamespaceAndPath(
            TEST_NS, "definitely_not_registered_" + System.nanoTime()
        );

        AnimationAccessor<? extends StaticAnimation> resolved =
            TiedUpAnimationRegistry.resolveWithFallback(unknown);

        assertNotNull(resolved, "Fallback ne doit jamais être null — "
            + "les callers (packet handler, tick) supposent non-null.");
        assertSame(TiedUpRigRegistry.EMPTY_ANIMATION, resolved,
            "Fallback doit être EMPTY_ANIMATION (singleton canonique), pas une "
            + "autre instance d'empty. Checks d'identité == EMPTY_ANIMATION "
            + "dans Layer/AnimationPlayer/LayerOffAnimation dépendent de ça.");
    }

    /**
     * {@link TiedUpAnimationRegistry#resolveWithFallback} ne doit jamais throw
     * même sur input pathologique (ID inconnu, ID null).
     *
     * <p>Garantit la robustesse du pipeline : un bug dans un JSON data-driven
     * ou un packet cinematic mal formé ne doit pas crasher le client. Un
     * fallback silencieux + WARN log est préférable à un {@code NullPointerException}
     * qui remonterait jusqu'à l'event bus et casserait tout le rendu.</p>
     */
    @Test
    void resolveWithFallback_unknownId_doesNotThrow() {
        ResourceLocation unknown = ResourceLocation.fromNamespaceAndPath(
            TEST_NS, "throw_test_" + System.nanoTime()
        );

        assertDoesNotThrow(() -> {
            TiedUpAnimationRegistry.resolveWithFallback(unknown);
        }, "Fallback safe doit swallow le miss — throw = régression robustesse.");

        // Input null : couvre la branche défensive en tête de resolveWithFallback.
        assertDoesNotThrow(() -> {
            TiedUpAnimationRegistry.resolveWithFallback(null);
        }, "null ID doit être swallow aussi (caller pathologique).");
    }

    /**
     * Deux calls consécutifs avec le même ID invalide doivent produire un seul
     * WARN log (dedup via {@code WARNED_MISSING_ANIMS}).
     *
     * <p>Comme on ne peut pas facilement intercepter les logs SLF4J sans
     * dépendance supplémentaire (logback test appender, etc.), on teste le
     * comportement observable du set dedup :</p>
     * <ul>
     *   <li>Avant le 1er call : set vide → {@code resetWarnedMissing()}
     *       ne throw pas (pas de précondition implicite).</li>
     *   <li>Après le 1er call sur ID inconnu : le set contient l'ID
     *       (vérifiable indirectement — un reset suivi d'un 2e call sur le
     *       même ID doit re-warn, donc comportement observable).</li>
     *   <li>Après le 2e call sur même ID : set inchangé (déjà présent).</li>
     * </ul>
     *
     * <p>Pattern équivalent à ce qu'on fait pour {@code LOGGED_ERRORS} dans
     * {@code RigAnimationTickHandlerTest} — on teste l'idempotence du reset
     * qui n'a de sens que si le set existe et est mutable.</p>
     */
    @Test
    void resolveWithFallback_warnsOnlyOncePerMissingId() {
        ResourceLocation unknown = ResourceLocation.fromNamespaceAndPath(
            TEST_NS, "dedup_test_" + System.nanoTime()
        );

        // Premier call : fallback + add(id) → true (log émis).
        AnimationAccessor<? extends StaticAnimation> first =
            TiedUpAnimationRegistry.resolveWithFallback(unknown);
        assertSame(TiedUpRigRegistry.EMPTY_ANIMATION, first,
            "1er call sur ID inconnu doit fallback.");

        // Deuxième call sur le MÊME ID : fallback + add(id) → false (pas de log).
        // On ne peut pas vérifier "pas de log" directement sans log capture,
        // mais on vérifie que le résultat est identique (même instance EMPTY)
        // et que pas de throw — ce qui couvre le chemin add→false du Set.
        AnimationAccessor<? extends StaticAnimation> second =
            TiedUpAnimationRegistry.resolveWithFallback(unknown);
        assertSame(TiedUpRigRegistry.EMPTY_ANIMATION, second,
            "2e call sur même ID doit retourner la même instance EMPTY_ANIMATION.");

        // Comportement observable du dedup : après un reset, un 3e call sur
        // le même ID doit re-traiter comme "première fois" (Set.add retourne
        // à nouveau true). C'est l'invariant que reset rend le state "fresh".
        TiedUpAnimationRegistry.resetWarnedMissing();
        AnimationAccessor<? extends StaticAnimation> third =
            TiedUpAnimationRegistry.resolveWithFallback(unknown);
        assertSame(TiedUpRigRegistry.EMPTY_ANIMATION, third,
            "3e call après reset doit toujours fallback sur EMPTY_ANIMATION.");

        // Sanity check : l'accessor fallback EMPTY_ANIMATION n'est pas le
        // même objet qu'un fake accessor registered via AnimationBuilder —
        // garantit qu'on ne confond pas les deux chemins.
        AnimationAccessor<StaticAnimation> fakeRegistered =
            new AnimationManager.AnimationBuilder(TEST_NS, b -> {})
                .nextAccessor("sanity_check_" + System.nanoTime(), acc -> null);
        assertNotSame(TiedUpRigRegistry.EMPTY_ANIMATION, fakeRegistered,
            "Un accessor registered via AnimationBuilder doit être distinct "
            + "de EMPTY_ANIMATION (sinon le test happy path ne teste rien).");
    }
}
