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

package com.tiedup.remake.rig.bridge;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.joml.Matrix4f;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.junit.jupiter.api.Test;

import net.minecraft.resources.ResourceLocation;

import com.tiedup.remake.client.gltf.GltfData;
import com.tiedup.remake.client.gltf.GltfData.Primitive;
import com.tiedup.remake.rig.armature.Armature;
import com.tiedup.remake.rig.armature.Joint;
import com.tiedup.remake.rig.asset.AssetAccessor;
import com.tiedup.remake.rig.math.OpenMatrix4f;
import com.tiedup.remake.rig.mesh.SkinnedMesh;

class GltfToSkinnedMeshTest {

	// ---------- Test fixtures ----------

	/**
	 * Armature biped minimale pour test : Root → Chest → {Arm_L, Arm_R}.
	 * 4 joints avec IDs 0..3. Suffisant pour valider le mapping des noms legacy
	 * sans dépendre d'un resource reload.
	 */
	private static AssetAccessor<Armature> buildMinimalArmature() {
		Joint root = new Joint("Root", 0, new OpenMatrix4f());
		Joint chest = new Joint("Chest", 1, new OpenMatrix4f());
		Joint armL = new Joint("Arm_L", 2, new OpenMatrix4f());
		Joint armR = new Joint("Arm_R", 3, new OpenMatrix4f());

		root.addSubJoints(chest);
		chest.addSubJoints(armL, armR);

		Map<String, Joint> jointMap = new LinkedHashMap<>();
		jointMap.put("Root", root);
		jointMap.put("Chest", chest);
		jointMap.put("Arm_L", armL);
		jointMap.put("Arm_R", armR);

		Armature arm = new Armature("test_biped", 4, root, jointMap);
		arm.bakeOriginMatrices();

		return new AssetAccessor<>() {
			@Override public Armature get() { return arm; }
			@Override public ResourceLocation registryName() {
				return ResourceLocation.fromNamespaceAndPath("test", "armature/test_biped");
			}
			@Override public boolean inRegistry() { return false; }
		};
	}

	/**
	 * GltfData synthétique : 3 vertices (triangle), 3 joints glTF
	 * (body, leftUpperArm, rightUpperArm). Chaque vertex attaché à un joint
	 * différent avec poids 1.0 sur le joint principal + 0 sur les 3 autres.
	 * Permet de vérifier le mapping glTF→biped via le jointIdMap.
	 */
	private static GltfData buildSyntheticGltf() {
		int vertexCount = 3;

		float[] positions = {
			0.0F, 0.0F, 0.0F,    // v0 - attached to body/Chest
			1.0F, 0.0F, 0.0F,    // v1 - attached to leftUpperArm/Arm_L
			-1.0F, 0.0F, 0.0F    // v2 - attached to rightUpperArm/Arm_R
		};
		float[] normals = {
			0.0F, 1.0F, 0.0F,
			0.0F, 1.0F, 0.0F,
			0.0F, 1.0F, 0.0F
		};
		float[] texCoords = {
			0.5F, 0.5F,
			1.0F, 0.5F,
			0.0F, 0.5F
		};
		int[] indices = { 0, 1, 2 };

		// 4 joints/weights par vertex. Le 4e est toujours 0 → sera drop.
		int[] joints = {
			0, 1, 2, 0,    // v0
			1, 0, 2, 0,    // v1
			2, 0, 1, 0     // v2
		};
		float[] weights = {
			1.0F, 0.0F, 0.0F, 0.0F,    // v0 : 100% body
			1.0F, 0.0F, 0.0F, 0.0F,    // v1 : 100% leftUpperArm
			1.0F, 0.0F, 0.0F, 0.0F     // v2 : 100% rightUpperArm
		};

		String[] jointNames = { "body", "leftUpperArm", "rightUpperArm" };
		int[] parentJoints = { -1, 0, 0 };
		Matrix4f[] invBind = {
			new Matrix4f().identity(),
			new Matrix4f().identity(),
			new Matrix4f().identity()
		};
		Quaternionf[] restRot = {
			new Quaternionf(),
			new Quaternionf(),
			new Quaternionf()
		};
		Vector3f[] restTrans = {
			new Vector3f(),
			new Vector3f(),
			new Vector3f()
		};

		return new GltfData(
			positions, normals, texCoords, indices,
			joints, weights,
			jointNames, parentJoints, invBind, restRot, restTrans,
			restRot,                                    // rawGltfRestRotations
			null,                                        // rawGltfAnimation
			null,                                        // animation
			new LinkedHashMap<>(),                       // namedAnimations
			new LinkedHashMap<>(),                       // rawNamedAnimations
			List.of(new Primitive(indices, "material_0", false, null)),
			vertexCount, jointNames.length
		);
	}

	// ---------- Tests ----------

	@Test
	void convertSyntheticGltfDoesNotThrow() {
		GltfData data = buildSyntheticGltf();
		AssetAccessor<Armature> armature = buildMinimalArmature();

		SkinnedMesh mesh = assertDoesNotThrow(() -> GltfToSkinnedMesh.convert(data, armature));
		assertNotNull(mesh);
	}

	@Test
	void convertSyntheticGltfHasExpectedParts() {
		GltfData data = buildSyntheticGltf();
		AssetAccessor<Armature> armature = buildMinimalArmature();

		SkinnedMesh mesh = GltfToSkinnedMesh.convert(data, armature);

		// Une seule primitive → une seule part (partName = "material_0")
		assertNotNull(mesh);
		assertEquals(1, mesh.getAllParts().size(), "Expected one part per primitive");
		assertTrue(mesh.hasPart("material_0"), "Part name should match material name");
	}

	@Test
	void convertThrowsOnNullArmature() {
		GltfData data = buildSyntheticGltf();
		assertThrows(IllegalStateException.class, () -> GltfToSkinnedMesh.convert(data, null));
	}

	/**
	 * Vertex influencé par 4 joints distincts avec poids non triviaux.
	 * Le plus faible (0.05) doit être drop, les 3 autres renormalisés
	 * pour sommer à 1.0.
	 */
	@Test
	void convertDropsLowestWeightAndRenormalizes() {
		int vc = 1;
		float[] positions = { 0F, 0F, 0F };
		float[] normals = { 0F, 1F, 0F };
		float[] texCoords = { 0.5F, 0.5F };
		int[] indices = { 0 };

		// Vertex avec poids 4→3 : body=0.5, leftUpperArm=0.3, rightUpperArm=0.15, [drop]=0.05
		// Après drop + renorm : (0.5+0.3+0.15) = 0.95 → 0.526/0.316/0.158
		int[] joints = { 0, 1, 2, 0 };
		float[] weights = { 0.5F, 0.3F, 0.15F, 0.05F };

		GltfData data = new GltfData(
			positions, normals, texCoords, indices,
			joints, weights,
			new String[] { "body", "leftUpperArm", "rightUpperArm" },
			new int[] { -1, 0, 0 },
			new Matrix4f[] { new Matrix4f().identity(), new Matrix4f().identity(), new Matrix4f().identity() },
			new Quaternionf[] { new Quaternionf(), new Quaternionf(), new Quaternionf() },
			new Vector3f[] { new Vector3f(), new Vector3f(), new Vector3f() },
			new Quaternionf[] { new Quaternionf(), new Quaternionf(), new Quaternionf() },
			null, null,
			new LinkedHashMap<>(), new LinkedHashMap<>(),
			List.of(new Primitive(indices, "m", false, null)),
			vc, 3
		);

		SkinnedMesh mesh = assertDoesNotThrow(() ->
			GltfToSkinnedMesh.convert(data, buildMinimalArmature())
		);
		assertNotNull(mesh);
		// Si on arrive ici sans NaN ni exception, le drop+renorm a marché.
		// La validation des valeurs exactes passerait par accès aux VertexBuilder
		// internals (non exposés) — suffit de vérifier non-crash.
	}

	/**
	 * Vertex avec tous les poids à zéro (GLB bugué ou non-skinné).
	 * Le fallback doit attacher au Root avec poids 1.0 sans crasher.
	 */
	@Test
	void convertHandlesZeroWeightVertex() {
		int vc = 1;
		GltfData data = new GltfData(
			new float[] { 0F, 0F, 0F },
			new float[] { 0F, 1F, 0F },
			new float[] { 0.5F, 0.5F },
			new int[] { 0 },
			new int[] { 0, 1, 2, 0 },
			new float[] { 0F, 0F, 0F, 0F },   // tous zéro
			new String[] { "body", "leftUpperArm", "rightUpperArm" },
			new int[] { -1, 0, 0 },
			new Matrix4f[] { new Matrix4f().identity(), new Matrix4f().identity(), new Matrix4f().identity() },
			new Quaternionf[] { new Quaternionf(), new Quaternionf(), new Quaternionf() },
			new Vector3f[] { new Vector3f(), new Vector3f(), new Vector3f() },
			new Quaternionf[] { new Quaternionf(), new Quaternionf(), new Quaternionf() },
			null, null,
			new LinkedHashMap<>(), new LinkedHashMap<>(),
			List.of(new Primitive(new int[] { 0 }, "m", false, null)),
			vc, 3
		);

		SkinnedMesh mesh = assertDoesNotThrow(() ->
			GltfToSkinnedMesh.convert(data, buildMinimalArmature())
		);
		assertNotNull(mesh);
	}

	/**
	 * Joint GLB avec nom inconnu (ni alias ni biped natif).
	 * Doit logger WARN et fallback sur Root (id 0), sans crash.
	 */
	@Test
	void convertFallsBackToRootForUnknownJointName() {
		int vc = 1;
		GltfData data = new GltfData(
			new float[] { 0F, 0F, 0F },
			new float[] { 0F, 1F, 0F },
			new float[] { 0.5F, 0.5F },
			new int[] { 0 },
			new int[] { 0, 0, 0, 0 },
			new float[] { 1F, 0F, 0F, 0F },
			new String[] { "TentacleJoint42" },         // nom inconnu
			new int[] { -1 },
			new Matrix4f[] { new Matrix4f().identity() },
			new Quaternionf[] { new Quaternionf() },
			new Vector3f[] { new Vector3f() },
			new Quaternionf[] { new Quaternionf() },
			null, null,
			new LinkedHashMap<>(), new LinkedHashMap<>(),
			List.of(new Primitive(new int[] { 0 }, "m", false, null)),
			vc, 1
		);

		SkinnedMesh mesh = assertDoesNotThrow(() ->
			GltfToSkinnedMesh.convert(data, buildMinimalArmature())
		);
		assertNotNull(mesh);
		// Le log WARN "unknown joint 'TentacleJoint42' — fallback to Root" doit apparaître ;
		// cf. TiedUpRigConstants.LOGGER. Non assertable en test sans mock logger.
	}
}
