Effigy.Tests/VmdlDocumentTests.cs
using System;
using Effigy;
using static Effigy.Tests.Report;

namespace Effigy.Tests;

/// <summary>
/// The one thing this file exists to hold: an OBJ-backed .vmdl carries the import_rotation
/// correction and a DMX-backed one does not.
///
/// THIS WAS A REAL MODEL THAT COMPILED LYING ON ITS SIDE. The correction was measured once and
/// written into EffigyWindow.BuildVmdl, and then the same template was copied into
/// BuildSkinnedVmdl, TreeGen and TentacleGen without it — three silent copies, because a literal
/// string cannot tell you which mesh format it is about. Deriving the rotation from the filename
/// is what makes the mistake unrepresentable; this is what keeps it that way.
/// </summary>
public static class VmdlDocumentTests
{
	public static void Run()
	{
		Section( "vmdl document: an OBJ is turned, a DMX is not" );
		TestObjGetsTheCorrection();
		TestDmxAndSmdDoNot();

		Section( "vmdl document: the nodes each kind of model needs" );
		TestStaticHasOneMeshFile();
		TestSkinnedCarriesTheBindPose();
	}

	static void TestObjGetsTheCorrection()
	{
		Check( "an OBJ needs the -90/-90 inverse of ModelDoc's axis permutation",
			VmdlDocument.ImportRotation( "models/effigy/x.obj" ) == "-90.0, -90.0, 0.0" );

		// Case does not decide it. An exporter that wrote .OBJ would otherwise land on its side.
		Check( "the extension test is case insensitive",
			VmdlDocument.ImportRotation( "X.OBJ" ) == "-90.0, -90.0, 0.0" );

		var vmdl = VmdlDocument.Static( "models/effigy/remesh_all.obj" );

		Check( "a static OBJ model writes the corrected rotation",
			vmdl.Contains( "import_rotation = [ -90.0, -90.0, 0.0 ]" ) );
	}

	static void TestDmxAndSmdDoNot()
	{
		// Only the OBJ importer turns the mesh. Applying the correction to a DMX would break the
		// rigged path, which is the export that actually ships.
		Check( "the DMX importer lands the mesh where the file says",
			VmdlDocument.ImportRotation( "models/effigy/x.dmx" ) == "0.0, 0.0, 0.0" );

		Check( "nor does an SMD get turned",
			VmdlDocument.ImportRotation( "models/effigy/x.smd" ) == "0.0, 0.0, 0.0" );

		var skinned = VmdlDocument.Skinned( "models/effigy/x.dmx", new Skeleton() );

		Check( "a skinned DMX model leaves the rotation alone",
			skinned.Contains( "import_rotation = [ 0.0, 0.0, 0.0 ]" ) );
	}

	static void TestStaticHasOneMeshFile()
	{
		var vmdl = VmdlDocument.Static( "models/effigy/box.obj", VmdlPhysics.MeshFromRender() );

		Check( "the document has a root node", vmdl.Contains( "_class = \"RootNode\"" ) );
		Check( "it names the mesh", vmdl.Contains( "filename = \"models/effigy/box.obj\"" ) );

		Check( "exactly one render mesh, not a duplicated node",
			CountOf( vmdl, "_class = \"RenderMeshFile\"" ) == 1 );

		Check( "the physics the caller passed is in it", vmdl.Contains( "PhysicsMeshFromRender" ) );

		// A static model has no skeleton, so no bone markup: those nodes are the rigged path's
		// business and describe bones a static mesh does not have.
		Check( "no bone markup on a static model", !vmdl.Contains( "BoneMarkup" ) );
	}

	static void TestSkinnedCarriesTheBindPose()
	{
		var skinned = VmdlDocument.Skinned( "models/effigy/x.dmx", new Skeleton() );

		Check( "a rigged model carries the bind pose, or morph targets and IK break quietly",
			skinned.Contains( "AnimationList" ) || skinned.Contains( "BindPose" ) );

		// The caller's own AnimationList replaces the bind-pose-only default rather than joining it.
		var withClips = VmdlDocument.Skinned( "models/effigy/x.dmx", new Skeleton(),
			animations: "\t\t\tCLIPS\n" );

		Check( "clips reach the document", withClips.Contains( "CLIPS" ) );
		Check( "and are not written twice", CountOf( withClips, "CLIPS" ) == 1 );
	}

	static int CountOf( string haystack, string needle )
	{
		var n = 0;

		for ( var i = haystack.IndexOf( needle, StringComparison.Ordinal ); i >= 0;
			i = haystack.IndexOf( needle, i + needle.Length, StringComparison.Ordinal ) )
			n++;

		return n;
	}
}