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

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

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;
import com.tiedup.remake.rig.anim.TiedUpLivingMotions;

/**
 * Tests unitaires pour le parsing du bloc {@code "animations"} par
 * {@link DataDrivenItemParser#parseAnimationBindings} (P3-03).
 *
 * <p>Couvre :
 * <ul>
 *   <li>absence &rarr; {@code null},</li>
 *   <li>bloc vide &rarr; {@link AnimationBindings#EMPTY},</li>
 *   <li>motions vanilla EF et custom TiedUp!,</li>
 *   <li>tolerance typos (unknown motion) avec fuzzy-match suggestion,</li>
 *   <li>tolerance ResourceLocation malformee,</li>
 *   <li>tolerance values non-string,</li>
 *   <li>one-shots on_equip / on_unequip,</li>
 *   <li>Levenshtein distance (sanity check).</li>
 * </ul>
 *
 * Pur Java (JsonParser + ResourceLocation hors runtime MC).
 */
class DataDrivenItemParserAnimationsTest {

    private static final ResourceLocation ITEM_ID =
        ResourceLocation.fromNamespaceAndPath("tiedup", "test_item");

    /** Helper : parse un JSON string en JsonObject. */
    private static JsonObject json(String s) {
        return JsonParser.parseString(s).getAsJsonObject();
    }

    // ========== absence / presence de la cle ==========

    @Test
    void parseAnimations_absent_returnsNull() {
        JsonObject root = json("{\"display_name\": \"test\"}");

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(root, ITEM_ID);

        assertNull(result,
            "cle 'animations' absente => null (pas de binding, vanilla behavior)");
    }

    @Test
    void parseAnimations_empty_returnsEmpty() {
        JsonObject root = json("{\"animations\": {}}");

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(root, ITEM_ID);

        assertNotNull(result);
        assertSame(AnimationBindings.EMPTY, result,
            "bloc 'animations' vide => sentinel EMPTY");
        assertTrue(result.isEmpty());
    }

    @Test
    void parseAnimations_notObject_returnsNull() {
        JsonObject root = json("{\"animations\": 42}");

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(root, ITEM_ID);

        assertNull(result, "animations non-objet => null (tolere, logue warn)");
    }

    // ========== living_motions : cas nominaux ==========

    @Test
    void parseAnimations_validLivingMotions_parses() {
        String jsonStr = """
            {
              "animations": {
                "living_motions": {
                  "IDLE": "tiedup:arms_cuffed_idle",
                  "WALK": "tiedup:arms_cuffed_walk",
                  "SNEAK": "tiedup:arms_cuffed_sneak"
                }
              }
            }
            """;

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);

        assertNotNull(result);
        assertEquals(3, result.livingMotions().size());
        assertEquals(
            ResourceLocation.fromNamespaceAndPath("tiedup", "arms_cuffed_idle"),
            result.livingMotions().get(LivingMotions.IDLE)
        );
        assertEquals(
            ResourceLocation.fromNamespaceAndPath("tiedup", "arms_cuffed_walk"),
            result.livingMotions().get(LivingMotions.WALK)
        );
        assertEquals(
            ResourceLocation.fromNamespaceAndPath("tiedup", "arms_cuffed_sneak"),
            result.livingMotions().get(LivingMotions.SNEAK)
        );
        assertNull(result.onEquip());
        assertNull(result.onUnequip());
    }

    @Test
    void parseAnimations_tiedupCustomMotions_parses() {
        String jsonStr = """
            {
              "animations": {
                "living_motions": {
                  "STRUGGLE_BOUND": "tiedup:bondage_struggle",
                  "POSE_DOG": "tiedup:pose_dog_idle",
                  "WALK_BOUND": "tiedup:cuffed_walk"
                }
              }
            }
            """;

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);

        assertNotNull(result);
        assertEquals(3, result.livingMotions().size());
        assertNotNull(result.livingMotions().get(TiedUpLivingMotions.STRUGGLE_BOUND));
        assertNotNull(result.livingMotions().get(TiedUpLivingMotions.POSE_DOG));
        assertNotNull(result.livingMotions().get(TiedUpLivingMotions.WALK_BOUND));
    }

    @Test
    void parseAnimations_mixedVanillaAndCustomMotions_parses() {
        String jsonStr = """
            {
              "animations": {
                "living_motions": {
                  "WALK": "tiedup:arms_cuffed_walk",
                  "STRUGGLE_BOUND": "tiedup:bondage_struggle"
                }
              }
            }
            """;

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);

        assertNotNull(result);
        assertEquals(2, result.livingMotions().size());
        assertNotNull(result.livingMotions().get(LivingMotions.WALK));
        assertNotNull(result.livingMotions().get(TiedUpLivingMotions.STRUGGLE_BOUND));
    }

    // ========== living_motions : tolerance erreurs ==========

    @Test
    void parseAnimations_unknownMotion_logsWarnAndSkips() {
        // "IDEL" (typo) doit etre skip, "WALK" doit passer
        String jsonStr = """
            {
              "animations": {
                "living_motions": {
                  "IDEL": "tiedup:foo",
                  "WALK": "tiedup:arms_cuffed_walk"
                }
              }
            }
            """;

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);

        assertNotNull(result);
        assertEquals(1, result.livingMotions().size(),
            "La motion inconnue est skip, l'autre passe");
        assertNotNull(result.livingMotions().get(LivingMotions.WALK));
    }

    @Test
    void parseAnimations_malformedResourceLocation_skipsEntry() {
        // "noNamespace" n'a pas de ':' => RL invalide (strict parser)
        String jsonStr = """
            {
              "animations": {
                "living_motions": {
                  "WALK": "noNamespace",
                  "IDLE": "tiedup:arms_cuffed_idle"
                }
              }
            }
            """;

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);

        assertNotNull(result);
        assertEquals(1, result.livingMotions().size(),
            "RL malformee => skip, l'autre passe");
        assertNotNull(result.livingMotions().get(LivingMotions.IDLE));
        assertNull(result.livingMotions().get(LivingMotions.WALK));
    }

    @Test
    void parseAnimations_nonStringValue_skipsEntry() {
        String jsonStr = """
            {
              "animations": {
                "living_motions": {
                  "WALK": 42,
                  "IDLE": "tiedup:arms_cuffed_idle"
                }
              }
            }
            """;

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);

        assertNotNull(result);
        assertEquals(1, result.livingMotions().size(),
            "Value non-string => skip");
        assertNotNull(result.livingMotions().get(LivingMotions.IDLE));
    }

    @Test
    void parseAnimations_livingMotionsNotObject_tolerates() {
        // living_motions est un array au lieu d'un objet => tolere (warn + skip bloc)
        String jsonStr = """
            {
              "animations": {
                "living_motions": [1, 2, 3],
                "on_equip": "tiedup:equip_oneshot"
              }
            }
            """;

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);

        assertNotNull(result);
        assertTrue(result.livingMotions().isEmpty(),
            "living_motions non-objet => map vide, pas crash");
        assertEquals(
            ResourceLocation.fromNamespaceAndPath("tiedup", "equip_oneshot"),
            result.onEquip(),
            "on_equip reste traite normalement"
        );
    }

    // ========== on_equip / on_unequip ==========

    @Test
    void parseAnimations_onEquipOnUnequip_parses() {
        String jsonStr = """
            {
              "animations": {
                "on_equip": "tiedup:cuffs_equip_oneshot",
                "on_unequip": "tiedup:cuffs_unequip_oneshot"
              }
            }
            """;

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);

        assertNotNull(result);
        assertTrue(result.livingMotions().isEmpty());
        assertEquals(
            ResourceLocation.fromNamespaceAndPath("tiedup", "cuffs_equip_oneshot"),
            result.onEquip()
        );
        assertEquals(
            ResourceLocation.fromNamespaceAndPath("tiedup", "cuffs_unequip_oneshot"),
            result.onUnequip()
        );
    }

    @Test
    void parseAnimations_onEquipMissing_isNull() {
        String jsonStr = """
            {
              "animations": {
                "living_motions": {
                  "IDLE": "tiedup:arms_cuffed_idle"
                }
              }
            }
            """;

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);

        assertNotNull(result);
        assertNull(result.onEquip(), "on_equip absent => null");
        assertNull(result.onUnequip(), "on_unequip absent => null");
        assertFalse(result.livingMotions().isEmpty());
    }

    @Test
    void parseAnimations_onEquipMalformed_isNull() {
        String jsonStr = """
            {
              "animations": {
                "on_equip": "noNamespace",
                "on_unequip": "tiedup:valid_rl"
              }
            }
            """;

        AnimationBindings result =
            DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);

        assertNotNull(result);
        assertNull(result.onEquip(),
            "on_equip RL malformee => null (skip)");
        assertEquals(
            ResourceLocation.fromNamespaceAndPath("tiedup", "valid_rl"),
            result.onUnequip(),
            "on_unequip valide => parse OK"
        );
    }

    // ========== resolveMotionByName ==========

    @Test
    void resolveMotionByName_vanilla_resolves() {
        LivingMotion m = DataDrivenItemParser.resolveMotionByName("IDLE");
        assertSame(LivingMotions.IDLE, m);
    }

    @Test
    void resolveMotionByName_custom_resolves() {
        LivingMotion m =
            DataDrivenItemParser.resolveMotionByName("STRUGGLE_BOUND");
        assertSame(TiedUpLivingMotions.STRUGGLE_BOUND, m);
    }

    @Test
    void resolveMotionByName_unknown_returnsNull() {
        assertNull(DataDrivenItemParser.resolveMotionByName("IDEL"));
        assertNull(DataDrivenItemParser.resolveMotionByName(""));
        assertNull(DataDrivenItemParser.resolveMotionByName(null));
    }

    @Test
    void resolveMotionByName_caseSensitive() {
        // JSON convention = UPPER_SNAKE_CASE (matches enum constants)
        assertNull(DataDrivenItemParser.resolveMotionByName("idle"),
            "Resolution case-sensitive : 'idle' ne match pas IDLE");
    }

    // ========== suggestClosestMotion ==========

    @Test
    void suggestClosestMotion_oneEdit_suggestsMatch() {
        // "IDEL" est a 1 edit de "IDLE" (swap E<->L)
        String s = DataDrivenItemParser.suggestClosestMotion("IDEL");
        assertEquals("IDLE", s);
    }

    @Test
    void suggestClosestMotion_completelyUnrelated_returnsNull() {
        // "XYZQWERTY" n'est a moins de 3 edits d'aucune motion
        String s = DataDrivenItemParser.suggestClosestMotion("XYZQWERTY");
        assertNull(s);
    }

    @Test
    void suggestClosestMotion_caseInsensitive() {
        // "idel" en minuscule doit quand meme suggerer "IDLE"
        String s = DataDrivenItemParser.suggestClosestMotion("idel");
        assertEquals("IDLE", s);
    }

    @Test
    void suggestClosestMotion_customMotion_suggestsCustom() {
        // "STRUGLE_BOUND" (typo) devrait suggerer "STRUGGLE_BOUND"
        String s = DataDrivenItemParser.suggestClosestMotion("STRUGLE_BOUND");
        assertEquals("STRUGGLE_BOUND", s);
    }

    // ========== levenshtein (sanity) ==========

    @Test
    void levenshtein_sameString_returnsZero() {
        assertEquals(0, DataDrivenItemParser.levenshtein("IDLE", "IDLE"));
    }

    @Test
    void levenshtein_oneEdit_returnsOne() {
        // substitution
        assertEquals(1, DataDrivenItemParser.levenshtein("IDLE", "IDLA"));
        // insertion
        assertEquals(1, DataDrivenItemParser.levenshtein("IDLE", "IDLEE"));
        // deletion
        assertEquals(1, DataDrivenItemParser.levenshtein("IDLE", "IDL"));
    }

    @Test
    void levenshtein_emptyStrings_handled() {
        assertEquals(0, DataDrivenItemParser.levenshtein("", ""));
        assertEquals(4, DataDrivenItemParser.levenshtein("IDLE", ""));
        assertEquals(4, DataDrivenItemParser.levenshtein("", "IDLE"));
    }

    @Test
    void levenshtein_swap_returnsTwo() {
        // "IDEL" -> "IDLE" : 2 edits (swap E/L = substitution + substitution)
        assertEquals(2, DataDrivenItemParser.levenshtein("IDEL", "IDLE"));
    }
}
