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

package com.tiedup.remake.rig.network;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.function.Supplier;

import io.netty.buffer.Unpooled;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.network.NetworkDirection;
import net.minecraftforge.network.NetworkEvent;

import org.junit.jupiter.api.Test;

import com.tiedup.remake.rig.anim.client.Layer;

/**
 * Tests de {@link PacketPlayRigAnim} : encode/decode roundtrip + priority
 * fallback. Aucune dépendance MC bootstrap — on instancie juste
 * {@code FriendlyByteBuf(Unpooled.buffer())} et on vérifie que les bytes
 * écrits roundtrip correctement.
 */
class PacketPlayRigAnimTest {

    private static FriendlyByteBuf newBuf() {
        return new FriendlyByteBuf(Unpooled.buffer());
    }

    /** Roundtrip nominal : packet construit avec valeurs usuelles. */
    @Test
    void encode_decode_roundtrip_nominal() {
        ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "context_stand_idle");
        PacketPlayRigAnim original = PacketPlayRigAnim.of(42, anim, 0.15F, Layer.Priority.MIDDLE);

        FriendlyByteBuf buf = newBuf();
        original.encode(buf);
        PacketPlayRigAnim decoded = PacketPlayRigAnim.decode(buf);

        assertEquals(original.entityId(), decoded.entityId(), "entityId roundtrip");
        assertEquals(original.animId(), decoded.animId(), "animId roundtrip");
        assertEquals(original.transitionTime(), decoded.transitionTime(), 0.0F, "transitionTime roundtrip");
        assertEquals(original.priorityOrdinal(), decoded.priorityOrdinal(), "priorityOrdinal roundtrip");
        assertEquals(original, decoded, "record equals()");
        assertEquals(0, buf.readableBytes(), "buffer must be fully consumed");
    }

    /** Boundary entity IDs : 0, Integer.MAX_VALUE, negatif. */
    @Test
    void encode_decode_entityId_boundary() {
        ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "test");

        for (int entityId : new int[]{0, 1, Integer.MAX_VALUE, -1, Integer.MIN_VALUE}) {
            PacketPlayRigAnim original = PacketPlayRigAnim.of(entityId, anim, 0.1F, Layer.Priority.LOW);
            FriendlyByteBuf buf = newBuf();
            original.encode(buf);
            PacketPlayRigAnim decoded = PacketPlayRigAnim.decode(buf);

            assertEquals(entityId, decoded.entityId(),
                "entityId " + entityId + " must roundtrip via writeVarInt/readVarInt");
        }
    }

    /**
     * transitionTime=0.0F roundtrip (no blend = cut) + negative (-1.0F) roundtrip.
     * Le packet ne doit PAS valider silencieusement — roundtrip brut.
     */
    @Test
    void encode_decode_transitionTime_zero_and_negative() {
        ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "test");

        for (float t : new float[]{0.0F, -1.0F, 99.9F, Float.MIN_VALUE, Float.MAX_VALUE}) {
            PacketPlayRigAnim original = PacketPlayRigAnim.of(7, anim, t, Layer.Priority.LOWEST);
            FriendlyByteBuf buf = newBuf();
            original.encode(buf);
            PacketPlayRigAnim decoded = PacketPlayRigAnim.decode(buf);

            assertEquals(t, decoded.transitionTime(), 0.0F,
                "transitionTime " + t + " must roundtrip verbatim (no silent validation)");
        }
    }

    /** Loop sur les 5 Layer.Priority : of() + roundtrip, priority() retourne pareil. */
    @Test
    void encode_decode_allPriorities() {
        ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "test");

        for (Layer.Priority p : Layer.Priority.values()) {
            PacketPlayRigAnim original = PacketPlayRigAnim.of(1, anim, 0.15F, p);
            FriendlyByteBuf buf = newBuf();
            original.encode(buf);
            PacketPlayRigAnim decoded = PacketPlayRigAnim.decode(buf);

            assertEquals(p, decoded.priority(),
                "priority " + p + " must roundtrip and priority() must return same enum");
            assertEquals((byte) p.ordinal(), decoded.priorityOrdinal(),
                "priorityOrdinal must match enum.ordinal()");
        }
    }

    /**
     * priorityOrdinal hors-range (99) : priority() doit retourner LOWEST (fallback safe),
     * PAS throw. Simule un peer malveillant ou un mismatch de protocole.
     */
    @Test
    void priority_outOfRange_returnsLowestFallback() {
        ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "test");

        // Construit directement avec ordinal invalide (bypass of())
        PacketPlayRigAnim malformed = new PacketPlayRigAnim(1, anim, 0.1F, (byte) 99);
        assertEquals(Layer.Priority.LOWEST, malformed.priority(),
            "priorityOrdinal=99 doit fallback sur LOWEST (pas de throw)");

        // Idem pour ordinal négatif signé (qui devient 128+ via & 0xFF)
        PacketPlayRigAnim negative = new PacketPlayRigAnim(1, anim, 0.1F, (byte) -1);
        assertEquals(Layer.Priority.LOWEST, negative.priority(),
            "priorityOrdinal=-1 (byte) doit fallback sur LOWEST (& 0xFF → 255, hors range)");

        // Sanity : ordinal exactement PRIORITY_COUNT (5) est hors-range
        PacketPlayRigAnim boundary = new PacketPlayRigAnim(1, anim, 0.1F, (byte) PacketPlayRigAnim.PRIORITY_COUNT);
        assertEquals(Layer.Priority.LOWEST, boundary.priority(),
            "priorityOrdinal=PRIORITY_COUNT doit fallback sur LOWEST");
    }

    /** animId avec namespace + path contenant underscores et slashes. */
    @Test
    void encode_decode_animId_namespaceSpecialChars() {
        ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("my_mod", "deep/path/with_underscores");
        PacketPlayRigAnim original = PacketPlayRigAnim.of(123, anim, 0.2F, Layer.Priority.HIGH);

        FriendlyByteBuf buf = newBuf();
        original.encode(buf);
        PacketPlayRigAnim decoded = PacketPlayRigAnim.decode(buf);

        assertEquals(anim, decoded.animId(), "ResourceLocation complexe doit roundtrip verbatim");
        assertEquals("my_mod", decoded.animId().getNamespace());
        assertEquals("deep/path/with_underscores", decoded.animId().getPath());
    }

    /**
     * Direction guard (RISK-001) : handleOnClient doit rejeter les packets
     * reçus avec direction PLAY_TO_SERVER (spoofing client malveillant).
     * On mock NetworkEvent.Context + son Supplier, on stub getDirection() et
     * on vérifie que enqueueWork() n'est PAS appelé + setPacketHandled(true)
     * est appelé.
     *
     * <p>Note : on n'exécute pas la branche PLAY_TO_CLIENT ici car enqueueWork
     * invoque LogicalSidedProvider.WORKQUEUE qui NPE sans bootstrap MC —
     * testable uniquement en runClient/gameday.
     */
    @Test
    void handleOnClient_rejectsServerBoundDirection() {
        ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "test");
        PacketPlayRigAnim pkt = PacketPlayRigAnim.of(42, anim, 0.15F, Layer.Priority.MIDDLE);

        NetworkEvent.Context context = mock(NetworkEvent.Context.class);
        when(context.getDirection()).thenReturn(NetworkDirection.PLAY_TO_SERVER);

        @SuppressWarnings("unchecked")
        Supplier<NetworkEvent.Context> ctxSupplier = mock(Supplier.class);
        when(ctxSupplier.get()).thenReturn(context);

        PacketPlayRigAnim.handleOnClient(pkt, ctxSupplier);

        // Guard doit short-circuit : setPacketHandled(true) appelé, enqueueWork jamais.
        verify(context, times(1)).setPacketHandled(true);
        verify(context, never()).enqueueWork(org.mockito.ArgumentMatchers.any());
    }

    /** Sanity check : deux packets distincts ne sont pas equal. */
    @Test
    void records_notEqual_whenDifferent() {
        ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "a");
        PacketPlayRigAnim a = PacketPlayRigAnim.of(1, anim, 0.15F, Layer.Priority.LOWEST);
        PacketPlayRigAnim b = PacketPlayRigAnim.of(2, anim, 0.15F, Layer.Priority.LOWEST);
        assertNotEquals(a, b, "packets avec entityId différents ne doivent pas être equal");
    }
}
