309 results

global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	[AssemblyInitialize]
	public static void ClassInitialize( TestContext context )
	{
		Sandbox.Application.InitUnitTest();
	}
}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
using Braxnet;
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			// var go = new GameObject();
			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
			Assert.IsTrue( scene.Directory.FindByName( "LibraryTestComponent" ) != null );
		}
	}

}

[Autoload]
public class LibraryTestComponent : Component
{
	
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	[AssemblyInitialize]
	public static void ClassInitialize( TestContext context )
	{
		Sandbox.Application.InitUnitTest();
	}
}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
using Hexagon.V2.Application;

namespace Hexagon.V2.Tests.Application;

[TestClass]
public sealed class CharacterNameSkeletonTests
{
	[TestMethod]
	public void WithinScriptLookAlikesCollapseOntoTheNameTheyImitate()
	{
		var alice = CharacterNameSkeleton.Of( "Alice" );
		// Digit-for-letter and the i/l/1 shape collision.
		Assert.AreEqual( alice, CharacterNameSkeleton.Of( "A1ice" ) );
		Assert.AreEqual( alice, CharacterNameSkeleton.Of( "Alice" ) );
		Assert.AreEqual( alice, CharacterNameSkeleton.Of( "AIice" ) );
		Assert.AreEqual( alice, CharacterNameSkeleton.Of( "4lice" ) );
		// Separators and punctuation carry no identity.
		Assert.AreEqual( alice, CharacterNameSkeleton.Of( "A l i c e" ) );
		Assert.AreEqual( alice, CharacterNameSkeleton.Of( "A-l-i-c-e" ) );
		// Accents are decoration, not identity.
		Assert.AreEqual( CharacterNameSkeleton.Of( "Jose" ), CharacterNameSkeleton.Of( "Jos\u00E9" ) );
		// The classic digraph pair.
		Assert.AreEqual( CharacterNameSkeleton.Of( "Bemard" ), CharacterNameSkeleton.Of( "Bernard" ) );
		Assert.AreEqual( CharacterNameSkeleton.Of( "Walker" ), CharacterNameSkeleton.Of( "VValker" ) );
	}

	[TestMethod]
	public void WholeNameWrittenInAnotherScriptCollapsesOntoTheLatinNameItImitates()
	{
		// The script profile permits these - they are single-script names. Only the skeleton
		// sees that they are drawn to read as "Alice" and "Petro".
		// Cyrillic es-o-o-er-ie-ghe, which draws "Cooper" without one Latin letter in it.
		Assert.AreEqual(
			CharacterNameSkeleton.Of( "Cooper" ),
			CharacterNameSkeleton.Of( "\u0441\u043E\u043E\u0440\u0435\u0433" ) );
		// Greek rho-omicron-rho-omicron-nu, which draws "Popov".
		Assert.AreEqual(
			CharacterNameSkeleton.Of( "Popov" ),
			CharacterNameSkeleton.Of( "\u03C1\u03BF\u03C1\u03BF\u03BD" ) );
	}

	[TestMethod]
	public void FoldingFollowsUnicodeRatherThanLocalIntuition()
	{
		// A documented residual, asserted so a table bump surfaces it rather than hiding it:
		// UTS #39 judges Cyrillic em confusable with a turned w, NOT with Latin m, so a Cyrillic
		// name using it does not collapse onto the Latin name it arguably resembles. The script
		// profile still blocks the mixed-script form, which is the reachable attack.
		Assert.AreNotEqual(
			CharacterNameSkeleton.Of( "Maxwell" ),
			CharacterNameSkeleton.Of( "\u043C\u0430\u0445\u051D\u0435\u0456\u0456" ) );

		// Latin i and l stay distinct - the standard folds the digit 1, capital I and the bar
		// onto l, but never merges lowercase i with it.
		Assert.AreEqual( CharacterNameSkeleton.Of( "Alice" ), CharacterNameSkeleton.Of( "A1ice" ) );
		Assert.AreNotEqual( CharacterNameSkeleton.Of( "Bili" ), CharacterNameSkeleton.Of( "Bill" ) );
	}

	[TestMethod]
	public void DistinctNamesKeepDistinctSkeletons()
	{
		Assert.AreNotEqual( CharacterNameSkeleton.Of( "Alice" ), CharacterNameSkeleton.Of( "Alicia" ) );
		Assert.AreNotEqual( CharacterNameSkeleton.Of( "Alice" ), CharacterNameSkeleton.Of( "Alison" ) );
		Assert.AreNotEqual( CharacterNameSkeleton.Of( "Barney" ), CharacterNameSkeleton.Of( "Gordon" ) );
		Assert.AreNotEqual( CharacterNameSkeleton.Of( "Judith Mossman" ), CharacterNameSkeleton.Of( "Eli Vance" ) );
		// A name in a script with no Latin look-alikes is left alone rather than flattened.
		Assert.AreNotEqual(
			CharacterNameSkeleton.Of( "\u0410\u043B\u0438\u0441\u0430" ),      // Cyrillic "Alisa"
			CharacterNameSkeleton.Of( "\u0411\u043E\u0440\u0438\u0441" ) );    // Cyrillic "Boris"
	}

	[TestMethod]
	public void NameWithNoIdentityBearingCharactersHasAnEmptySkeleton()
	{
		Assert.AreEqual( string.Empty, CharacterNameSkeleton.Of( "..." ) );
		Assert.AreEqual( string.Empty, CharacterNameSkeleton.Of( "---" ) );
		Assert.AreEqual( string.Empty, CharacterNameSkeleton.Of( string.Empty ) );
	}
}
using System.Text.Json;
using Hexagon.V2.Application;
using Hexagon.V2.Domain;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Hexagon.V2.Tests.Application;

[TestClass]
public sealed class CurrencyAndCharacterRulesTests
{
	[TestMethod]
	public void CurrencyRejectsUnderflowAndOverflow()
	{
		var character = CreateCharacter( 10 );
		Assert.IsTrue( CurrencyService.Debit( character, 11 ).Failed );
		Assert.IsTrue( CurrencyService.Credit( character with { Balance = long.MaxValue }, 1 ).Failed );
	}

	[TestMethod]
	public void SlotAllocatorUsesLowestGap()
	{
		var characters = new[] { CreateCharacter( 0 ) with { Slot = 0 }, CreateCharacter( 0 ) with { Slot = 2 } };
		Assert.AreEqual( 1, CharacterRules.FindLowestFreeSlot( characters ) );
	}

	[TestMethod]
	public void SlotAllocatorReturnsMinusOneWhenEverySlotIsOccupied()
	{
		var characters = Enumerable.Range( 0, CharacterRules.MaximumSlots )
			.Select( slot => CreateCharacter( 0 ) with { Slot = slot } )
			.ToArray();
		Assert.AreEqual( -1, CharacterRules.FindLowestFreeSlot( characters ) );

		var oneFree = characters.Where( character => character.Slot != CharacterRules.MaximumSlots - 1 ).ToArray();
		Assert.AreEqual( CharacterRules.MaximumSlots - 1, CharacterRules.FindLowestFreeSlot( oneFree ) );
	}

	[TestMethod]
	public void CreationRejectsUnregisteredFields()
	{
		var request = new CharacterCreationRequest
		{
			Name = "Valid Name",
			Description = "A sufficiently long character description.",
			Model = new DefinitionId( "test.model" ),
			Faction = new FactionId( "test.faction" ),
			Fields = new Dictionary<string, CreationValue> { ["money"] = CreationValue.Integer( long.MaxValue ) }
		};

		Assert.IsTrue( CharacterRules.ValidateCreationRequest( request, new HashSet<string>() ).Failed );
	}

	[TestMethod]
	public void IdentityTextRejectsControlAndFormatCharacters()
	{
		// A name reaches other players verbatim through recognition records and nameplates, so
		// spoofing payloads must not survive the creation boundary. These are written as escapes
		// on purpose: as literals they are invisible in an editor and lost by copy/paste.
		foreach ( var name in new[]
		{
			"Bob\nCombine Overwatch", // U+000A interior line break
			"Bob\u0007Smith",         // C0 control
			"Bob\u202Eelbat",         // right-to-left override
			"Bo\u200Bb Smith",        // zero-width space
			"Bob\u200DSmith",         // zero-width joiner
			"Bob\u2066Smith\u2069",   // bidirectional isolates
			"Bob\uFEFFSmith"          // zero-width no-break space
		} )
		{
			Assert.IsTrue(
				CharacterRules.NormalizeName( name ).Failed,
				$"Name '{name.Replace( "\n", "\\n" )}' should have been rejected." );
			Assert.IsTrue( CharacterRules.ValidateCreationRequest( Request( name: name ), NoFields ).Failed );
		}

		// Zero-width characters are not whitespace, so they survive Trim: a length check
		// alone can never catch a name built entirely from them.
		Assert.IsTrue( CharacterRules.NormalizeName( "\u200B\u200B\u200B\u200B" ).Failed );
	}

	[TestMethod]
	public void DescriptionKeepsLineBreaksButRejectsOtherControlCharacters()
	{
		var prose = "A tall citizen in a worn jacket.\nHe keeps his hands in his pockets.";
		Assert.AreEqual( prose, CharacterRules.NormalizeDescription( prose ).Value );
		Assert.IsTrue( CharacterRules.NormalizeDescription( "A citizen\u202Ewith a reversed tail." ).Failed );
		Assert.IsTrue( CharacterRules.NormalizeDescription( "A citizen with a\u0000 null byte." ).Failed );
	}

	[TestMethod]
	public void IdentityTextIsCanonicalizedSoValidationAndStorageAgree()
	{
		// Combining and precomposed acutes render identically, so they must not be
		// storable as two distinct names.
		var decomposed = CharacterRules.NormalizeName( "  Ame\u0301lie  " );
		Assert.IsTrue( decomposed.Succeeded );
		Assert.AreEqual( "Am\u00E9lie", decomposed.Value );
		Assert.AreEqual( decomposed.Value, CharacterRules.NormalizeName( "Am\u00E9lie" ).Value );

		// An unpaired surrogate is rejected rather than thrown out of Normalize.
		Assert.IsTrue( CharacterRules.NormalizeName( "Bob\uD800Smith" ).Failed );

		// Ordinary names still pass.
		Assert.IsTrue( CharacterRules.NormalizeName( "Valid Name" ).Succeeded );
		Assert.IsTrue( CharacterRules.NormalizeName( "Baßmann-O'Neill" ).Succeeded );
		Assert.IsTrue( CharacterRules.ValidateCreationRequest( Request(), NoFields ).Succeeded );
	}

	[TestMethod]
	public void NameRejectsMixedScriptImpersonation()
	{
		// Each of these renders as ordinary Latin text but smuggles one letter from another
		// script, which is exactly how a name is made to look like someone else's.
		Assert.IsTrue( CharacterRules.NormalizeName( "\u0410lice" ).Failed );  // Cyrillic A
		Assert.IsTrue( CharacterRules.NormalizeName( "Alic\u0435" ).Failed );  // Cyrillic e
		Assert.IsTrue( CharacterRules.NormalizeName( "\u03A1eter" ).Failed );  // Greek Rho
		Assert.IsTrue( CharacterRules.NormalizeName( "\u13AAlice" ).Failed );  // Cherokee
		Assert.IsTrue( CharacterRules.ValidateCreationRequest( Request( name: "\u0410lice" ), NoFields ).Failed );

		// A wholly Cyrillic or Greek name is a real name, not an impersonation.
		Assert.IsTrue( CharacterRules.NormalizeName( "Алиса" ).Succeeded );
		Assert.IsTrue( CharacterRules.NormalizeName( "Γεωργος" ).Succeeded );
	}

	[TestMethod]
	public void NameAllowsTheScriptCombinationsRealNamesNeed()
	{
		// Japanese mixes Han, Hiragana and Katakana by nature, and may carry Latin.
		Assert.IsTrue( CharacterRules.NormalizeName( "田中 ひろし" ).Succeeded );
		Assert.IsTrue( CharacterRules.NormalizeName( "タナカ Tanaka" ).Succeeded );
		// Korean mixes Hangul with Latin.
		Assert.IsTrue( CharacterRules.NormalizeName( "김철수 Kim" ).Succeeded );
		// Digits, spaces and punctuation are script-neutral and never constrain a name.
		Assert.IsTrue( CharacterRules.NormalizeName( "Dr. Judith Mossman II" ).Succeeded );
		// Greek alongside Latin is not a combination any real name needs.
		Assert.IsTrue( CharacterRules.NormalizeName( "Alice Γεω" ).Failed );
	}

	[TestMethod]
	public void NameFoldsCompatibilityLookAlikesOntoWhatTheyImitate()
	{
		// Fullwidth Latin is Latin script, so mixed-script detection cannot see it. NFKC folding
		// is what stops it being a second, visually identical spelling of an existing name.
		var fullwidth = CharacterRules.NormalizeName( "\uFF21lice" );
		Assert.IsTrue( fullwidth.Succeeded );
		Assert.AreEqual( "Alice", fullwidth.Value );

		// A description is prose, not identity: it keeps compatibility characters and may mix
		// scripts freely. Only its control- and format-character rules apply.
		Assert.IsTrue( CharacterRules.NormalizeDescription(
			"A citizen who mutters Алиса under their breath." ).Succeeded );
		Assert.IsTrue( CharacterRules.NormalizeDescription(
			"A citizen wearing a \uFF21-series jumpsuit, collar upturned." ).Succeeded );
	}

	[TestMethod]
	public void TwoUnenumeratedScriptsAreStillTwoScripts()
	{
		// Neither Coptic nor Runic is in the named script table. They must not share one "other"
		// bucket, or a name could mix two writing systems the profile never actually looked at.
		Assert.IsTrue( CharacterRules.NormalizeName( "\u2C81\u2C83\u2C85" ).Succeeded );  // Coptic alone
		Assert.IsTrue( CharacterRules.NormalizeName( "\u16A0\u16A2\u16A6" ).Succeeded );  // Runic alone
		Assert.IsTrue( CharacterRules.NormalizeName( "\u2C81\u2C83\u16A0" ).Failed );     // the two mixed
		Assert.IsTrue( CharacterRules.NormalizeName( "\u2C81\u2C83Ab" ).Failed );         // one with Latin
	}
	private static IReadOnlySet<string> NoFields => new HashSet<string>();

	private static CharacterCreationRequest Request( string name = "Valid Name" ) =>
		new()
		{
			Name = name,
			Description = "A sufficiently long character description.",
			Model = new DefinitionId( "test.model" ),
			Faction = new FactionId( "test.faction" ),
			Fields = new Dictionary<string, CreationValue>()
		};

	private static CharacterRecord CreateCharacter( long balance )
	{
		using var document = JsonDocument.Parse( "{}" );
		return new CharacterRecord
		{
			Id = CharacterId.New(),
			AccountId = new AccountId( 1 ),
			Slot = 0,
			Name = "Test",
			Description = "A sufficiently long description.",
			Model = new DefinitionId( "test.model" ),
			Faction = new FactionId( "test.faction" ),
			Balance = balance,
			CreatedAt = DateTimeOffset.UtcNow,
			LastPlayedAt = DateTimeOffset.UtcNow,
			SchemaState = new TypedPayload
			{
				TypeId = new PersistedTypeId( "test.character" ),
				TypeVersion = 1,
				Data = document.RootElement.Clone()
			}
		};
	}
}
#nullable enable

using Hexagon.V2.Client;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Networking;

namespace Hexagon.V2.Tests.Client;

[TestClass]
public sealed class ClientControllerTests
{
	[TestMethod]
	public async Task ControllerMapsEveryUiIntentToAnExplicitCommand()
	{
		var transport = new RecordingTransport();
		var controller = new HexClientController(transport);
		var characterId = CharacterId.New();
		var sourceId = InventoryId.New();
		var targetId = InventoryId.New();
		var itemId = ItemId.New();
		var actionId = new ActionId("use");
		var actionInstance = Guid.NewGuid();
		var actionArguments = new Dictionary<string, SnapshotValue>( StringComparer.Ordinal )
		{
			["amount"] = SnapshotValue.Integer( 3 )
		};
		var creation = new CharacterCreationInput(
			"Alyx", "Description", new DefinitionId("citizen_model"),
			new FactionId("citizen"), null);

		await controller.RequestCharactersAsync();
		await controller.CreateCharacterAsync(creation);
		await controller.LoadCharacterAsync(characterId);
		await controller.DeleteCharacterAsync(characterId);
		await controller.UnloadCharacterAsync();
		await controller.MoveItemAsync(sourceId, targetId, itemId, 2, 3);
		await controller.RunItemActionAsync(sourceId, itemId, actionId, actionArguments);
		actionArguments["amount"] = SnapshotValue.Integer( 99 );
		await controller.DropItemAsync(sourceId, itemId);
		await controller.PickUpItemAsync(itemId, targetId);
		await controller.SendChatAsync("ic", "Hello");
		await controller.CancelActionAsync(actionInstance);

		Assert.HasCount(11, transport.Commands);
		Assert.IsInstanceOfType<RequestCharacterListCommand>(transport.Commands[0]);
		Assert.AreSame(creation, ((CreateCharacterCommand)transport.Commands[1]).Input);
		Assert.AreEqual(characterId, ((LoadCharacterCommand)transport.Commands[2]).CharacterId);
		Assert.IsInstanceOfType<DeleteCharacterCommand>(transport.Commands[3]);
		Assert.IsInstanceOfType<UnloadCharacterCommand>(transport.Commands[4]);
		Assert.AreEqual((2, 3),
			(((MoveInventoryItemCommand)transport.Commands[5]).X,
				((MoveInventoryItemCommand)transport.Commands[5]).Y));
		Assert.AreEqual(actionId, ((RunItemActionCommand)transport.Commands[6]).ActionId);
		Assert.AreEqual( 3L, ((RunItemActionCommand)transport.Commands[6]).Arguments["amount"].IntegerValue );
		Assert.IsInstanceOfType<DropItemCommand>(transport.Commands[7]);
		Assert.IsInstanceOfType<PickUpItemCommand>(transport.Commands[8]);
		Assert.AreEqual("Hello", ((SendChatCommand)transport.Commands[9]).Text);
		Assert.AreEqual(actionInstance, ((CancelActionCommand)transport.Commands[10]).InstanceId);
	}

	[TestMethod]
	public async Task TransportFailureAndCancellationTokenArePropagated()
	{
		var expected = OperationResult.Failure(ErrorCode.Unauthorized, "Denied.");
		var transport = new RecordingTransport(expected);
		var controller = new HexClientController(transport);
		using var source = new CancellationTokenSource();

		var result = await controller.SendChatAsync("ic", "Hello", source.Token);

		Assert.AreEqual(ErrorCode.Unauthorized, result.Error!.Code);
		Assert.AreEqual(source.Token, transport.LastCancellationToken);
	}

	private sealed class RecordingTransport : IClientCommandTransport
	{
		private readonly OperationResult _result;

		public RecordingTransport(OperationResult? result = null) =>
			_result = result ?? OperationResult.Success();

		public List<ClientCommand> Commands { get; } = new();
		public CancellationToken LastCancellationToken { get; private set; }

		public ValueTask<OperationResult> SendAsync(
			ClientCommand command,
			CancellationToken cancellationToken = default)
		{
			Commands.Add(command);
			LastCancellationToken = cancellationToken;
			return ValueTask.FromResult(_result);
		}
	}
}
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;

using Microsoft.VisualStudio.TestTools.UnitTesting;

[assembly: Parallelize( Scope = ExecutionScope.MethodLevel )]
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	[AssemblyInitialize]
	public static void ClassInitialize( TestContext context )
	{
		Sandbox.Application.InitUnitTest();
	}
}
using System.Numerics;
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Skeleton;
using HumanoidRetargeter.Core.Target;
using HumanoidRetargeter.Tests.Skeleton;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Core.Skeleton.Skeleton;

namespace HumanoidRetargeter.Tests.Target;

public class SmartPortAdditiveIkTests
{
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void RecoilDoesNotInventGoalTranslationFromDifferentArmBindPoses(bool authoredGoalMotion)
    {
        var original = TargetRig.Load(TargetRigGenerator.Generate(File.ReadAllText(SkeletonTests.FixturePath("rig_human_male.json")))).Skeleton;
        const string goal = "support_goal";
        var definitions = original.Bones.Select(b => new BoneDefinition(b.Name,
            b.ParentIndex < 0 ? null : original[b.ParentIndex].Name, b.RestLocal)).ToList();
        definitions.Add(new(goal, "hand_R", XForm.ToLocal(original.RestWorld[original.IndexOf("hand_R")],
            original.RestWorld[original.IndexOf("hand_L")])));
        var source = SkeletonModel.Create(definitions);
        var target = SkeletonModel.Create(source.Bones.Select(b => new BoneDefinition(b.Name,
            b.ParentIndex < 0 ? null : source[b.ParentIndex].Name,
            new XForm(b.RestLocal.Pos * .8f, b.RestLocal.Rot *
                (b.Name.StartsWith("arm_upper_") ? Quaternion.CreateFromAxisAngle(Vector3.UnitY, .6f) : Quaternion.Identity)))).ToArray());
        var rig = new SmartPortRig(source, target, new Dictionary<string, string> { [goal] = "hand_L" });
        var ordinary = new SmartPortRig(source, target);
        var pose = Enumerable.Repeat(XForm.Identity, source.Count).ToArray();
        pose[source.IndexOf("arm_upper_R")].Rot = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, .2f);
        pose[source.IndexOf("arm_lower_L")].Rot = Quaternion.CreateFromAxisAngle(Vector3.UnitX, -.3f);
        pose[source.IndexOf(goal)] = new XForm(authoredGoalMotion ? new Vector3(.2f, -.1f, .3f) : Vector3.Zero,
            Quaternion.CreateFromAxisAngle(Vector3.UnitY, .1f));
        var actual = rig.Transfer(pose, true)[rig.Target.IndexOf(goal)];
        var expected = ordinary.Transfer(pose, true)[ordinary.Target.IndexOf(goal)];
        // Additive goals are local deltas on the already fitted grip, not absolute
        // effector positions computed against a reconstructed rest-pose animation.
        Assert.True(Vector3.Distance(expected.Pos, actual.Pos) < .0001f, $"Expected {expected.Pos}, got {actual.Pos}");
        Assert.True(MathQ.AngleBetween(expected.Rot, actual.Rot) < .001f);
        if (!authoredGoalMotion) Assert.True(actual.Pos.Length() < .0001f);
        else Assert.True(actual.Pos.Length() > .1f); // Preserve authored motion; do not freeze the goal.
    }
}
using System.Numerics;
using HumanoidRetargeter.Core.Cleanup;
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Skeleton;
using HumanoidRetargeter.Core.Target;
using HumanoidRetargeter.Tests.Skeleton;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Core.Skeleton.Skeleton;

namespace HumanoidRetargeter.Tests.Target;

public class SmartPortGripReachTests
{
    [Fact]
    public void SharedGripFitsShorterArmsWithoutStretching()
    {
        var original = TargetRig.Load(TargetRigGenerator.Generate(File.ReadAllText(SkeletonTests.FixturePath("rig_human_male.json")))).Skeleton;
        var definitions = original.Bones.Select(b => new BoneDefinition(b.Name,
            b.ParentIndex < 0 ? null : original[b.ParentIndex].Name, b.RestLocal)).ToList();
        foreach (var side in new[] { "L", "R" }) definitions.Add(new("grip_" + side, "hand_" + side, XForm.Identity));
        definitions.Add(new("support_goal", "hand_R", XForm.Identity));
        var source = SkeletonModel.Create(definitions);
        var target = SkeletonModel.Create(original.Bones.Select(b => new BoneDefinition(b.Name,
            b.ParentIndex < 0 ? null : original[b.ParentIndex].Name,
            new XForm(b.RestLocal.Pos * (b.Name is "arm_lower_L" or "hand_L" ? .55f : 1), b.RestLocal.Rot))).ToArray());
        var pose = source.Bones.Select(b => b.RestLocal).ToArray();
        var left = source.IndexOf("arm_upper_L"); var right = source.IndexOf("arm_upper_R");
        var center = (source.RestWorld[left].Pos + source.RestWorld[right].Pos) * .5f;
        var up = Vector3.Normalize(source.RestWorld[source.IndexOf("head")].Pos - source.RestWorld[source.IndexOf("pelvis")].Pos);
        var lateral = Vector3.Normalize(source.RestWorld[right].Pos - source.RestWorld[left].Pos);
        var forward = Vector3.Normalize(Vector3.Cross(up, lateral));
        var reach = source[source.IndexOf("arm_lower_R")].RestLocal.Pos.Length() + source[source.IndexOf("hand_R")].RestLocal.Pos.Length();
        foreach (var side in new[] { "L", "R" })
        {
            var chain = new LimbChain { Upper = source.IndexOf("arm_upper_" + side), Lower = source.IndexOf("arm_lower_" + side), End = source.IndexOf("hand_" + side) };
            var goal = center + forward * reach * (side == "L" ? .75f : .5f) + lateral * (side == "L" ? -4 : 4) - up * reach * .1f;
            EffectorIk.ApplyGoals(new() { pose }, source, chain, new[] { goal }, lateral, soften: 0);
        }
        var input = new Pose(pose).ToWorld(source);
        pose[source.IndexOf("support_goal")] = XForm.ToLocal(input[source.IndexOf("hand_R")], input[source.IndexOf("hand_L")]);
        var targets = new Dictionary<string, string> { ["support_goal"] = "hand_L" };
        var rig = new SmartPortRig(source, target, targets, new[] { "grip_L", "grip_R" });
        var result = rig.Transfer(pose);
        var world = new Pose(result).ToWorld(rig.Target);
        var error = Vector3.Distance(world[rig.Target.IndexOf("hand_L")].Pos, world[rig.Target.IndexOf("support_goal")].Pos);
        Assert.True(error < .01f, $"Support hand missed its goal by {error}");
        foreach (var side in new[] { "L", "R" })
        foreach (var name in new[] { "arm_upper_", "arm_lower_", "hand_" })
        {
            var index = rig.Target.IndexOf(name + side);
            Assert.True(Vector3.Distance(rig.Target[index].RestLocal.Pos, result[index].Pos) < .0001f);
        }
        var unfitted = new SmartPortRig(source, target, targets);
        var before = new Pose(unfitted.Transfer(pose)).ToWorld(unfitted.Target);
        Assert.True(Vector3.Distance(before[unfitted.Target.IndexOf("hand_L")].Pos, before[unfitted.Target.IndexOf("support_goal")].Pos) > .1f);
        Assert.All(rig.Transfer(Enumerable.Repeat(XForm.Identity, source.Count).ToArray(), true), delta =>
        {
            Assert.True(delta.Pos.Length() < .0001f);
            Assert.True(MathQ.AngleBetween(delta.Rot, Quaternion.Identity) < .001f);
        });
    }
}
using System.Numerics;
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Skeleton;
using HumanoidRetargeter.Core.Target;
using HumanoidRetargeter.Tests.Skeleton;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Core.Skeleton.Skeleton;

namespace HumanoidRetargeter.Tests.Target;

public class SmartPortHandSocketTests
{
    [Theory]
    [InlineData(0)]
    [InlineData(2)]
    public void CopiedSocketFitsTheTargetHandNotItsLegProportions(int helperDepth)
    {
        var original = TargetRig.Load(TargetRigGenerator.Generate(File.ReadAllText(SkeletonTests.FixturePath("rig_human_male.json")))).Skeleton;
        var names = new[] { "finger_index_0_R", "finger_middle_0_R", "finger_ring_0_R" };
        Vector3 Center(SkeletonModel sk, XForm[] world) => names.Select(n => world[sk.IndexOf(n)].Pos).Aggregate(Vector3.Zero, (a,b) => a+b) / names.Length;
        const string socket = "new_hand_socket";
        var bind = original.RestWorld.ToArray();
        var hand = original.IndexOf("hand_R");
        var definitions = original.Bones.Select(b => new BoneDefinition(b.Name,
            b.ParentIndex < 0 ? null : original[b.ParentIndex].Name, b.RestLocal)).ToList();
        definitions.Add(new(socket, "hand_R", new XForm(bind[hand].Inverse().TransformPoint(Center(original, bind)), Quaternion.Identity)));
        var attachment = socket;
        for (var i = 0; i < helperDepth; i++)
        {
            var child = "nested_socket_" + i;
            definitions.Add(new(child, attachment, XForm.Identity));
            attachment = child;
        }
        var source = SkeletonModel.Create(definitions);
        var target = SkeletonModel.Create(original.Bones.Select(b => new BoneDefinition(b.Name,
            b.ParentIndex < 0 ? null : original[b.ParentIndex].Name,
            new XForm(b.RestLocal.Pos * (b.Name.StartsWith("finger_") ? .5f : 1f), b.RestLocal.Rot))).ToArray());
        var rig = new SmartPortRig(source, target, attachmentBones: new[] { attachment, socket });
        var pose = source.Bones.Select(b => b.RestLocal).ToArray();
        var result = rig.Transfer(pose);
        var world = new Pose(result).ToWorld(rig.Target);
        var unfitted = new SmartPortRig(source, target);
        var before = new Pose(unfitted.Transfer(pose)).ToWorld(unfitted.Target);
        Assert.True(Vector3.Distance(Center(unfitted.Target, before), before[unfitted.Target.IndexOf(socket)].Pos) > .1f);
        Assert.True(Vector3.Distance(Center(rig.Target, world), world[rig.Target.IndexOf(socket)].Pos) < .001f,
            $"Expected {Center(rig.Target, world)}, got {world[rig.Target.IndexOf(socket)].Pos}");
        Assert.True(Vector3.Distance(Center(rig.Target, world), world[rig.Target.IndexOf(attachment)].Pos) < .001f);
        foreach (var bone in target.Bones)
            Assert.Equal(bone.RestLocal, rig.Target[rig.Target.IndexOf(bone.Name)].RestLocal);
        Assert.All(rig.Transfer(Enumerable.Repeat(XForm.Identity, source.Count).ToArray(), true), delta =>
        {
            Assert.True(delta.Pos.Length() < .0001f);
            Assert.True(MathQ.AngleBetween(delta.Rot, Quaternion.Identity) < .001f);
        });
    }

    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void ExistingFittedSocketIsNotReplacedByHandGeometry(bool nestedSource)
    {
        var original = TargetRig.Load(TargetRigGenerator.Generate(File.ReadAllText(SkeletonTests.FixturePath("rig_human_male.json")))).Skeleton;
        var definitions = original.Bones.Select(b => new BoneDefinition(b.Name,
            nestedSource && b.Name == "hold_R" ? "weapon_pivot" : b.ParentIndex < 0 ? null : original[b.ParentIndex].Name, b.RestLocal)).ToList();
        if (nestedSource) definitions.Add(new("weapon_pivot", "hand_R", XForm.Identity));
        var source = SkeletonModel.Create(definitions);
        var target = SkeletonModel.Create(original.Bones.Select(b => new BoneDefinition(b.Name,
            b.ParentIndex < 0 ? null : original[b.ParentIndex].Name,
            new XForm(b.RestLocal.Pos * (b.Name.StartsWith("finger_") ? .5f : 1f)
                + (b.Name == "hold_R" ? new Vector3(.2f, .1f, .3f) : Vector3.Zero), b.RestLocal.Rot))).ToArray());
        var fitted = new SmartPortRig(source, target, attachmentBones: new[] { "hold_R" });
        var ordinary = new SmartPortRig(source, target);
        var pose = source.Bones.Select(b => b.RestLocal).ToArray();
        Assert.Equal(ordinary.Transfer(pose), fitted.Transfer(pose));
    }

    /// <summary>A target with a half-size hand whose hold_R sits outside the hand entirely.</summary>
    static (SkeletonModel Source, SkeletonModel Target) OffHandSocket()
    {
        var original = TargetRig.Load(TargetRigGenerator.Generate(File.ReadAllText(SkeletonTests.FixturePath("rig_human_male.json")))).Skeleton;
        var small = SkeletonModel.Create(original.Bones.Select(b => new BoneDefinition(b.Name,
            b.ParentIndex < 0 ? null : original[b.ParentIndex].Name,
            new XForm(b.RestLocal.Pos * (b.Name.StartsWith("finger_") ? .5f : 1f), b.RestLocal.Rot))).ToArray());
        var hand = small.IndexOf("hand_R");
        var reach = small.Bones.Where(b => b.Name.StartsWith("finger_") && b.Name.EndsWith("_R"))
            .Max(b => Vector3.Distance(small.RestWorld[b.Index].Pos, small.RestWorld[hand].Pos));
        // Well past the fingertips: a weapon on it would hang in the air.
        var place = small.RestWorld[hand].Pos + Vector3.UnitZ * reach * 2.2f;
        var target = SkeletonModel.Create(small.Bones.Select(b => new BoneDefinition(b.Name, b.ParentIndex < 0 ? null : small[b.ParentIndex].Name,
            b.Name == "hold_R" ? new XForm(small.RestWorld[hand].Inverse().TransformPoint(place), b.RestLocal.Rot) : b.RestLocal)).ToArray());
        return (original, target);
    }

    static Vector3 Knuckles(SkeletonModel sk, XForm[] world) => new[] { "finger_index_0_R", "finger_middle_0_R", "finger_ring_0_R" }
        .Select(n => world[sk.IndexOf(n)].Pos).Aggregate(Vector3.Zero, (a, b) => a + b) / 3f;

    [Fact]
    public void ExistingSocketOutsideTheHandIsFittedToTheKnuckles()
    {
        var (source, target) = OffHandSocket();
        var rig = new SmartPortRig(source, target, attachmentBones: new[] { "hold_R" });
        var unfitted = new SmartPortRig(source, target);
        var pose = source.Bones.Select(b => b.RestLocal).ToArray();
        var world = new Pose(rig.Transfer(pose)).ToWorld(rig.Target);
        var before = new Pose(unfitted.Transfer(pose)).ToWorld(unfitted.Target);
        var socket = rig.Target.IndexOf("hold_R");
        var sourceGrip = source.RestWorld[source.IndexOf("hold_R")].Pos - Knuckles(source, source.RestWorld.ToArray());
        // Where the source's grip lands on this hand: the knuckle centre plus the source offset.
        var expected = Knuckles(rig.Target, world) + sourceGrip * rig.MotionScale;
        Assert.True(Vector3.Distance(before[socket].Pos, expected) > 1f, "the socket must start off the grip");
        Assert.True(Vector3.Distance(world[socket].Pos, expected) < .001f, $"Expected {expected}, got {world[socket].Pos}");
        // Rotation stays as animated; the bind and every other bone are untouched.
        Assert.True(MathQ.AngleBetween(world[socket].Rot, before[socket].Rot) < .001f);
        foreach (var bone in target.Bones)
            Assert.Equal(bone.RestLocal, rig.Target[rig.Target.IndexOf(bone.Name)].RestLocal);
        var result = rig.Transfer(pose);
        var plain = unfitted.Transfer(pose);
        for (var i = 0; i < result.Length; i++)
            if (i != socket) Assert.Equal(plain[i], result[i]);
        // Additive clips: a zero delta stays zero.
        Assert.All(rig.Transfer(Enumerable.Repeat(XForm.Identity, source.Count).ToArray(), true), delta =>
        {
            Assert.True(delta.Pos.Length() < .0001f);
            Assert.True(MathQ.AngleBetween(delta.Rot, Quaternion.Identity) < .001f);
        });
    }

    [Fact]
    public void FittedExistingSocketFollowsTheGripWhileTheHandAnimates()
    {
        var (source, target) = OffHandSocket();
        var rig = new SmartPortRig(source, target, attachmentBones: new[] { "hold_R" });
        // Bend the wrist: the grip moves with the hand, the socket must stay on it.
        var hand = source.IndexOf("hand_R");
        var pose = source.Bones.Select(b => b.RestLocal).ToArray();
        pose[hand] = new XForm(pose[hand].Pos, Quaternion.Normalize(pose[hand].Rot * Quaternion.CreateFromAxisAngle(Vector3.UnitZ, .6f)));
        var sourceWorld = new Pose(pose).ToWorld(source);
        var sourceGrip = sourceWorld[source.IndexOf("hold_R")].Pos - Knuckles(source, sourceWorld);
        var world = new Pose(rig.Transfer(pose)).ToWorld(rig.Target);
        var expected = Knuckles(rig.Target, world) + sourceGrip * rig.MotionScale;
        Assert.True(Vector3.Distance(world[rig.Target.IndexOf("hold_R")].Pos, expected) < .001f,
            $"Expected {expected}, got {world[rig.Target.IndexOf("hold_R")].Pos}");
    }
}
using System.Numerics;
using HumanoidRetargeter.Core.Dl;
using HumanoidRetargeter.Core.Mapping;
using HumanoidRetargeter.Core.Skeleton;
using HumanoidRetargeter.Core.Solve;
using Xunit;

namespace HumanoidRetargeter.Tests.Dl;

/// <summary>
/// Quality gate for the DL solver (Milestone 10): on the spike's fixture clip, the SAME
/// port's role trajectories must agree with the verified geometric solver's — the spike
/// measured a mean role cosine of 0.937 against the geometric dump; the gate sits below it
/// (mean ≥ 0.85 across body roles, feet/head ≥ 0.9 individually, hands are the known-weak
/// spot of the pretrained checkpoint).
/// </summary>
public class DlSolverTests
{
    private static readonly Lazy<byte[]> WeightBytes = new(() => File.ReadAllBytes(
        DlFixtures.RepoFile("Assets", "data", "humanoid_retargeter", "dl", "same_v1.weights")));

    private static readonly string[] BodyRoleBones =
    {
        "head", "hand_L", "hand_R", "ankle_L", "ankle_R",
        "ball_L", "ball_R", "arm_lower_L", "arm_lower_R",
        "leg_lower_L", "leg_lower_R",
    };

    private static readonly string[] StrictBones = { "head", "ankle_L", "ankle_R", "ball_L", "ball_R" };

    private static (Clip Dl, Clip Geo, SourceScene Scene) Solve()
    {
        var scene = DlFixtures.FixtureBvh();
        var map = ProfileDetector.Detect(scene.Skeleton) is { } detected
            ? detected.Result
            : throw new InvalidOperationException("fixture should detect as mixamo");
        var rig = DlFixtures.SboxRig.Value;

        var dl = new DlSolver(WeightBytes.Value).Solve(scene, map, rig, new SolveOptions());
        var geo = new GeometricSolver().Solve(scene, map, rig, new SolveOptions());
        return (dl, geo, scene);
    }

    /// <summary>One shared solve pair for this class and the deriver tests.</summary>
    internal static readonly Lazy<(Clip Dl, Clip Geo, SourceScene Scene)> Solved = new(Solve);

    [Fact]
    public void SolveIsFiniteAndFrameAligned()
    {
        var (dl, _, scene) = Solved.Value;
        var clip = scene.Clips[0];

        Assert.Equal(clip.FrameCount, dl.FrameCount);
        Assert.Equal(clip.Fps, dl.Fps);
        foreach (var frame in dl.Frames)
        {
            Assert.Equal(DlFixtures.SboxRig.Value.Skeleton.Count, frame.Length);
            foreach (var xf in frame)
            {
                Assert.True(float.IsFinite(xf.Pos.X) && float.IsFinite(xf.Pos.Y) && float.IsFinite(xf.Pos.Z));
                Assert.True(float.IsFinite(xf.Rot.X) && float.IsFinite(xf.Rot.Y)
                    && float.IsFinite(xf.Rot.Z) && float.IsFinite(xf.Rot.W));
            }
        }
    }

    [Fact]
    public void SolveIsDeterministic()
    {
        var (dl, _, scene) = Solved.Value;
        var map = ProfileDetector.Detect(scene.Skeleton)!.Value.Result;
        var again = new DlSolver(WeightBytes.Value).Solve(
            scene, map, DlFixtures.SboxRig.Value, new SolveOptions());

        for (var f = 0; f < dl.FrameCount; f++)
        {
            for (var b = 0; b < dl.Frames[f].Length; b++)
                Assert.Equal(dl.Frames[f][b], again.Frames[f][b]);
        }
    }

    [Fact]
    public void QualityGate_RoleTrajectoriesAgreeWithGeometricSolver()
    {
        var (dl, geo, _) = Solved.Value;
        var rig = DlFixtures.SboxRig.Value;
        var skeleton = rig.Skeleton;
        var frames = Math.Min(dl.FrameCount, geo.FrameCount);

        var pelvis = skeleton.IndexOf("pelvis");
        var head = skeleton.IndexOf("head");
        Assert.True(pelvis >= 0 && head >= 0);

        // World positions per frame for both solves.
        var dlPos = WorldPositions(skeleton, dl, frames);
        var geoPos = WorldPositions(skeleton, geo, frames);

        // Per-side scale proxy (mean pelvis→head distance) for scale-invariant comparison.
        var dlScale = MeanDistance(dlPos, pelvis, head, frames);
        var geoScale = MeanDistance(geoPos, pelvis, head, frames);
        Assert.True(dlScale > 1f && geoScale > 1f);

        var report = new System.Text.StringBuilder();
        var cosines = new Dictionary<string, float>();
        foreach (var bone in BodyRoleBones)
        {
            var index = skeleton.IndexOf(bone);
            Assert.True(index >= 0, $"rig has no bone '{bone}'");
            var sum = 0f;
            for (var f = 0; f < frames; f++)
            {
                var a = (dlPos[f][index] - dlPos[f][pelvis]) / dlScale;
                var b = (geoPos[f][index] - geoPos[f][pelvis]) / geoScale;
                var denom = a.Length() * b.Length() + 1e-8f;
                sum += Vector3.Dot(a, b) / denom;
            }
            var cos = sum / frames;
            cosines[bone] = cos;
            report.AppendLine($"{bone}: {cos:0.000}");
        }

        var mean = cosines.Values.Average();
        Assert.True(mean >= 0.85f,
            $"mean role-trajectory cosine {mean:0.000} < 0.85 (spike measured 0.937)\n{report}");
        foreach (var bone in StrictBones)
        {
            Assert.True(cosines[bone] >= 0.9f,
                $"{bone} trajectory cosine {cosines[bone]:0.000} < 0.90\n{report}");
        }
    }

    private static Vector3[][] WorldPositions(
        HumanoidRetargeter.Core.Skeleton.Skeleton skeleton, Clip clip, int frames)
    {
        var result = new Vector3[frames][];
        for (var f = 0; f < frames; f++)
        {
            var world = new Pose(clip.Frames[f]).ToWorld(skeleton);
            result[f] = new Vector3[skeleton.Count];
            for (var b = 0; b < skeleton.Count; b++)
                result[f][b] = world[b].Pos;
        }
        return result;
    }

    private static float MeanDistance(Vector3[][] positions, int a, int b, int frames)
    {
        var sum = 0f;
        for (var f = 0; f < frames; f++)
            sum += Vector3.Distance(positions[f][a], positions[f][b]);
        return sum / frames;
    }
}
using System.Globalization;
using System.Numerics;
using System.Text;
using HumanoidRetargeter.Core.Formats.Bvh;
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Skeleton;
using Xunit;

namespace HumanoidRetargeter.Tests.Formats;

/// <summary>
/// The BVH importer's calibration rest-frame trim (user report: 47_01.bvh — "starts either
/// ends or starts at t-pose … when i move the preview slider to 0 then there's a t-pose").
/// Mocap conversions (CMU asf/amc exports among them) prepend a skeleton-calibration
/// segment: the rest pose itself, hard-cut — or blend-ramped over 2–3 frames
/// (makehuman-retarget exports) — into the real motion. Measured on the repro corpus:
/// calibration frames/ramps sit ≤ 24° (max joint rotation) from the identity-rotation bind
/// with a 25–177° discontinuity into the motion; every real clip edge sits ≥ 80° from the
/// bind with ≤ 8° edge continuity. The importer drops such a segment (≤ 4 frames) per clip
/// end, and ONLY when its frames are rest-like AND the clip beyond is not AND the segment's
/// exit discontinuity is large both absolutely and against the clip's median inter-frame
/// delta — a clip legitimately idling near rest must never lose its first frame.
/// </summary>
public class BvhCalibrationTrimTests
{
    private const float Rad2Deg = 180f / MathF.PI;

    // ---------------------------------------------------------------- synthetic fixtures

    /// <summary>
    /// Two-joint rig (30 native fps = the import grid, so native frames map 1:1 onto clip
    /// frames) whose Head rotation per native frame is caller-chosen. Offsets keep the rest
    /// height above the meters heuristic threshold.
    /// </summary>
    private static byte[] BvhWithHeadAngles(params float[] headZDegPerFrame)
    {
        var sb = new StringBuilder();
        sb.AppendLine("HIERARCHY");
        sb.AppendLine("ROOT Hips");
        sb.AppendLine("{");
        sb.AppendLine("    OFFSET 0 0 0");
        sb.AppendLine("    CHANNELS 6 Xposition Yposition Zposition Zrotation Yrotation Xrotation");
        sb.AppendLine("    JOINT Head");
        sb.AppendLine("    {");
        sb.AppendLine("        OFFSET 0 70 0");
        sb.AppendLine("        CHANNELS 3 Zrotation Yrotation Xrotation");
        sb.AppendLine("        End Site");
        sb.AppendLine("        {");
        sb.AppendLine("            OFFSET 0 10 0");
        sb.AppendLine("        }");
        sb.AppendLine("    }");
        sb.AppendLine("}");
        sb.AppendLine("MOTION");
        sb.AppendLine($"Frames: {headZDegPerFrame.Length}");
        sb.AppendLine("Frame Time: 0.0333333");
        foreach (var z in headZDegPerFrame)
        {
            sb.AppendLine(string.Create(
                CultureInfo.InvariantCulture, $"0 90 0 0 0 0 {z} 0 0"));
        }
        return Encoding.ASCII.GetBytes(sb.ToString());
    }

    private static float HeadAngleDeg(SourceScene scene, int frame)
    {
        var head = scene.Skeleton.IndexOf("Head");
        return MathQ.AngleBetween(
            Quaternion.Identity, scene.Clips[0].Frames[frame][head].Rot) * Rad2Deg;
    }

    /// <summary>A held pose at 70° with ~1°/frame drift; interior deltas ≈ 1°.</summary>
    private static float[] HeldPose(int frames, float startDeg = 70f)
    {
        var values = new float[frames];
        for (var f = 0; f < frames; f++)
            values[f] = startDeg + f;
        return values;
    }

    // ---------------------------------------------------------------- trims

    [Fact]
    public void LeadingCalibrationRestFrame_IsTrimmed()
    {
        var angles = new float[10];
        HeldPose(9).CopyTo(angles, 1); // frame 0 stays 0 = the rest pose; 1..9 held at ~70°
        var scene = BvhImporter.Import(BvhWithHeadAngles(angles));

        var clip = Assert.Single(scene.Clips);
        Assert.Equal(9, clip.FrameCount); // 10 native − 1 calibration frame
        Assert.InRange(HeadAngleDeg(scene, 0), 65f, 75f); // clip now opens on the real pose
    }

    [Fact]
    public void TrailingCalibrationRestFrame_IsTrimmed()
    {
        var angles = new float[10];
        HeldPose(9).CopyTo(angles, 0); // frames 0..8 held at ~70°; frame 9 back to rest
        var scene = BvhImporter.Import(BvhWithHeadAngles(angles));

        var clip = Assert.Single(scene.Clips);
        Assert.Equal(9, clip.FrameCount);
        Assert.InRange(HeadAngleDeg(scene, clip.FrameCount - 1), 65f, 85f); // ends on the pose
    }

    [Fact]
    public void CalibrationFramesAtBothEnds_AreBothTrimmed()
    {
        var angles = new float[11];
        HeldPose(9).CopyTo(angles, 1); // frame 0 AND frame 10 at rest, 1..9 held
        var scene = BvhImporter.Import(BvhWithHeadAngles(angles));

        var clip = Assert.Single(scene.Clips);
        Assert.Equal(9, clip.FrameCount);
        Assert.InRange(HeadAngleDeg(scene, 0), 65f, 75f);
        Assert.InRange(HeadAngleDeg(scene, clip.FrameCount - 1), 65f, 85f);
    }

    /// <summary>A 2-frame rest→motion blend ramp (measured on a makehuman-retarget export:
    /// rest 10°, ramp 24°, then motion ≥ 49°) is a calibration segment too.</summary>
    [Fact]
    public void CalibrationBlendRamp_IsTrimmed()
    {
        var angles = new float[12];
        angles[1] = 12f; // frame 0 at rest, frame 1 mid-blend — both rest-like
        HeldPose(10).CopyTo(angles, 2); // frames 2..11 held at ~70°
        var scene = BvhImporter.Import(BvhWithHeadAngles(angles));

        var clip = Assert.Single(scene.Clips);
        Assert.Equal(10, clip.FrameCount); // both ramp frames trimmed
        Assert.InRange(HeadAngleDeg(scene, 0), 65f, 75f);
    }

    // ---------------------------------------------------------------- must-NOT-trim guards

    /// <summary>An idle that legitimately STARTS at the rest pose ramps continuously into
    /// motion — rest-similarity alone must never cost it its first frame.</summary>
    [Fact]
    public void IdleStartingAtRest_KeepsItsFirstFrame()
    {
        var angles = new float[20];
        for (var f = 0; f < angles.Length; f++)
            angles[f] = f * 3f; // 3°/frame ramp from exact rest
        var scene = BvhImporter.Import(BvhWithHeadAngles(angles));

        var clip = Assert.Single(scene.Clips);
        Assert.Equal(20, clip.FrameCount);
        Assert.InRange(HeadAngleDeg(scene, 0), 0f, 0.01f);
    }

    /// <summary>A clip resting throughout (e.g. a bind-pose export) has nothing to trim:
    /// the neighbor frame is rest-like too.</summary>
    [Fact]
    public void AllRestClip_IsNotTrimmed()
    {
        var scene = BvhImporter.Import(BvhWithHeadAngles(new float[8]));

        var clip = Assert.Single(scene.Clips);
        Assert.Equal(8, clip.FrameCount);
    }

    /// <summary>A hard cut between two NON-rest poses (action edit) is not a calibration
    /// frame — the edge must be rest-like for the trim to engage.</summary>
    [Fact]
    public void HardCutBetweenRealPoses_IsNotTrimmed()
    {
        var angles = HeldPose(10, startDeg: 120f);
        for (var f = 1; f < angles.Length; f++)
            angles[f] = 60f + f; // frame 0 at 120°, cut to ~60° held
        var scene = BvhImporter.Import(BvhWithHeadAngles(angles));

        var clip = Assert.Single(scene.Clips);
        Assert.Equal(10, clip.FrameCount);
    }

    /// <summary>Two-frame clips are never trimmed (no interior to judge typical motion by).</summary>
    [Fact]
    public void TwoFrameClip_IsNotTrimmed()
    {
        var scene = BvhImporter.Import(BvhWithHeadAngles(0f, 70f));
        Assert.Equal(2, Assert.Single(scene.Clips).FrameCount);
    }

    // ---------------------------------------------------------------- real repro file

    /// <summary>47_01.bvh (CMU mocap, the user repro): native frame 0 is a calibration
    /// T-pose 21° from the bind with an 89.6° cut into frame 1 (measured); the imported
    /// clip must open on the real motion instead. Silently green when the local corpus
    /// file is absent.</summary>
    [Fact]
    public void Cmu4701_CalibrationTPose_DoesNotReachTheClip()
    {
        var path = TestUtil.RepoFile("dev", "corpus", "todo", "47_01.bvh");
        if (!File.Exists(path))
            return;

        var scene = BvhImporter.Import(File.ReadAllBytes(path));
        var clip = Assert.Single(scene.Clips);

        // 1320 native frames at 120 fps → 1319 after the trim → 330 on the 30 fps grid.
        Assert.Equal(330, clip.FrameCount);

        // Frame 0 is now a real pose, far from the identity-rotation bind …
        float MaxJointAngleDeg(int frame)
        {
            var max = 0f;
            foreach (var x in clip.Frames[frame])
                max = MathF.Max(max, MathQ.AngleBetween(Quaternion.Identity, x.Rot) * Rad2Deg);
            return max;
        }
        Assert.True(MaxJointAngleDeg(0) > 60f,
            $"clip still opens rest-like ({MaxJointAngleDeg(0):F1}° from bind) — calibration frame not trimmed");

        // … and continuous into its neighbor (the old frame 0 cut 89.6° deep).
        var maxDelta = 0f;
        for (var i = 0; i < scene.Skeleton.Count; i++)
        {
            maxDelta = MathF.Max(maxDelta, MathQ.AngleBetween(
                clip.Frames[0][i].Rot, clip.Frames[1][i].Rot) * Rad2Deg);
        }
        Assert.True(maxDelta < 30f, $"frame 0→1 still discontinuous ({maxDelta:F1}°)");
    }
}
using System.Numerics;
using System.Text.Json;
using HumanoidRetargeter.Core.Formats.Fbx;
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Skeleton;
using Xunit;
using Skel = HumanoidRetargeter.Core.Skeleton.Skeleton;

namespace HumanoidRetargeter.Tests.Formats;

// =============================================================================================
// UNIT / SPACE RESOLUTION (determined empirically from research/extract_anim.py + the JSONs):
//
// The ground-truth JSONs (fixtures/anim_truth/*.json) were produced by Blender's FBX importer
// (automatic_bone_orientation=False) and mix TWO spaces:
//
//  1. `bones[]` rest data comes from `armature.data.bones[*].matrix_local`, which is in
//     ARMATURE space. Blender's FBX importer does not bake the axis/unit conversion into the
//     armature data — it stores it on the object (rotation_euler.x = +90 deg, scale = 0.01 *
//     UnitScaleFactor). Armature space therefore equals the FBX file's native space and units
//     (Y-up, centimeters for these fixtures, UnitScaleFactor = 1). Rest values compare to our
//     imported skeleton DIRECTLY, no conversion.
//
//  2. `frames[]` come from `obj.matrix_world @ pose_bone.matrix`, which is in BLENDER WORLD
//     space (Z-up, meters, the 0.01 object scale applied). To compare a truth world transform
//     against our FBX-space result: p_fbx = Rx(-90deg) * wp / object_scale,
//     q_fbx = Rx(-90deg) * wr (column-vector quaternion composition, parent on the left).
//
// Frame alignment: Blender places the first FBX key at action frame_start, and the truth was
// sampled on an fps grid from there; our importer samples from the earliest curve key on the
// same grid, so truth frame i == our clip frame i.
//
// Note: "Lead Jab.fbx" is NOT a Mixamo rig — it is a citizen-skeleton clip (84 s&box bones,
// pelvis/spine_0/...), which conveniently exercises a second rig family.
//
// TOLERANCES: the task gates were 0.5% of skeleton height / 1.0 deg (rest) and 0.7% / 1.0 deg
// (animation). Observed residuals are float-precision noise — max 0.0002 cm / 0.0004 deg over
// every bone of both rigs at all sampled frames — so the tolerances below are tightened to
// 0.25 cm / 0.25 deg absolute (~1000x above the observed residual, ~4x tighter than the gate).
// =============================================================================================
public class FbxImporterTests
{
    private const float KDegPerRad = 180f / MathF.PI;
    private const float PosTolCm = 0.25f;
    private const float RotTolDeg = 0.25f;

    private static byte[] Fixture(string name)
        => File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "fixtures", "fbx", name));

    private static SourceScene ImportFixture(string name)
        => FbxImporter.Import(Fixture(name), new FbxImportOptions());

    // ---- ground truth -----------------------------------------------------------------

    private sealed record TruthBone(
        string Name, string? Parent, Vector3 RestWorldPos, Quaternion RestWorldRot);

    private sealed record TruthFrameBone(Vector3 WorldPos, Quaternion WorldRot);

    private sealed class Truth
    {
        public required float SampleFps;
        public required int NSamples;
        public required float ObjectScale;
        public required List<TruthBone> Bones;
        public required List<TruthFrameBone[]> Frames;

        /// <summary>Rest skeleton height (max-min world Y, FBX Y-up) in native units (cm).</summary>
        public float Height()
        {
            float min = float.MaxValue, max = float.MinValue;
            foreach (var b in Bones)
            {
                min = MathF.Min(min, b.RestWorldPos.Y);
                max = MathF.Max(max, b.RestWorldPos.Y);
            }
            return max - min;
        }
    }

    private static Truth LoadTruth(string name)
    {
        var path = Path.Combine(AppContext.BaseDirectory, "fixtures", "anim_truth", name);
        using var doc = JsonDocument.Parse(File.ReadAllBytes(path));
        var r = doc.RootElement;

        static Vector3 V3(JsonElement e) => new(
            e[0].GetSingle(), e[1].GetSingle(), e[2].GetSingle());
        static Quaternion Q(JsonElement e) => new(
            e[0].GetSingle(), e[1].GetSingle(), e[2].GetSingle(), e[3].GetSingle());

        var bones = new List<TruthBone>();
        foreach (var b in r.GetProperty("bones").EnumerateArray())
        {
            bones.Add(new TruthBone(
                b.GetProperty("name").GetString()!,
                b.GetProperty("parent").ValueKind == JsonValueKind.Null
                    ? null : b.GetProperty("parent").GetString(),
                V3(b.GetProperty("rest_world_pos")),
                Q(b.GetProperty("rest_world_rot_xyzw"))));
        }

        var frames = new List<TruthFrameBone[]>();
        foreach (var f in r.GetProperty("frames").EnumerateArray())
        {
            var fb = new TruthFrameBone[bones.Count];
            int i = 0;
            foreach (var e in f.EnumerateArray())
                fb[i++] = new TruthFrameBone(V3(e.GetProperty("wp")), Q(e.GetProperty("wr")));
            frames.Add(fb);
        }

        return new Truth
        {
            SampleFps = r.GetProperty("sample_fps").GetSingle(),
            NSamples = r.GetProperty("n_samples").GetInt32(),
            ObjectScale = r.GetProperty("object_scale")[0].GetSingle(),
            Bones = bones,
            Frames = frames,
        };
    }

    // Blender world -> FBX native space: undo object rotation (X +90 deg) and object scale.
    private static readonly Quaternion BlenderToFbx =
        Quaternion.CreateFromAxisAngle(Vector3.UnitX, -MathF.PI / 2f);

    private static Vector3 FramePosToFbx(Vector3 wp, float objectScale)
        => Vector3.Transform(wp, BlenderToFbx) / objectScale;

    private static Quaternion FrameRotToFbx(Quaternion wr)
        => BlenderToFbx * wr;

    // ---- comparison helper -------------------------------------------------------------

    /// <summary>
    /// Compares world transforms of all common bones; fails with a per-bone diff dump of the
    /// worst offenders. Returns the number of common bones compared.
    /// </summary>
    private static int AssertWorldsMatch(
        Skel skeleton,
        IReadOnlyList<XForm> worlds,
        IEnumerable<(string Name, Vector3 Pos, Quaternion Rot)> truthWorlds,
        float posTolCm,
        float rotTolDeg,
        string context)
    {
        var failures = new List<(string Bone, float PosErr, float RotErrDeg)>();
        int common = 0;
        float maxPos = 0f, maxRot = 0f;

        foreach (var (name, tPos, tRot) in truthWorlds)
        {
            int idx = skeleton.IndexOf(name);
            if (idx < 0)
                continue;
            common++;

            float posErr = Vector3.Distance(worlds[idx].Pos, tPos);
            float rotErr = MathQ.AngleBetween(worlds[idx].Rot, tRot) * KDegPerRad;
            maxPos = MathF.Max(maxPos, posErr);
            maxRot = MathF.Max(maxRot, rotErr);

            if (posErr > posTolCm || rotErr > rotTolDeg)
                failures.Add((name, posErr, rotErr));
        }

        if (failures.Count > 0)
        {
            var worst = failures
                .OrderByDescending(f => MathF.Max(f.PosErr / posTolCm, f.RotErrDeg / rotTolDeg))
                .Take(12)
                .Select(f => $"  {f.Bone}: posErr={f.PosErr:F3} cm (tol {posTolCm:F3}), rotErr={f.RotErrDeg:F3} deg (tol {rotTolDeg:F2})");
            Assert.Fail(
                $"{context}: {failures.Count}/{common} bones out of tolerance " +
                $"(max posErr={maxPos:F3} cm, max rotErr={maxRot:F3} deg):\n" +
                string.Join("\n", worst));
        }

        return common;
    }

    // ---- 1. Zombie Crawl: skeleton + clip basics ----------------------------------------

    [Fact]
    public void ZombieCrawl_SkeletonAndClipBasics()
    {
        var scene = ImportFixture("Zombie Crawl.fbx");

        // Spot list of 10 well-known Mixamo bones (this export uses the "mixamorig1:" namespace).
        string[] expected =
        {
            "mixamorig1:Hips", "mixamorig1:Spine", "mixamorig1:Spine1", "mixamorig1:Spine2",
            "mixamorig1:Neck", "mixamorig1:Head", "mixamorig1:LeftUpLeg",
            "mixamorig1:LeftHandIndex1", "mixamorig1:RightFoot", "mixamorig1:LeftShoulder",
        };
        foreach (var name in expected)
            Assert.True(scene.Skeleton.IndexOf(name) >= 0, $"missing bone {name}");

        Assert.Single(scene.Clips);
        var clip = scene.Clips[0];
        Assert.Equal(30f, clip.Fps);
        Assert.InRange(clip.FrameCount, 154, 156);

        Assert.Equal(1f, scene.UnitScaleCm, 3);
        Assert.Equal(1, scene.UpAxis);   // Y-up source, axes recorded but NOT converted
        Assert.Equal(1, scene.UpAxisSign);
    }

    // ---- 2/3. Zombie Crawl vs Blender ground truth --------------------------------------

    [Fact]
    public void ZombieCrawl_RestPose_MatchesBlender()
    {
        var scene = ImportFixture("Zombie Crawl.fbx");
        var truth = LoadTruth("zombie_crawl.json");

        int common = AssertWorldsMatch(
            scene.Skeleton,
            scene.Skeleton.RestWorld,
            truth.Bones.Select(b => (b.Name, b.RestWorldPos, b.RestWorldRot)),
            PosTolCm, RotTolDeg, "rest pose");

        Assert.True(common >= 60, $"only {common} common bones (truth has {truth.Bones.Count})");
    }

    [Fact]
    public void ZombieCrawl_Animation_MatchesBlender()
    {
        var scene = ImportFixture("Zombie Crawl.fbx");
        var truth = LoadTruth("zombie_crawl.json");
        var clip = scene.Clips[0];

        Assert.InRange(clip.FrameCount, truth.NSamples - 1, truth.NSamples + 1);

        foreach (int f in new[] { 0, 38, 77, 116, 154 })
        {
            var worlds = new Pose(clip.Frames[Math.Min(f, clip.FrameCount - 1)]).ToWorld(scene.Skeleton);
            var truthFrame = truth.Frames[f];
            int common = AssertWorldsMatch(
                scene.Skeleton,
                worlds,
                truth.Bones.Select((b, i) => (
                    b.Name,
                    FramePosToFbx(truthFrame[i].WorldPos, truth.ObjectScale),
                    FrameRotToFbx(truthFrame[i].WorldRot))),
                PosTolCm, RotTolDeg, $"frame {f}");
            Assert.True(common >= 60, $"frame {f}: only {common} common bones");
        }
    }

    // ---- 4. Lead Jab (citizen-skeleton clip) vs Blender ground truth ---------------------

    [Fact]
    public void LeadJab_RestPose_MatchesBlender()
    {
        var scene = ImportFixture("Lead Jab.fbx");
        var truth = LoadTruth("lead_jab.json");

        int common = AssertWorldsMatch(
            scene.Skeleton,
            scene.Skeleton.RestWorld,
            truth.Bones.Select(b => (b.Name, b.RestWorldPos, b.RestWorldRot)),
            PosTolCm, RotTolDeg, "rest pose");

        Assert.True(common >= 75, $"only {common} common bones (truth has {truth.Bones.Count})");
    }

    [Fact]
    public void LeadJab_Animation_MatchesBlender()
    {
        var scene = ImportFixture("Lead Jab.fbx");
        var truth = LoadTruth("lead_jab.json");
        Assert.NotEmpty(scene.Clips);
        var clip = scene.Clips[0];

        Assert.InRange(clip.FrameCount, truth.NSamples - 1, truth.NSamples + 1);

        foreach (int f in new[] { 0, 27, 54 })
        {
            var worlds = new Pose(clip.Frames[Math.Min(f, clip.FrameCount - 1)]).ToWorld(scene.Skeleton);
            var truthFrame = truth.Frames[f];
            int common = AssertWorldsMatch(
                scene.Skeleton,
                worlds,
                truth.Bones.Select((b, i) => (
                    b.Name,
                    FramePosToFbx(truthFrame[i].WorldPos, truth.ObjectScale),
                    FrameRotToFbx(truthFrame[i].WorldRot))),
                PosTolCm, RotTolDeg, $"frame {f}");
            Assert.True(common >= 75, $"frame {f}: only {common} common bones");
        }
    }

    // ---- 5. Edge fixtures: parse without throwing, non-empty skeleton --------------------

    [Theory]
    [InlineData("prod_fix7400.fbx")]
    [InlineData("mut_strip-prerotation.fbx")]
    [InlineData("gen_inherit.fbx")]
    public void EdgeFixture_ImportsWithNonEmptySkeleton(string name)
    {
        var scene = ImportFixture(name);
        Assert.True(scene.Skeleton.Count > 0, "skeleton is empty");
    }

    // ---- 6. ActorCore -------------------------------------------------------------------

    [Fact]
    public void ActorCore_CatwalkLoop_ImportsFiniteSkeletonAndClip()
    {
        var scene = ImportFixture("catwalk-loop-378982.fbx");

        Assert.True(scene.Skeleton.IndexOf("CC_Base_Hip") >= 0, "missing CC_Base_Hip");
        Assert.True(scene.Skeleton.IndexOf("CC_Base_L_Thigh") >= 0, "missing CC_Base_L_Thigh");

        Assert.NotEmpty(scene.Clips);
        Assert.True(scene.Clips[0].FrameCount > 1, "clip has <= 1 frame");

        static void AssertFinite(XForm x, string what)
        {
            Assert.True(
                float.IsFinite(x.Pos.X) && float.IsFinite(x.Pos.Y) && float.IsFinite(x.Pos.Z) &&
                float.IsFinite(x.Rot.X) && float.IsFinite(x.Rot.Y) && float.IsFinite(x.Rot.Z) &&
                float.IsFinite(x.Rot.W),
                $"non-finite transform in {what}: {x}");
        }

        for (int i = 0; i < scene.Skeleton.Count; i++)
        {
            AssertFinite(scene.Skeleton[i].RestLocal, $"rest local {scene.Skeleton[i].Name}");
            AssertFinite(scene.Skeleton.RestWorld[i], $"rest world {scene.Skeleton[i].Name}");
        }

        foreach (var clip in scene.Clips)
            for (int f = 0; f < clip.FrameCount; f++)
                foreach (var x in clip.Frames[f])
                    AssertFinite(x, $"clip {clip.Name} frame {f}");
    }

    // ---- 7. UE Mannequin: static translation channels override bind rest geometry --------
    //
    // The UE animation FBX files carry rest data (BindPose/Lcl defaults) whose foot->ball
    // local offset disagrees with the clip's STATIC ball translation channels by ~7.9 deg on
    // both feet, while every other bone pair agrees to 0.000 deg (dev/verification/RESULTS.md
    // root-cause analysis). The animation plays the static channel value, so the rest must use
    // it — otherwise the canonical foot->ball direction is constantly ~7.9 deg off in output.

    [Theory]
    [InlineData("foot_l", "ball_l")]
    [InlineData("foot_r", "ball_r")]
    public void UeMannequin_StaticTranslationChannel_OverridesBindRest(string foot, string ball)
    {
        var scene = ImportFixture("ThirdPersonWalk.FBX");
        var skel = scene.Skeleton;

        int ballIdx = skel.IndexOf(ball);
        Assert.True(ballIdx >= 0, $"missing bone {ball}");
        Assert.Equal(skel.IndexOf(foot), skel[ballIdx].ParentIndex);

        Assert.NotEmpty(scene.Clips);
        var frame0 = scene.Clips[0].Frames[0];

        // The ball's translation channels are static, so its sampled frame-0 local translation
        // IS the channel value: the rest local must match it in direction (foot->ball, both
        // vectors live in the foot frame; ~7.94 deg apart before the fix) and in value.
        var restPos = skel[ballIdx].RestLocal.Pos;
        var animPos = frame0[ballIdx].Pos;

        float angleDeg = MathF.Acos(Math.Clamp(
            Vector3.Dot(Vector3.Normalize(restPos), Vector3.Normalize(animPos)),
            -1f, 1f)) * KDegPerRad;
        Assert.True(angleDeg < 0.5f,
            $"rest foot->ball direction is {angleDeg:F3} deg away from the animation's static geometry");
        Assert.True(Vector3.Distance(restPos, animPos) < 0.01f,
            $"rest local translation {restPos} != static channel value {animPos}");
    }

    [Fact]
    public void UeMannequin_SingleStack_ProducesNoImportNotes()
    {
        // One animation stack: there is nothing to disagree with, so the multi-stack
        // static-translation detection must stay silent.
        var scene = ImportFixture("ThirdPersonWalk.FBX");
        Assert.Empty(scene.Notes);
    }

    [Theory]
    [InlineData("foot_l", "ball_l")]
    [InlineData("foot_r", "ball_r")]
    public void UeMannequin_StaticOverride_EqualsFrame0SampledLocalTranslation(string foot, string ball)
    {
        // The invariant the override formula must satisfy regardless of ancestor scale:
        // a bone whose translation channels are static plays exactly one local translation,
        // so the rest local must equal the frame-0 sampled local translation.
        var scene = ImportFixture("ThirdPersonWalk.FBX");
        var skel = scene.Skeleton;

        int ballIdx = skel.IndexOf(ball);
        Assert.True(ballIdx >= 0, $"missing bone {ball}");
        Assert.Equal(skel.IndexOf(foot), skel[ballIdx].ParentIndex);

        var restPos = skel[ballIdx].RestLocal.Pos;
        var frame0Pos = scene.Clips[0].Frames[0][ballIdx].Pos;
        Assert.True(Vector3.Distance(restPos, frame0Pos) < 0.005f,
            $"rest local {restPos} != frame-0 sampled local {frame0Pos}");
    }

    [Fact]
    public void UeMannequin_VaryingTranslationChannels_DoNotTouchRest()
    {
        var scene = ImportFixture("ThirdPersonWalk.FBX");
        var skel = scene.Skeleton;
        int pelvis = skel.IndexOf("pelvis");
        Assert.True(pelvis >= 0, "missing pelvis");

        // The pelvis carries the walk trajectory (VARYING translation channels): its rest must
        // stay bind-derived — pinned at the UE Mannequin bind height (Z-up, cm) — and must NOT
        // equal the mid-stride frame-0 trajectory sample.
        Assert.InRange(skel.RestWorld[pelvis].Pos.Z, 96.7f, 96.8f);
        var frame0 = scene.Clips[0].Frames[0];
        Assert.NotEqual(frame0[pelvis].Pos, skel[pelvis].RestLocal.Pos);
    }
}
using HumanoidRetargeter.Core.Formats;
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Skeleton;
using Xunit;

namespace HumanoidRetargeter.Tests.Formats;

/// <summary>
/// Unity <c>.fbx.meta</c> sidecar support: the <see cref="UnityMeta.ParseClipAnimations"/>
/// line parser over the clipAnimations YAML subset, and the <see cref="UnityMeta.Slice"/>
/// native-frame → resampled-grid range mapping. The fixture
/// (fixtures/unity/synthetic_stance.fbx.meta) is SYNTHETIC, replicating the structure Unity
/// 2020+ writes (ModelImporter serializedVersion 20300, clipAnimations serializedVersion 16).
/// </summary>
public class UnityMetaTests
{
    private static string FixturePath(params string[] parts)
        => Path.Combine(new[] { AppContext.BaseDirectory, "fixtures" }.Concat(parts).ToArray());

    private static string FixtureText()
        => File.ReadAllText(FixturePath("unity", "synthetic_stance.fbx.meta"));

    // ---------------------------------------------------------------- parser

    [Fact]
    public void Parse_SyntheticUnityMeta_YieldsBothDefinitions()
    {
        var defs = UnityMeta.ParseClipAnimations(FixtureText());

        Assert.Equal(2, defs.Count);

        Assert.Equal("Idle", defs[0].Name);
        Assert.Equal("root|Animation", defs[0].TakeName);
        Assert.Equal(0f, defs[0].FirstFrame);
        Assert.Equal(20f, defs[0].LastFrame);
        Assert.True(defs[0].Loop); // loopTime: 1 (NOT the unrelated "loop: 0" field)

        // Quoted name unwrapped; fractional frames parsed.
        Assert.Equal("Idle Short", defs[1].Name);
        Assert.Equal("root|Animation", defs[1].TakeName);
        Assert.Equal(5.5f, defs[1].FirstFrame);
        Assert.Equal(6.5f, defs[1].LastFrame);
        Assert.False(defs[1].Loop); // loopTime: 0
    }

    [Fact]
    public void Parse_NestedEventListEntries_DoNotBleedIntoDefinitions()
    {
        // The first item carries an events list whose entries are themselves "- key: value"
        // lines at deeper indentation — they must neither start new definitions nor
        // overwrite the item's own fields.
        var defs = UnityMeta.ParseClipAnimations(FixtureText());
        Assert.Equal(2, defs.Count);
        Assert.Equal("Idle", defs[0].Name);
    }

    [Fact]
    public void Parse_InlineEmptyList_YieldsNoDefinitions()
        => Assert.Empty(UnityMeta.ParseClipAnimations(
            "ModelImporter:\n  animations:\n    clipAnimations: []\n    isReadable: 0\n"));

    [Fact]
    public void Parse_NoClipAnimationsKey_YieldsNoDefinitions()
        => Assert.Empty(UnityMeta.ParseClipAnimations(
            "fileFormatVersion: 2\nguid: abc\nTextureImporter:\n  mipmaps: 1\n"));

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("complete garbage \0 {{{{ ---")]
    [InlineData("clipAnimations:")]
    [InlineData("clipAnimations:\n- name: x")] // dash at indent 0 < key indent is malformed → tolerated
    public void Parse_DegenerateInput_NeverThrows(string? text)
    {
        var defs = UnityMeta.ParseClipAnimations(text);
        Assert.NotNull(defs);
    }

    [Fact]
    public void Parse_MissingFields_KeepDefaults()
    {
        var defs = UnityMeta.ParseClipAnimations(
            "  clipAnimations:\n  - name: OnlyName\n");

        var def = Assert.Single(defs);
        Assert.Equal("OnlyName", def.Name);
        Assert.Equal("", def.TakeName);
        Assert.Equal(0f, def.FirstFrame);
        Assert.True(float.IsPositiveInfinity(def.LastFrame)); // "to the end of the take"
        Assert.False(def.Loop);
    }

    [Fact]
    public void Parse_ListEndsAtSiblingKey()
    {
        var defs = UnityMeta.ParseClipAnimations(
            "  clipAnimations:\n  - name: A\n    lastFrame: 3\n  isReadable: 0\n  - name: NotAClip\n");
        var def = Assert.Single(defs);
        Assert.Equal("A", def.Name);
    }

    [Fact]
    public void Parse_NoiseOnlyItems_AreDropped()
    {
        var defs = UnityMeta.ParseClipAnimations(
            "  clipAnimations:\n  - serializedVersion: 16\n    wrapMode: 0\n  - name: Real\n    lastFrame: 2\n");
        var def = Assert.Single(defs);
        Assert.Equal("Real", def.Name);
    }

    // ---------------------------------------------------------------- slicing math

    /// <summary>Clip whose frame f carries Pos.X = f on its single bone — slice contents
    /// are then directly checkable.</summary>
    private static Clip IndexClip(int frameCount, float fps, float nativeFps)
    {
        var frames = new List<XForm[]>(frameCount);
        for (var f = 0; f < frameCount; f++)
            frames.Add(new[] { new XForm(new System.Numerics.Vector3(f, 0, 0), System.Numerics.Quaternion.Identity) });
        return new Clip("take", fps, looping: false, frames, nativeFps);
    }

    private static ExternalClipDef Def(float first, float last, bool loop = false, string name = "part")
        => new() { Name = name, FirstFrame = first, LastFrame = last, Loop = loop };

    [Fact]
    public void Slice_SameNativeAndSampleRate_UsesFrameIndicesDirectly()
    {
        var source = IndexClip(44, fps: 30f, nativeFps: 30f);

        var full = UnityMeta.Slice(source, Def(0, 43));
        Assert.Equal(44, full.FrameCount);

        var pose = UnityMeta.Slice(source, Def(0, 1));
        Assert.Equal(2, pose.FrameCount);
        Assert.Equal(0f, pose.Frames[0][0].Pos.X);
        Assert.Equal(1f, pose.Frames[1][0].Pos.X);
    }

    [Fact]
    public void Slice_RescalesNativeFramesOntoSampleGrid()
    {
        // 24 fps native take of 54 native intervals resampled at 30 fps:
        // round(54/24*30)+1 = 69 samples. Native frame n maps to sample round(n*30/24).
        var source = IndexClip(69, fps: 30f, nativeFps: 24f);

        var sliced = UnityMeta.Slice(source, Def(8, 43));
        Assert.Equal(10f, sliced.Frames[0][0].Pos.X);  // round(8 * 1.25) = 10
        Assert.Equal(54f, sliced.Frames[^1][0].Pos.X); // round(43 * 1.25) = 54 (53.75 rounds up)
        Assert.Equal(45, sliced.FrameCount);
    }

    [Fact]
    public void Slice_ClampsRangeIntoClip()
    {
        var source = IndexClip(44, fps: 30f, nativeFps: 30f);

        var past = UnityMeta.Slice(source, Def(100, 999));
        Assert.Equal(1, past.FrameCount);
        Assert.Equal(43f, past.Frames[0][0].Pos.X);

        var overrun = UnityMeta.Slice(source, Def(40, 999));
        Assert.Equal(4, overrun.FrameCount);
        Assert.Equal(43f, overrun.Frames[^1][0].Pos.X);
    }

    [Fact]
    public void Slice_SingleFrameRange_YieldsOneFrame()
    {
        var source = IndexClip(44, fps: 30f, nativeFps: 30f);
        var sliced = UnityMeta.Slice(source, Def(5, 5));
        Assert.Equal(1, sliced.FrameCount);
        Assert.Equal(5f, sliced.Frames[0][0].Pos.X);
    }

    [Fact]
    public void Slice_InvertedRange_CollapsesToFirstFrame()
    {
        var source = IndexClip(44, fps: 30f, nativeFps: 30f);
        var sliced = UnityMeta.Slice(source, Def(10, 2));
        Assert.Equal(1, sliced.FrameCount);
        Assert.Equal(10f, sliced.Frames[0][0].Pos.X);
    }

    [Fact]
    public void Slice_MissingLastFrame_RunsToTheEnd()
    {
        var source = IndexClip(44, fps: 30f, nativeFps: 30f);
        var sliced = UnityMeta.Slice(source, new ExternalClipDef { Name = "tail", FirstFrame = 30 });
        Assert.Equal(14, sliced.FrameCount);
        Assert.Equal(43f, sliced.Frames[^1][0].Pos.X);
    }

    [Fact]
    public void Slice_CarriesNameLoopAndRates()
    {
        var source = IndexClip(44, fps: 30f, nativeFps: 24f);
        var sliced = UnityMeta.Slice(source, Def(0, 5, loop: true, name: "walk cycle"));

        Assert.Equal("walk cycle", sliced.Name); // sanitization happens pipeline-side
        Assert.True(sliced.Looping);
        Assert.Equal(30f, sliced.Fps);
        Assert.Equal(24f, sliced.NativeFps);

        var named = UnityMeta.Slice(source, Def(0, 5), "explicit");
        Assert.Equal("explicit", named.Name);
    }
}
using HumanoidRetargeter.Core.Mapping;
using Xunit;

namespace HumanoidRetargeter.Tests.Mapping;

public class ProfileTests
{
    private static readonly BoneRole[] FingerProxMidDistBothSides = BuildFingerRoles();

    private static BoneRole[] BuildFingerRoles()
    {
        var fingers = new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" };
        var segments = new[] { "Prox", "Mid", "Dist" };
        var sides = new[] { "L", "R" };
        return (
            from finger in fingers
            from segment in segments
            from side in sides
            select Enum.Parse<BoneRole>(finger + segment + side)).ToArray();
    }

    // ---------------------------------------------------------------- detection: mixamo

    [Fact]
    public void Detect_ZombieCrawl_PicksMixamoWithHighConfidence()
    {
        var skeleton = MappingFixtures.LoadZombieCrawl();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("mixamo", profile.Name);
        Assert.Equal("mixamo", result.ProfileName);
        Assert.Equal(MappingSource.Preset, result.Source);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        Assert.True(result.RoleToBone.Count >= 20, $"Only {result.RoleToBone.Count} roles mapped");

        AssertRole(skeleton, result, BoneRole.Hips, "mixamorig1:Hips");
        AssertRole(skeleton, result, BoneRole.Spine0, "mixamorig1:Spine");
        AssertRole(skeleton, result, BoneRole.Spine1, "mixamorig1:Spine1");
        AssertRole(skeleton, result, BoneRole.Spine2, "mixamorig1:Spine2");
        AssertRole(skeleton, result, BoneRole.Neck, "mixamorig1:Neck");
        AssertRole(skeleton, result, BoneRole.Head, "mixamorig1:Head");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "mixamorig1:LeftShoulder");
        AssertRole(skeleton, result, BoneRole.UpperArmL, "mixamorig1:LeftArm");
        AssertRole(skeleton, result, BoneRole.LowerArmL, "mixamorig1:LeftForeArm");
        AssertRole(skeleton, result, BoneRole.HandL, "mixamorig1:LeftHand");
        AssertRole(skeleton, result, BoneRole.IndexProxL, "mixamorig1:LeftHandIndex1");
        AssertRole(skeleton, result, BoneRole.MiddleDistR, "mixamorig1:RightHandMiddle3");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "mixamorig1:LeftUpLeg");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "mixamorig1:RightLeg");
        AssertRole(skeleton, result, BoneRole.FootL, "mixamorig1:LeftFoot");
        AssertRole(skeleton, result, BoneRole.ToeR, "mixamorig1:RightToeBase");

        // All 5 finger chains, both sides, all three phalanges.
        foreach (var role in FingerProxMidDistBothSides)
            Assert.True(result.RoleToBone.ContainsKey(role), $"Finger role {role} unmapped");
    }

    // ---------------------------------------------------------------- detection: actorcore

    [Fact]
    public void Detect_ActorCore_PicksActorCorePreset()
    {
        var skeleton = MappingFixtures.LoadActorCore();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("actorcore_cc", profile.Name);
        Assert.Equal(MappingSource.Preset, result.Source);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        Assert.True(result.RoleToBone.Count >= 20, $"Only {result.RoleToBone.Count} roles mapped");

        // Empirical hips resolution: CC_Base_Hip is the parent of both CC_Base_Pelvis (leg
        // branch) and CC_Base_Waist (spine branch) — it is the LCA of legs+spine and the
        // translating root, so it carries the Hips role; CC_Base_Pelvis stays unmapped.
        AssertRole(skeleton, result, BoneRole.Hips, "CC_Base_Hip");
        AssertRole(skeleton, result, BoneRole.Spine0, "CC_Base_Waist");
        AssertRole(skeleton, result, BoneRole.Spine1, "CC_Base_Spine01");
        AssertRole(skeleton, result, BoneRole.Spine2, "CC_Base_Spine02");
        AssertRole(skeleton, result, BoneRole.Neck, "CC_Base_NeckTwist01");
        AssertRole(skeleton, result, BoneRole.Head, "CC_Base_Head");
        AssertRole(skeleton, result, BoneRole.ClavicleR, "CC_Base_R_Clavicle");
        AssertRole(skeleton, result, BoneRole.UpperArmL, "CC_Base_L_Upperarm");
        AssertRole(skeleton, result, BoneRole.LowerArmR, "CC_Base_R_Forearm");
        AssertRole(skeleton, result, BoneRole.HandL, "CC_Base_L_Hand");
        AssertRole(skeleton, result, BoneRole.ThumbProxL, "CC_Base_L_Thumb1");
        AssertRole(skeleton, result, BoneRole.MiddleMidR, "CC_Base_R_Mid2");
        AssertRole(skeleton, result, BoneRole.PinkyDistL, "CC_Base_L_Pinky3");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "CC_Base_L_Thigh");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "CC_Base_R_Calf");
        AssertRole(skeleton, result, BoneRole.FootL, "CC_Base_L_Foot");
        AssertRole(skeleton, result, BoneRole.ToeL, "CC_Base_L_ToeBase");

        foreach (var role in FingerProxMidDistBothSides)
            Assert.True(result.RoleToBone.ContainsKey(role), $"Finger role {role} unmapped");

        // ShareBone helpers and limb twist bones must never be mapped. The single allowed
        // "Twist" name is CC_Base_NeckTwist01, which IS the rig's neck bone.
        foreach (var (role, boneIndex) in result.RoleToBone)
        {
            var name = skeleton[boneIndex].Name;
            Assert.False(name.Contains("ShareBone"), $"{role} mapped to share bone {name}");
            if (name.Contains("Twist"))
                Assert.True(role == BoneRole.Neck && name == "CC_Base_NeckTwist01",
                    $"{role} mapped to twist bone {name}");
        }
    }

    // ---------------------------------------------------------------- detection: UE

    [Fact]
    public void Detect_SyntheticUeMannequin_PicksUePreset()
    {
        var skeleton = MappingFixtures.BuildUeMannequin();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("ue_mannequin", profile.Name);
        Assert.True(result.Confidence >= 0.8f, $"Confidence {result.Confidence} < 0.8");

        AssertRole(skeleton, result, BoneRole.Hips, "pelvis");
        AssertRole(skeleton, result, BoneRole.Spine0, "spine_01");
        AssertRole(skeleton, result, BoneRole.Spine4, "spine_05");
        AssertRole(skeleton, result, BoneRole.Neck, "neck_01");
        AssertRole(skeleton, result, BoneRole.Head, "head");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "clavicle_l");
        AssertRole(skeleton, result, BoneRole.UpperArmR, "upperarm_r");
        AssertRole(skeleton, result, BoneRole.LowerArmL, "lowerarm_l");
        AssertRole(skeleton, result, BoneRole.HandR, "hand_r");
        AssertRole(skeleton, result, BoneRole.ThumbProxL, "thumb_01_l");
        AssertRole(skeleton, result, BoneRole.IndexMetaL, "index_metacarpal_l");
        AssertRole(skeleton, result, BoneRole.RingDistR, "ring_03_r");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "thigh_l");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "calf_r");
        AssertRole(skeleton, result, BoneRole.FootL, "foot_l");
        AssertRole(skeleton, result, BoneRole.ToeL, "ball_l");

        // *_twist_* bones are never mapped.
        foreach (var (role, boneIndex) in result.RoleToBone)
            Assert.DoesNotContain("_twist_", skeleton[boneIndex].Name);
    }

    // ---------------------------------------------------------------- detection: rokoko/bvh

    [Fact]
    public void Detect_SyntheticRokokoBvhNames_PicksRokokoPreset()
    {
        var bones = new List<(string, string?, System.Numerics.Vector3)>
        {
            ("Hips", null, new(0, 98, 0)),
            ("Spine", "Hips", new(0, 108, 0)),
            ("Spine1", "Spine", new(0, 120, 0)),
            ("Spine2", "Spine1", new(0, 132, 0)),
            ("Spine3", "Spine2", new(0, 142, 0)),
            ("Neck", "Spine3", new(0, 150, 0)),
            ("Head", "Neck", new(0, 158, 0)),
        };
        foreach (var (side, x) in new[] { ("Left", 1f), ("Right", -1f) })
        {
            bones.Add(($"{side}Shoulder", "Spine3", new(5 * x, 146, 0)));
            bones.Add(($"{side}Arm", $"{side}Shoulder", new(18 * x, 144, 0)));
            bones.Add(($"{side}ForeArm", $"{side}Arm", new(44 * x, 144, 0)));
            bones.Add(($"{side}Hand", $"{side}ForeArm", new(66 * x, 144, 0)));
            bones.Add(($"{side}UpLeg", "Hips", new(9 * x, 92, 0)));
            bones.Add(($"{side}Leg", $"{side}UpLeg", new(9 * x, 50, 0)));
            bones.Add(($"{side}Foot", $"{side}Leg", new(9 * x, 9, 0)));
            bones.Add(($"{side}Toe", $"{side}Foot", new(9 * x, 2, 12)));
        }
        var skeleton = MappingFixtures.FromWorldPositions(bones);

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("rokoko_bvh", profile.Name);
        AssertRole(skeleton, result, BoneRole.Hips, "Hips");
        AssertRole(skeleton, result, BoneRole.Spine0, "Spine");
        AssertRole(skeleton, result, BoneRole.Spine3, "Spine3");
        AssertRole(skeleton, result, BoneRole.UpperArmL, "LeftArm");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "RightLeg");
        AssertRole(skeleton, result, BoneRole.ToeL, "LeftToe");
    }

    // ---------------------------------------------------------------- scoring honesty

    [Fact]
    public void Score_GenericCoreNamesOnly_NoPresetReachesThreshold()
    {
        // Only the generic center names (Hips/Spine1/Spine2/Neck/Head) match any preset;
        // every limb is named outside all alias families. Rich center matches alone must
        // never carry a preset over the detection threshold.
        var bones = new List<(string, string?, System.Numerics.Vector3)>
        {
            ("Hips", null, new(0, 98, 0)),
            ("Spine1", "Hips", new(0, 110, 0)),
            ("Spine2", "Spine1", new(0, 124, 0)),
            ("Neck", "Spine2", new(0, 148, 0)),
            ("Head", "Neck", new(0, 156, 0)),
        };
        foreach (var (side, x) in new[] { ("A", 1f), ("B", -1f) })
        {
            bones.Add(($"limb_{side}_1", "Spine2", new(18 * x, 144, 0)));
            bones.Add(($"limb_{side}_2", $"limb_{side}_1", new(44 * x, 144, 0)));
            bones.Add(($"limb_{side}_3", $"limb_{side}_2", new(66 * x, 144, 0)));
            bones.Add(($"limb_{side}_4", "Hips", new(9 * x, 92, 0)));
            bones.Add(($"limb_{side}_5", $"limb_{side}_4", new(9 * x, 50, 0)));
            bones.Add(($"limb_{side}_6", $"limb_{side}_5", new(9 * x, 9, 0)));
        }
        var skeleton = MappingFixtures.FromWorldPositions(bones);

        foreach (var profile in ProfileLibrary.All)
        {
            var score = ProfileDetector.Score(profile, skeleton);
            Assert.True(score < ProfileDetector.DetectionThreshold,
                $"{profile.Name} scored {score} on a rig with no matching limb names");
        }
    }

    // ---------------------------------------------------------------- detection: negative

    [Fact]
    public void Detect_UnknownNames_ReturnsNull()
    {
        var skeleton = MappingFixtures.RenameAll(MappingFixtures.LoadZombieCrawl(), i => $"bone_{i:000}");
        Assert.Null(ProfileDetector.Detect(skeleton));
    }

    [Fact]
    public void Score_MixamoProfileOnZombieCrawl_BeatsOtherPresets()
    {
        var skeleton = MappingFixtures.LoadZombieCrawl();
        var mixamo = ProfileDetector.Score(ProfileLibrary.Mixamo, skeleton);
        Assert.True(mixamo >= 0.9f);
        Assert.True(mixamo > ProfileDetector.Score(ProfileLibrary.ActorCoreCc, skeleton));
        Assert.True(mixamo > ProfileDetector.Score(ProfileLibrary.UeMannequin, skeleton));
        Assert.True(mixamo > ProfileDetector.Score(ProfileLibrary.RokokoBvh, skeleton));
    }

    // ---------------------------------------------------------------- json (de)serialization

    [Fact]
    public void Profile_JsonRoundTrips()
    {
        foreach (var profile in ProfileLibrary.All)
        {
            var json = profile.ToJson();
            var restored = Profile.FromJson(json);

            Assert.Equal(profile.Name, restored.Name);
            Assert.Equal(profile.NamespacePatterns, restored.NamespacePatterns);
            Assert.Equal(profile.Aliases.Count, restored.Aliases.Count);
            foreach (var (role, aliases) in profile.Aliases)
                Assert.Equal(aliases, restored.Aliases[role]);
        }
    }

    [Fact]
    public void Profile_UserPresetShape_V1RoundTrips()
    {
        // The exact document shape the editor's UserPresets.Save produces: name
        // "user_<sig8>", no namespace patterns, ONE alias per role - the literal source
        // bone name. This is the v1 user-preset contract; FromJson must keep reading it.
        var skeleton = MappingFixtures.LoadZombieCrawl();
        var aliases = new Dictionary<BoneRole, string[]>
        {
            [BoneRole.Hips] = new[] { "mixamorig1:Hips" },
            [BoneRole.Spine0] = new[] { "mixamorig1:Spine" },
            [BoneRole.Head] = new[] { "mixamorig1:Head" },
            [BoneRole.HandL] = new[] { "mixamorig1:LeftHand" },
            [BoneRole.PinkyDistR] = new[] { "mixamorig1:RightHandPinky3" },
        };
        var profile = new Profile("user_a1b2c3d4", Array.Empty<string>(), aliases);

        var json = profile.ToJson();
        Assert.Contains("\"v\": 1", json);
        var restored = Profile.FromJson(json);

        Assert.Equal(profile.Name, restored.Name);
        Assert.Empty(restored.NamespacePatterns);
        Assert.Equal(aliases.Count, restored.Aliases.Count);
        foreach (var (role, names) in aliases)
            Assert.Equal(names, restored.Aliases[role]);

        // And the restored preset actually resolves on the rig it was saved from.
        var applied = ProfileDetector.Apply(restored, skeleton);
        foreach (var (role, names) in aliases)
            Assert.Equal(names[0], skeleton[applied.RoleToBone[role]].Name);
    }

    [Fact]
    public void PresetAssetsFolder_HasNoOrphanProfiles()
    {
        // Every committed (non-user) profile JSON must correspond to a shipped preset -
        // an orphan file would silently diverge from ProfileLibrary.
        var dir = FindRepoFile(Path.Combine("Assets", "data", "humanoid_retargeter", "profiles"));
        Assert.True(Directory.Exists(dir), $"profiles folder missing: {dir}");

        var known = ProfileLibrary.All.Select(p => p.Name).ToHashSet(StringComparer.Ordinal);
        var orphans = Directory.GetFiles(dir, "*.json", SearchOption.TopDirectoryOnly)
            .Select(Path.GetFileNameWithoutExtension)
            .Where(name => !known.Contains(name!))
            .ToList();
        Assert.True(orphans.Count == 0,
            $"Orphan profile JSONs without a ProfileLibrary preset: {string.Join(", ", orphans)}");
    }

    [Fact]
    public void Profile_FromJson_RejectsUnknownSchemaVersion()
    {
        var json = """{"v": 99, "name": "x", "namespace_patterns": [], "aliases": {}}""";
        Assert.Throws<ArgumentException>(() => Profile.FromJson(json));
    }

    [Fact]
    public void Profile_FromJson_RejectsUnknownRole()
    {
        var json = """{"v": 1, "name": "x", "namespace_patterns": [], "aliases": {"NotARole": ["a"]}}""";
        Assert.Throws<ArgumentException>(() => Profile.FromJson(json));
    }

    // ---------------------------------------------------------------- preset asset regeneration

    [Fact]
    public void PresetJsons_MatchCommittedAssets()
    {
        foreach (var profile in ProfileLibrary.All)
        {
            var generated = profile.ToJson();
            var assetPath = FindRepoFile(Path.Combine(
                "Assets", "data", "humanoid_retargeter", "profiles", profile.Name + ".json"));

            if (!File.Exists(assetPath))
            {
                // Regenerate-and-diff pattern: first run writes the committed artifact.
                Directory.CreateDirectory(Path.GetDirectoryName(assetPath)!);
                File.WriteAllText(assetPath, generated);
            }

            var committed = File.ReadAllText(assetPath);
            Assert.Equal(Normalize(committed), Normalize(generated));
        }
    }

    [Fact]
    public void PresetLibrary_HasExpectedProfiles()
    {
        Assert.Equal(
            new[]
            {
                "sbox", "mixamo", "actorcore_cc", "ue_mannequin", "xsens_mvn", "perception_neuron",
                "fight_night", "rokoko_bvh", "smpl_x", "smpl", "soma_bvh", "classic_bvh",
                "valvebiped", "biped", "daz_genesis", "daz_poser",
                "rigify", "vrm", "auto_rig_pro", "advanced_skeleton",
            },
            ProfileLibrary.All.Select(p => p.Name).ToArray());
    }

    // ---------------------------------------------------------------- detection: fight night

    /// <summary>
    /// The EA Fight Night boxer rig uses MotionBuilder/HumanIK joint names, so mixamo and
    /// perception_neuron both score respectably on it — the dedicated preset must win
    /// outright by covering the fourth spine bone, the toes AND declaring only the finger
    /// roles a gloved hand actually has.
    /// </summary>
    [Fact]
    public void Detect_SyntheticFightNight_PicksFightNightPreset()
    {
        var skeleton = MappingFixtures.BuildFightNight();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("fight_night", profile.Name);
        Assert.True(result.Confidence >= 0.99f, $"Confidence {result.Confidence} < 0.99");

        // It must beat the HumanIK-family presets that also match these names.
        var fightNight = ProfileDetector.Score(ProfileLibrary.FightNight, skeleton);
        Assert.True(fightNight > ProfileDetector.Score(ProfileLibrary.Mixamo, skeleton),
            "fight_night must outscore mixamo on a boxer rig");
        Assert.True(fightNight > ProfileDetector.Score(ProfileLibrary.PerceptionNeuron, skeleton),
            "fight_night must outscore perception_neuron on a boxer rig");

        AssertRole(skeleton, result, BoneRole.Hips, "Hips");
        AssertRole(skeleton, result, BoneRole.Spine0, "Spine");
        AssertRole(skeleton, result, BoneRole.Spine3, "Spine3");
        Assert.False(result.RoleToBone.ContainsKey(BoneRole.Spine4), "Spine4 role double-mapped");
        // Neck→Neck1→Head: the second neck bone stays unmapped.
        AssertRole(skeleton, result, BoneRole.Neck, "Neck");
        AssertRole(skeleton, result, BoneRole.Head, "Head");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "LeftShoulder");
        AssertRole(skeleton, result, BoneRole.UpperArmR, "RightArm");
        AssertRole(skeleton, result, BoneRole.LowerArmL, "LeftForeArm");
        AssertRole(skeleton, result, BoneRole.HandL, "LeftHand");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "LeftUpLeg");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "RightLeg");
        AssertRole(skeleton, result, BoneRole.FootL, "LeftFoot");
        AssertRole(skeleton, result, BoneRole.ToeR, "RightToeBase");

        // Gloved hand: two thumb segments, and the middle finger's InHand metacarpal maps
        // to the Meta role (NOT a phalanx — that is the SOMA finger bug).
        AssertRole(skeleton, result, BoneRole.ThumbProxL, "LeftHandThumb1");
        AssertRole(skeleton, result, BoneRole.ThumbMidL, "LeftHandThumb2");
        AssertRole(skeleton, result, BoneRole.MiddleMetaL, "LeftInHandMiddle");
        AssertRole(skeleton, result, BoneRole.MiddleProxL, "LeftHandMiddle1");
        AssertRole(skeleton, result, BoneRole.MiddleMidR, "RightHandMiddle2");
    }

    /// <summary>
    /// Every decoy family on the boxer rig stays unmapped: the glove mesh shells (which
    /// carry finger NAMES), the root/trajectory bones above the hips, the second neck bone
    /// and the twist/muscle/jiggle simulation layer.
    /// </summary>
    [Fact]
    public void FightNight_GloveShellsRootAndSimulationBones_AreNeverMapped()
    {
        var skeleton = MappingFixtures.BuildFightNight();

        var result = ProfileDetector.Apply(ProfileLibrary.FightNight, skeleton);

        var mapped = result.RoleToBone.Values.Select(i => skeleton[i].Name).ToHashSet(StringComparer.Ordinal);
        foreach (var decoy in new[]
        {
            "LeftHandThumbGlove", "LeftHandIndexGlove", "LeftHandMiddleGlove",
            "LeftInHandRingGlove", "LeftHandPinkyGlove", "RightHandIndexGlove",
            "Reference", "AITrajectory", "Neck1", "Crotch",
            "LeftArmTwist", "LeftForeArmTwist", "LeftGloveTwist", "LeftUpLegTwist",
            "Muscle_Left_Bicep", "Offset_Muscle_Left_Delt", "Left_Elbow_Helper",
            "Left_Knee_Helper", "LeftFootEnd", "Spine2_Jiggle", "Gut_Center", "StomachFront",
        })
        {
            Assert.True(skeleton.IndexOf(decoy) >= 0, $"fixture missing decoy bone {decoy}");
            Assert.False(mapped.Contains(decoy), $"{decoy} must never carry a role");
        }

        // No index/ring/pinky roles exist on a gloved hand.
        foreach (var role in new[]
        {
            BoneRole.IndexProxL, BoneRole.IndexProxR, BoneRole.RingProxL,
            BoneRole.PinkyProxL, BoneRole.ThumbDistL, BoneRole.MiddleDistL,
        })
        {
            Assert.False(result.RoleToBone.ContainsKey(role), $"{role} must not be mapped");
        }
    }

    /// <summary>The boxer preset must not claim the rigs whose naming it shares.</summary>
    [Fact]
    public void FightNight_DoesNotOutscoreNativePresetsOnTheirOwnRigs()
    {
        var mixamo = MappingFixtures.LoadZombieCrawl();
        Assert.True(
            ProfileDetector.Score(ProfileLibrary.Mixamo, mixamo)
            > ProfileDetector.Score(ProfileLibrary.FightNight, mixamo),
            "fight_night must not outscore mixamo on a real Mixamo rig");
        Assert.Equal("mixamo", ProfileDetector.Detect(mixamo)!.Value.Profile.Name);

        var neuron = MappingFixtures.BuildPerceptionNeuron();
        Assert.True(
            ProfileDetector.Score(ProfileLibrary.PerceptionNeuron, neuron)
            > ProfileDetector.Score(ProfileLibrary.FightNight, neuron),
            "fight_night must not outscore perception_neuron on a Neuron rig");
        Assert.Equal("perception_neuron", ProfileDetector.Detect(neuron)!.Value.Profile.Name);
    }

    // ---------------------------------------------------------------- detection: valvebiped

    [Fact]
    public void Detect_SyntheticValveBiped_PicksValveBipedPreset()
    {
        var skeleton = MappingFixtures.BuildValveBiped();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("valvebiped", profile.Name);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        Assert.True(ProfileDetector.Score(ProfileLibrary.Biped, skeleton) < 0.8f,
            "the plain Biped preset must not claim a ValveBiped rig");
        Assert.True(ProfileDetector.Score(ProfileLibrary.Mixamo, skeleton) < 0.8f,
            "mixamo must not claim a ValveBiped rig");

        AssertRole(skeleton, result, BoneRole.Hips, "ValveBiped.Bip01_Pelvis");
        AssertRole(skeleton, result, BoneRole.Spine0, "ValveBiped.Bip01_Spine");
        AssertRole(skeleton, result, BoneRole.Spine2, "ValveBiped.Bip01_Spine2");
        // The stock chain skips Spine3: Spine4 is the chest and fills the Spine3 role.
        AssertRole(skeleton, result, BoneRole.Spine3, "ValveBiped.Bip01_Spine4");
        Assert.False(result.RoleToBone.ContainsKey(BoneRole.Spine4), "Spine4 role double-mapped");
        AssertRole(skeleton, result, BoneRole.Neck, "ValveBiped.Bip01_Neck1");
        AssertRole(skeleton, result, BoneRole.Head, "ValveBiped.Bip01_Head1");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "ValveBiped.Bip01_L_Clavicle");
        AssertRole(skeleton, result, BoneRole.UpperArmR, "ValveBiped.Bip01_R_UpperArm");
        AssertRole(skeleton, result, BoneRole.LowerArmL, "ValveBiped.Bip01_L_Forearm");
        AssertRole(skeleton, result, BoneRole.HandL, "ValveBiped.Bip01_L_Hand");
        AssertRole(skeleton, result, BoneRole.ThumbProxL, "ValveBiped.Bip01_L_Finger0");
        AssertRole(skeleton, result, BoneRole.ThumbDistL, "ValveBiped.Bip01_L_Finger02");
        AssertRole(skeleton, result, BoneRole.IndexProxL, "ValveBiped.Bip01_L_Finger1");
        AssertRole(skeleton, result, BoneRole.MiddleMidR, "ValveBiped.Bip01_R_Finger21");
        AssertRole(skeleton, result, BoneRole.PinkyDistR, "ValveBiped.Bip01_R_Finger42");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "ValveBiped.Bip01_L_Thigh");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "ValveBiped.Bip01_R_Calf");
        AssertRole(skeleton, result, BoneRole.FootL, "ValveBiped.Bip01_L_Foot");
        AssertRole(skeleton, result, BoneRole.ToeL, "ValveBiped.Bip01_L_Toe0");

        foreach (var role in FingerProxMidDistBothSides)
            Assert.True(result.RoleToBone.ContainsKey(role), $"Finger role {role} unmapped");

        // Attachment/helper bones carry no role.
        foreach (var (role, boneIndex) in result.RoleToBone)
        {
            var name = skeleton[boneIndex].Name;
            Assert.False(name == "ValveBiped.forward" || name.Contains("Anim_Attachment"),
                $"{role} mapped to helper {name}");
        }
    }

    // ---------------------------------------------------------------- detection: xsens mvn

    [Fact]
    public void Detect_SyntheticXsensMvn_PicksXsensPreset()
    {
        var skeleton = MappingFixtures.BuildXsensMvn();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("xsens_mvn", profile.Name);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        Assert.True(ProfileDetector.Score(ProfileLibrary.RokokoBvh, skeleton) < 0.8f,
            "rokoko_bvh must not claim an MVN rig (no 'Hips' bone)");

        AssertRole(skeleton, result, BoneRole.Hips, "Pelvis");
        AssertRole(skeleton, result, BoneRole.Spine0, "L5");
        AssertRole(skeleton, result, BoneRole.Spine1, "L3");
        AssertRole(skeleton, result, BoneRole.Spine2, "T12");
        AssertRole(skeleton, result, BoneRole.Spine3, "T8");
        AssertRole(skeleton, result, BoneRole.Neck, "Neck");
        AssertRole(skeleton, result, BoneRole.Head, "Head");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "LeftShoulder");
        AssertRole(skeleton, result, BoneRole.UpperArmR, "RightUpperArm");
        AssertRole(skeleton, result, BoneRole.LowerArmL, "LeftForeArm");
        AssertRole(skeleton, result, BoneRole.HandR, "RightHand");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "LeftUpperLeg");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "RightLowerLeg");
        AssertRole(skeleton, result, BoneRole.FootL, "LeftFoot");
        AssertRole(skeleton, result, BoneRole.ToeR, "RightToe");
    }

    // ---------------------------------------------------------------- detection: perception neuron

    [Fact]
    public void Detect_SyntheticPerceptionNeuron_PicksNeuronPreset()
    {
        var skeleton = MappingFixtures.BuildPerceptionNeuron();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("perception_neuron", profile.Name);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        // Mixamo matches most Neuron names but misses Spine3 and the (absent) toes; the
        // dedicated preset must outscore it.
        Assert.True(
            ProfileDetector.Score(ProfileLibrary.Mixamo, skeleton) < result.Confidence,
            "perception_neuron must outscore mixamo on a Neuron rig");

        AssertRole(skeleton, result, BoneRole.Hips, "Hips");
        AssertRole(skeleton, result, BoneRole.Spine0, "Spine");
        AssertRole(skeleton, result, BoneRole.Spine3, "Spine3");
        AssertRole(skeleton, result, BoneRole.Neck, "Neck");
        AssertRole(skeleton, result, BoneRole.Head, "Head");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "LeftShoulder");
        AssertRole(skeleton, result, BoneRole.UpperArmL, "LeftArm");
        AssertRole(skeleton, result, BoneRole.LowerArmR, "RightForeArm");
        AssertRole(skeleton, result, BoneRole.HandL, "LeftHand");
        AssertRole(skeleton, result, BoneRole.ThumbProxL, "LeftHandThumb1");
        AssertRole(skeleton, result, BoneRole.IndexProxL, "LeftHandIndex1");
        AssertRole(skeleton, result, BoneRole.MiddleMidR, "RightHandMiddle2");
        AssertRole(skeleton, result, BoneRole.PinkyDistR, "RightHandPinky3");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "LeftUpLeg");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "RightLeg");
        AssertRole(skeleton, result, BoneRole.FootL, "LeftFoot");

        foreach (var role in FingerProxMidDistBothSides)
            Assert.True(result.RoleToBone.ContainsKey(role), $"Finger role {role} unmapped");

        // The InHand metacarpal helpers must never be mapped (mapping them as phalanges
        // shifts every curl one joint outward — the SOMA finger bug class).
        foreach (var (role, boneIndex) in result.RoleToBone)
            Assert.DoesNotContain("InHand", skeleton[boneIndex].Name);
    }

    // ---------------------------------------------------------------- detection: daz genesis

    [Fact]
    public void Detect_SyntheticDazGenesis_PicksDazGenesisPreset()
    {
        var skeleton = MappingFixtures.BuildDazGenesis();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("daz_genesis", profile.Name);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        Assert.True(ProfileDetector.Score(ProfileLibrary.DazPoser, skeleton) < 0.8f,
            "daz_poser must not claim a Genesis 3/8 rig (lShldr vs lShldrBend)");

        // hip is the LCA of the pelvis leg branch and the abdomen spine branch — it is
        // the hips; pelvis stays unmapped (ActorCore Hip/Pelvis policy).
        AssertRole(skeleton, result, BoneRole.Hips, "hip");
        AssertRole(skeleton, result, BoneRole.Spine0, "abdomenLower");
        AssertRole(skeleton, result, BoneRole.Spine1, "abdomenUpper");
        AssertRole(skeleton, result, BoneRole.Spine2, "chestLower");
        AssertRole(skeleton, result, BoneRole.Spine3, "chestUpper");
        AssertRole(skeleton, result, BoneRole.Neck, "neckLower");
        AssertRole(skeleton, result, BoneRole.Head, "head");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "lCollar");
        AssertRole(skeleton, result, BoneRole.UpperArmL, "lShldrBend");
        AssertRole(skeleton, result, BoneRole.LowerArmR, "rForearmBend");
        AssertRole(skeleton, result, BoneRole.HandL, "lHand");
        AssertRole(skeleton, result, BoneRole.ThumbProxL, "lThumb1");
        AssertRole(skeleton, result, BoneRole.MiddleMidR, "rMid2");
        AssertRole(skeleton, result, BoneRole.PinkyDistL, "lPinky3");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "lThighBend");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "rShin");
        AssertRole(skeleton, result, BoneRole.FootL, "lFoot");
        AssertRole(skeleton, result, BoneRole.ToeL, "lToe");

        foreach (var role in FingerProxMidDistBothSides)
            Assert.True(result.RoleToBone.ContainsKey(role), $"Finger role {role} unmapped");

        // Twist roll helpers, the pelvis intermediate, neckUpper and the metatarsals
        // carry no role.
        foreach (var (role, boneIndex) in result.RoleToBone)
        {
            var name = skeleton[boneIndex].Name;
            Assert.DoesNotContain("Twist", name);
            Assert.False(name is "pelvis" or "neckUpper", $"{role} mapped to {name}");
            Assert.DoesNotContain("Metatarsals", name);
        }
    }

    // ---------------------------------------------------------------- detection: 3ds max biped

    [Fact]
    public void Detect_SyntheticBiped_PicksBipedPreset()
    {
        var skeleton = MappingFixtures.BuildBiped();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("biped", profile.Name);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        Assert.True(ProfileDetector.Score(ProfileLibrary.Mixamo, skeleton) < 0.8f,
            "mixamo must not claim a Biped rig");

        AssertRole(skeleton, result, BoneRole.Hips, "Bip01 Pelvis");
        AssertRole(skeleton, result, BoneRole.Spine0, "Bip01 Spine");
        AssertRole(skeleton, result, BoneRole.Spine3, "Bip01 Spine3");
        AssertRole(skeleton, result, BoneRole.Neck, "Bip01 Neck");
        AssertRole(skeleton, result, BoneRole.Head, "Bip01 Head");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "Bip01 L Clavicle");
        AssertRole(skeleton, result, BoneRole.UpperArmR, "Bip01 R UpperArm");
        AssertRole(skeleton, result, BoneRole.LowerArmL, "Bip01 L Forearm");
        AssertRole(skeleton, result, BoneRole.HandL, "Bip01 L Hand");
        // Numbered fingers: Finger0 chain is the thumb, segment digits are phalanges.
        AssertRole(skeleton, result, BoneRole.ThumbProxL, "Bip01 L Finger0");
        AssertRole(skeleton, result, BoneRole.ThumbDistL, "Bip01 L Finger02");
        AssertRole(skeleton, result, BoneRole.IndexProxL, "Bip01 L Finger1");
        AssertRole(skeleton, result, BoneRole.MiddleMidR, "Bip01 R Finger21");
        AssertRole(skeleton, result, BoneRole.PinkyDistR, "Bip01 R Finger42");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "Bip01 L Thigh");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "Bip01 R Calf");
        AssertRole(skeleton, result, BoneRole.FootL, "Bip01 L Foot");
        AssertRole(skeleton, result, BoneRole.ToeL, "Bip01 L Toe0");

        foreach (var role in FingerProxMidDistBothSides)
            Assert.True(result.RoleToBone.ContainsKey(role), $"Finger role {role} unmapped");

        // The COM root, Footsteps and toe segments carry no role.
        foreach (var (role, boneIndex) in result.RoleToBone)
        {
            var name = skeleton[boneIndex].Name;
            Assert.False(name is "Bip01" or "Bip01 Footsteps", $"{role} mapped to {name}");
            Assert.False(name.EndsWith("Toe01") || name.EndsWith("Toe02"), $"{role} mapped to toe segment {name}");
        }
    }

    [Fact]
    public void Detect_BipedUnderscoreNames_StillDetected()
    {
        // Some exporters mangle the Biped spaces to underscores (Bip001_L_Thigh); the
        // namespace pattern and separator-insensitive normalization must absorb both.
        var original = MappingFixtures.BuildBiped();
        var skeleton = MappingFixtures.RenameAll(
            original, i => original[i].Name.Replace("Bip01 ", "Bip001_").Replace(' ', '_'));

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        Assert.Equal("biped", detected.Value.Profile.Name);
        Assert.True(detected.Value.Result.Confidence >= 0.9f);
        AssertRole(skeleton, detected.Value.Result, BoneRole.UpperLegL, "Bip001_L_Thigh");
    }

    // ---------------------------------------------------------------- detection: daz / poser

    [Fact]
    public void Detect_SyntheticDazPoser_PicksDazPreset()
    {
        var skeleton = MappingFixtures.BuildDazPoser();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("daz_poser", profile.Name);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        Assert.True(ProfileDetector.Score(ProfileLibrary.Mixamo, skeleton) < 0.8f,
            "mixamo must not claim a DAZ/Poser rig");

        AssertRole(skeleton, result, BoneRole.Hips, "hip");
        AssertRole(skeleton, result, BoneRole.Spine0, "abdomen");
        AssertRole(skeleton, result, BoneRole.Spine1, "abdomen2");
        AssertRole(skeleton, result, BoneRole.Spine2, "chest");
        AssertRole(skeleton, result, BoneRole.Neck, "neck");
        AssertRole(skeleton, result, BoneRole.Head, "head");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "lCollar");
        AssertRole(skeleton, result, BoneRole.UpperArmL, "lShldr");
        AssertRole(skeleton, result, BoneRole.LowerArmR, "rForeArm");
        AssertRole(skeleton, result, BoneRole.HandL, "lHand");
        AssertRole(skeleton, result, BoneRole.ThumbProxL, "lThumb1");
        AssertRole(skeleton, result, BoneRole.MiddleMidR, "rMid2");
        AssertRole(skeleton, result, BoneRole.PinkyDistL, "lPinky3");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "lThigh");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "rShin");
        AssertRole(skeleton, result, BoneRole.FootL, "lFoot");
        AssertRole(skeleton, result, BoneRole.ToeL, "lToe");

        // Buttock thigh-helpers and eyes carry no role.
        foreach (var (role, boneIndex) in result.RoleToBone)
        {
            var name = skeleton[boneIndex].Name;
            Assert.DoesNotContain("Buttock", name);
            Assert.DoesNotContain("Eye", name);
        }
    }

    // ---------------------------------------------------------------- detection: rigify

    [Theory]
    [InlineData(false)] // metarig names: spine, spine.001.., upper_arm.L
    [InlineData(true)]  // generated deform skeleton: DEF-spine, DEF-upper_arm.L (+ .001 twins)
    public void Detect_SyntheticRigify_PicksRigifyPreset(bool defVariant)
    {
        var skeleton = MappingFixtures.BuildRigify(defVariant);

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("rigify", profile.Name);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        Assert.True(ProfileDetector.Score(ProfileLibrary.Mixamo, skeleton) < 0.8f,
            "mixamo must not claim a rigify rig");

        var d = defVariant ? "DEF-" : string.Empty;
        // rigify's "spine" IS the pelvis bone (rigify/metarigs/human.py); spine.004 is
        // the first neck bone, spine.006 the head. spine.005 stays unmapped.
        AssertRole(skeleton, result, BoneRole.Hips, $"{d}spine");
        AssertRole(skeleton, result, BoneRole.Spine0, $"{d}spine.001");
        AssertRole(skeleton, result, BoneRole.Spine2, $"{d}spine.003");
        AssertRole(skeleton, result, BoneRole.Neck, $"{d}spine.004");
        AssertRole(skeleton, result, BoneRole.Head, $"{d}spine.006");
        Assert.DoesNotContain(skeleton.Bones.First(b => b.Name == $"{d}spine.005").Index,
            result.RoleToBone.Values);
        AssertRole(skeleton, result, BoneRole.ClavicleL, $"{d}shoulder.L");
        AssertRole(skeleton, result, BoneRole.UpperArmL, $"{d}upper_arm.L");
        AssertRole(skeleton, result, BoneRole.LowerArmR, $"{d}forearm.R");
        AssertRole(skeleton, result, BoneRole.HandL, $"{d}hand.L");
        AssertRole(skeleton, result, BoneRole.ThumbProxL, $"{d}thumb.01.L");
        AssertRole(skeleton, result, BoneRole.IndexProxL, $"{d}f_index.01.L");
        AssertRole(skeleton, result, BoneRole.RingDistR, $"{d}f_ring.03.R");
        AssertRole(skeleton, result, BoneRole.UpperLegL, $"{d}thigh.L");
        AssertRole(skeleton, result, BoneRole.LowerLegR, $"{d}shin.R");
        AssertRole(skeleton, result, BoneRole.FootL, $"{d}foot.L");
        AssertRole(skeleton, result, BoneRole.ToeL, $"{d}toe.L");

        foreach (var role in FingerProxMidDistBothSides)
            Assert.True(result.RoleToBone.ContainsKey(role), $"Finger role {role} unmapped");

        // pelvis.L/R, palm, heel helpers and segmented DEF twins carry no role.
        foreach (var (role, boneIndex) in result.RoleToBone)
        {
            var name = skeleton[boneIndex].Name;
            Assert.False(name.StartsWith("pelvis.") || name.StartsWith("palm.") || name.StartsWith("heel."),
                $"{role} mapped to helper {name}");
            Assert.False(name.EndsWith(".001") && name.StartsWith("DEF-") && !name.Contains("spine"),
                $"{role} mapped to segmented deform twin {name}");
        }
    }

    // ---------------------------------------------------------------- detection: vrm / vroid

    [Fact]
    public void Detect_SyntheticVrm_PicksVrmPreset()
    {
        var skeleton = MappingFixtures.BuildVrm();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("vrm", profile.Name);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        Assert.True(ProfileDetector.Score(ProfileLibrary.Mixamo, skeleton) < 0.8f,
            "mixamo must not claim a VRM rig");

        AssertRole(skeleton, result, BoneRole.Hips, "J_Bip_C_Hips");
        AssertRole(skeleton, result, BoneRole.Spine0, "J_Bip_C_Spine");
        AssertRole(skeleton, result, BoneRole.Spine1, "J_Bip_C_Chest");
        AssertRole(skeleton, result, BoneRole.Spine2, "J_Bip_C_UpperChest");
        AssertRole(skeleton, result, BoneRole.Neck, "J_Bip_C_Neck");
        AssertRole(skeleton, result, BoneRole.Head, "J_Bip_C_Head");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "J_Bip_L_Shoulder");
        AssertRole(skeleton, result, BoneRole.UpperArmL, "J_Bip_L_UpperArm");
        AssertRole(skeleton, result, BoneRole.LowerArmR, "J_Bip_R_LowerArm");
        AssertRole(skeleton, result, BoneRole.HandL, "J_Bip_L_Hand");
        AssertRole(skeleton, result, BoneRole.ThumbProxL, "J_Bip_L_Thumb1");
        AssertRole(skeleton, result, BoneRole.MiddleMidR, "J_Bip_R_Middle2");
        // VRM names the pinky "Little" (littleProximal.. humanoid bones).
        AssertRole(skeleton, result, BoneRole.PinkyDistL, "J_Bip_L_Little3");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "J_Bip_L_UpperLeg");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "J_Bip_R_LowerLeg");
        AssertRole(skeleton, result, BoneRole.FootL, "J_Bip_L_Foot");
        AssertRole(skeleton, result, BoneRole.ToeL, "J_Bip_L_ToeBase");

        foreach (var role in FingerProxMidDistBothSides)
            Assert.True(result.RoleToBone.ContainsKey(role), $"Finger role {role} unmapped");

        // Root and the secondary physics/adjust bones carry no role.
        foreach (var (role, boneIndex) in result.RoleToBone)
        {
            var name = skeleton[boneIndex].Name;
            Assert.False(name == "Root" || name.StartsWith("J_Sec_") || name.StartsWith("J_Adj_"),
                $"{role} mapped to non-humanoid bone {name}");
        }
    }

    // ---------------------------------------------------------------- detection: auto-rig pro

    [Fact]
    public void Detect_SyntheticAutoRigPro_PicksAutoRigProPreset()
    {
        var skeleton = MappingFixtures.BuildAutoRigPro();

        var detected = ProfileDetector.Detect(skeleton);

        Assert.NotNull(detected);
        var (profile, result) = detected.Value;
        Assert.Equal("auto_rig_pro", profile.Name);
        Assert.True(result.Confidence >= 0.9f, $"Confidence {result.Confidence} < 0.9");
        Assert.True(ProfileDetector.Score(ProfileLibrary.Mixamo, skeleton) < 0.8f,
            "mixamo must not claim an Auto-Rig Pro rig");

        // root.x is the hips (the ground bone "root" carries no role).
        AssertRole(skeleton, result, BoneRole.Hips, "root.x");
        AssertRole(skeleton, result, BoneRole.Spine0, "spine_01.x");
        AssertRole(skeleton, result, BoneRole.Spine2, "spine_03.x");
        AssertRole(skeleton, result, BoneRole.Neck, "neck.x");
        AssertRole(skeleton, result, BoneRole.Head, "head.x");
        AssertRole(skeleton, result, BoneRole.ClavicleL, "shoulder.l");
        AssertRole(skeleton, result, BoneRole.UpperArmL, "arm_stretch.l");
        AssertRole(skeleton, result, BoneRole.LowerArmR, "forearm_stretch.r");
        AssertRole(skeleton, result, BoneRole.HandL, "hand.l");
        AssertRole(skeleton, result, BoneRole.ThumbProxL, "c_thumb1.l");
        AssertRole(skeleton, result, BoneRole.MiddleMidR, "c_middle2.r");
        AssertRole(skeleton, result, BoneRole.PinkyDistL, "c_pinky3.l");
        AssertRole(skeleton, result, BoneRole.UpperLegL, "thigh_stretch.l");
        AssertRole(skeleton, result, BoneRole.LowerLegR, "leg_stretch.r");
        AssertRole(skeleton, result, BoneRole.FootL, "foot.l");
        AssertRole(skeleton, result, BoneRole.ToeL, "toes_01.l");

        foreach (var role in FingerProxMidDistBothSides)
            Assert.True(result.RoleToBone.ContainsKey(role), $"Finger role {role} unmapped");

        // The ground bone and the leftover mixamorig finger-tip markers carry no role.
        foreach (var (role, boneIndex) in result.RoleToBone)
        {
            var name = skeleton[boneIndex].Name;
            Assert.False(name == "root" || name.StartsWith("mixamorig:"),
                $"{role} mapped to non-deform bone {name}");
        }
    }

    // ---------------------------------------------------------------- helpers

    private static void AssertRole(
        HumanoidRetargeter.Core.Skeleton.Skeleton skeleton, MappingResult result, BoneRole role, string expectedBone)
    {
        Assert.True(result.RoleToBone.TryGetValue(role, out var index), $"Role {role} unmapped");
        Assert.Equal(expectedBone, skeleton[index].Name);
    }

    private static string Normalize(string text) => text.Replace("\r\n", "\n").TrimEnd('\n');

    private static string FindRepoFile(string relativePath)
    {
        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir is not null)
        {
            if (File.Exists(Path.Combine(dir.FullName, "humanoid-retargeter.sbproj")))
                return Path.Combine(dir.FullName, relativePath);
            dir = dir.Parent;
        }
        throw new InvalidOperationException("Repo root (humanoid-retargeter.sbproj) not found above test directory.");
    }
}
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Skeleton;
using Xunit;

namespace HumanoidRetargeter.Tests.Skeleton;

public class ClipTests
{
    private static Clip WithFrames(int frameCount, float fps)
    {
        var frames = new List<XForm[]>(frameCount);
        for (var i = 0; i < frameCount; i++)
            frames.Add(new[] { XForm.Identity });
        return new Clip("clip", fps, looping: false, frames);
    }

    [Theory]
    [InlineData(0, 30f, 0f)]      // empty
    [InlineData(1, 30f, 0f)]      // a single sample spans no time
    [InlineData(2, 30f, 1f / 30f)]
    [InlineData(31, 30f, 1f)]     // 31 fence posts = 30 intervals = 1 s
    [InlineData(3, 30f, 2f / 30f)] // matches the DMX timeFrame duration (FrameCount-1)/Fps
    public void Duration_IsSpanBetweenFirstAndLastSample(int frameCount, float fps, float expected)
    {
        Assert.Equal(expected, WithFrames(frameCount, fps).Duration, 6);
    }

    [Fact]
    public void Duration_NonPositiveFpsRejectedAtConstruction()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() => new Clip("clip", 0f, false));
    }
}
using System.Numerics;
using System.Text;
using HumanoidRetargeter.Core.Formats.Bvh;
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Mapping;
using HumanoidRetargeter.Core.Skeleton;
using HumanoidRetargeter.Core.Target;
using Xunit;
using Skel = HumanoidRetargeter.Core.Skeleton.Skeleton;
using HumanoidRetargeter.Core;

namespace HumanoidRetargeter.Tests.Solve;

public class GroundedLegDirectionTests
{
    [Theory]
    [InlineData(20f)]
    [InlineData(-20f)]
    public void GroundingDoesNotTiltPlantedLegsWithTargetTorso(float lean)
    {
        var bytes = Encoding.UTF8.GetBytes(WalkFixture.SyntheticWalkBvh());
        var source = BvhImporter.Import(bytes);
        var definitions = source.Skeleton.Bones.Select(b =>
        {
            var rest = b.RestLocal;
            if (b.Name == "mixamorig:Spine")
                rest.Rot = Quaternion.CreateFromAxisAngle(Vector3.UnitX, lean * MathF.PI / 180f);
            return new BoneDefinition(b.Name, b.ParentIndex < 0 ? null : source.Skeleton[b.ParentIndex].Name, rest);
        });
        var skeleton = Skel.Create(definitions.ToArray());
        var (map, _) = Retargeter.ResolveMapping(skeleton);
        var target = new RetargetTargetSpec { Rig = TargetRig.FromSkeleton(skeleton, map), VmdlScale = RetargetTargetSpec.SboxSourceScale };

        float PlantedLegAngle(bool cleanup)
        {
            var result = Retargeter.Convert(new RetargetRequest
            {
                SourceData = bytes, SourceFileName = "walk.bvh", FootPlantCleanup = cleanup,
            }, target);
            Assert.True(result.Success);
            var world = new Pose(result.Clips[0].SolvedFrames![7]).ToWorld(skeleton);
            var direction = world[map.RoleToBone[BoneRole.FootR]].Pos
                - world[map.RoleToBone[BoneRole.LowerLegR]].Pos;
            return MathQ.AngleBetween(direction, -Vector3.UnitY) * 180f / MathF.PI;
        }

        Assert.True(PlantedLegAngle(false) > 10f); // Repro: torso lean contaminates the stance.
        Assert.True(PlantedLegAngle(true) < 1f);
    }
}
using HumanoidRetargeter.Core.Formats.Fbx;
using HumanoidRetargeter.Core.Mapping;
using HumanoidRetargeter.Core.Target;
using Xunit;
using Xunit.Abstractions;
using HumanoidRetargeter.Core;

namespace HumanoidRetargeter.Tests.Target;

/// <summary>User repro (2026-07-04, local-only): DieExported.fbx picked as a custom FBX
/// target — skinned character, inch units (UnitScaleFactor 2.54), Y-up, textures folder
/// NEXT TO the source folder. Must import, be recognized (or at least accepted
/// best-effort), and convert cleanly onto its own skeleton.</summary>
public class UserDieFbxTargetTests
{
    private static readonly string FbxPath =
        TestUtil.RepoFile("dev", "corpus", "user_rigs", "die", "source", "DieExported.fbx");

    private readonly ITestOutputHelper _out;

    public UserDieFbxTargetTests(ITestOutputHelper output) => _out = output;

    [Fact]
    public void DieExported_ImportsAndResolvesAsTarget()
    {
        if (!File.Exists(FbxPath))
            return; // local-only

        var scene = FbxImporter.Import(File.ReadAllBytes(FbxPath));
        _out.WriteLine($"bones={scene.Skeleton.Count} unitScaleCm={scene.UnitScaleCm} upAxis={scene.UpAxis}");
        foreach (var bone in scene.Skeleton.Bones.Take(24))
            _out.WriteLine($"  {bone.Name}");

        var (map, report) = Retargeter.ResolveMapping(scene.Skeleton);
        _out.WriteLine($"profile={map.ProfileName} source={map.Source} conf={map.Confidence:0.00} "
            + $"needsUser={report.NeedsUserDecision} roles={map.RoleToBone.Count}");
        foreach (var (role, index) in map.RoleToBone.OrderBy(kv => kv.Key.ToString()))
            _out.WriteLine($"  {role} -> {scene.Skeleton[index].Name}");

        // The structural minimum TargetPickers accepts best-effort maps on.
        Assert.True(map.RoleToBone.ContainsKey(BoneRole.Hips), "no hips");
        Assert.True(map.RoleToBone.ContainsKey(BoneRole.UpperLegL), "no upper leg L");
        Assert.True(map.RoleToBone.ContainsKey(BoneRole.UpperLegR), "no upper leg R");

        // Converting a citizen clip onto it must solve.
        var rig = TargetRig.FromSkeleton(scene.Skeleton, map);
        var target = new HumanoidRetargeter.Core.RetargetTargetSpec
        {
            Rig = rig,
            VmdlScale = HumanoidRetargeter.Core.RetargetTargetSpec.SboxSourceScale,
            DefaultRootBone = scene.Skeleton[0].Name,
        };
        var bvh = TestUtil.RepoFile("dev", "corpus", "bvh", "cmu_01_01.bvh");
        if (!File.Exists(bvh))
            return;
        var batch = HumanoidRetargeter.Core.Retargeter.ConvertBatch(
            new[]
            {
                new HumanoidRetargeter.Core.RetargetRequest
                {
                    SourceData = File.ReadAllBytes(bvh),
                    SourceFileName = "cmu_01_01.bvh",
                },
            },
            target,
            new HumanoidRetargeter.Core.BatchOptions { DmxFolderRelative = "animations" });
        foreach (var clip in batch.Clips.Where(c => !c.Success))
            _out.WriteLine($"FAIL {clip.ClipName}: {clip.Error}");
        Assert.True(batch.Clips.All(c => c.Success));
        Assert.Contains("cmu_01_01", batch.StandaloneVmdl);
    }

    /// <summary>User request: "apply the Neutral throw ball animation to that model, see
    /// if the animation plays correctly" (and "try the surprised.fbx animation as well,
    /// which is mixamo"). Solves each clip onto DieExported and requires real motion: the
    /// hands must travel, the character must stay upright (pelvis height near rest, no
    /// axis flip), and the pelvis must not sink.</summary>
    [Theory]
    [InlineData("todo/Neutral_throw_ball_001__A057.bvh", 0.5f)]
    [InlineData("mixamo/Surprised.fbx", 0.15f)] // startled recoil - hands travel less than a throw
    public void DieExported_UserClips_SolveWithRealMotion(string corpusRelative, float travelFactor)
    {
        var throwPath = TestUtil.RepoFile(
            ("dev/corpus/" + corpusRelative).Split('/'));
        if (!File.Exists(FbxPath) || !File.Exists(throwPath))
            return; // local-only

        var scene = FbxImporter.Import(File.ReadAllBytes(FbxPath));
        var (map, _) = Retargeter.ResolveMapping(scene.Skeleton);
        var rig = TargetRig.FromSkeleton(scene.Skeleton, map);
        var target = new HumanoidRetargeter.Core.RetargetTargetSpec
        {
            Rig = rig,
            VmdlScale = HumanoidRetargeter.Core.RetargetTargetSpec.SboxSourceScale,
            DefaultRootBone = scene.Skeleton[0].Name,
        };

        var batch = HumanoidRetargeter.Core.Retargeter.ConvertBatch(
            new[]
            {
                new HumanoidRetargeter.Core.RetargetRequest
                {
                    SourceData = File.ReadAllBytes(throwPath),
                    SourceFileName = Path.GetFileName(throwPath),
                    FootPlantCleanup = true,
                },
            },
            target,
            new HumanoidRetargeter.Core.BatchOptions { DmxFolderRelative = "animations" });

        var clip = Assert.Single(batch.Clips);
        _out.WriteLine($"clip={clip.ClipName} success={clip.Success} err={clip.Error} "
            + $"frames={clip.SolvedFrames?.Count}");
        Assert.True(clip.Success, clip.Error);
        Assert.True(clip.SolvedFrames!.Count > 30, "suspiciously short solve");

        var skeleton = rig.Skeleton;
        var hips = rig.BoneForRole(BoneRole.Hips)!.Value;
        var handR = rig.BoneForRole(BoneRole.HandR)!.Value;

        var restPelvisY = skeleton.RestWorld[hips].Pos.Y;
        var minPelvis = float.MaxValue;
        var maxPelvis = float.MinValue;
        var handMin = new System.Numerics.Vector3(float.MaxValue);
        var handMax = new System.Numerics.Vector3(float.MinValue);
        foreach (var frame in clip.SolvedFrames)
        {
            var world = new HumanoidRetargeter.Core.Skeleton.Pose(frame).ToWorld(skeleton);
            minPelvis = MathF.Min(minPelvis, world[hips].Pos.Y);
            maxPelvis = MathF.Max(maxPelvis, world[hips].Pos.Y);
            handMin = System.Numerics.Vector3.Min(handMin, world[handR].Pos);
            handMax = System.Numerics.Vector3.Max(handMax, world[handR].Pos);
        }
        var handTravel = (handMax - handMin).Length();
        _out.WriteLine($"restPelvisY={restPelvisY:0.#} pelvisY=[{minPelvis:0.#}..{maxPelvis:0.#}] "
            + $"handTravel={handTravel:0.#}cm (rig units)");

        // Upright: pelvis stays within a sane band around its rest height (a lying/flipped
        // or buried character violates this immediately).
        Assert.InRange(minPelvis, restPelvisY * 0.5f, restPelvisY * 1.5f);
        Assert.InRange(maxPelvis, restPelvisY * 0.5f, restPelvisY * 1.5f);
        // The clip must move the hand substantially (relative to character size).
        Assert.True(handTravel > restPelvisY * travelFactor,
            $"hand barely moves ({handTravel:0.#} vs pelvis height {restPelvisY:0.#})");
    }
}

public class UserDieFbxHandDiagnostics
{
    private readonly Xunit.Abstractions.ITestOutputHelper _out;
    public UserDieFbxHandDiagnostics(Xunit.Abstractions.ITestOutputHelper o) => _out = o;

    [Xunit.Fact]
    public void Surprised_OnDieExported_HandTransferAudit()
    {
        var fbx = TestUtil.RepoFile("dev", "corpus", "user_rigs", "die", "source", "DieExported.fbx");
        var clipPath = TestUtil.RepoFile("dev", "corpus", "mixamo", "Surprised.fbx");
        if (!File.Exists(fbx) || !File.Exists(clipPath)) return;

        var tgtScene = HumanoidRetargeter.Core.Formats.Fbx.FbxImporter.Import(File.ReadAllBytes(fbx));
        var (tgtMap, tgtReport) = HumanoidRetargeter.Core.Retargeter.ResolveMapping(tgtScene.Skeleton);
        var rig = HumanoidRetargeter.Core.Target.TargetRig.FromSkeleton(tgtScene.Skeleton, tgtMap);
        var target = new HumanoidRetargeter.Core.RetargetTargetSpec
        {
            Rig = rig, VmdlScale = 0.3937f, DefaultRootBone = tgtScene.Skeleton[0].Name,
        };
        var batch = HumanoidRetargeter.Core.Retargeter.ConvertBatch(
            new[] { new HumanoidRetargeter.Core.RetargetRequest {
                SourceData = File.ReadAllBytes(clipPath), SourceFileName = "Surprised.fbx" } },
            target, new HumanoidRetargeter.Core.BatchOptions { DmxFolderRelative = "animations" });
        var clip = batch.Clips.Single();
        Xunit.Assert.True(clip.Success, clip.Error);

        foreach (var note in clip.Mapping!.Notes.Where(n => n.Contains("and") || n.Contains("roll")))
            _out.WriteLine("NOTE: " + note);

        // Local-rotation swing per bone at the surprise peak vs source counterparts.
        var srcScene = HumanoidRetargeter.Core.Retargeter.ImportSource(File.ReadAllBytes(clipPath), "Surprised.fbx");
        var (srcMap, _) = HumanoidRetargeter.Core.Retargeter.ResolveMapping(srcScene.Skeleton);
        var f = Math.Min(60, clip.SolvedFrames!.Count - 1);
        var srcF = Math.Min(60, srcScene.Clips[0].Frames.Count - 1);
        float Angle(System.Numerics.Quaternion a, System.Numerics.Quaternion b)
        {
            var d = MathF.Min(MathF.Abs(System.Numerics.Quaternion.Dot(
                System.Numerics.Quaternion.Normalize(a), System.Numerics.Quaternion.Normalize(b))), 1f);
            return 2f * MathF.Acos(d) * 180f / MathF.PI;
        }
        foreach (var role in new[] {
            HumanoidRetargeter.Core.Mapping.BoneRole.LowerArmR, HumanoidRetargeter.Core.Mapping.BoneRole.HandR,
            HumanoidRetargeter.Core.Mapping.BoneRole.IndexProxR, HumanoidRetargeter.Core.Mapping.BoneRole.MiddleProxR,
            HumanoidRetargeter.Core.Mapping.BoneRole.ThumbProxR,
            HumanoidRetargeter.Core.Mapping.BoneRole.HandL, HumanoidRetargeter.Core.Mapping.BoneRole.IndexProxL })
        {
            var t = rig.BoneForRole(role);
            srcMap.RoleToBone.TryGetValue(role, out var s);
            if (t is null) { _out.WriteLine($"{role}: unmapped on target"); continue; }
            var tgtSwing = Angle(clip.SolvedFrames[f][t.Value].Rot, rig.Skeleton[t.Value].RestLocal.Rot);
            var srcSwing = srcMap.RoleToBone.ContainsKey(role)
                ? Angle(srcScene.Clips[0].Frames[srcF][s].Rot, srcScene.Skeleton[s].RestLocal.Rot)
                : -1f;
            _out.WriteLine($"{role}: target swing {tgtSwing:0.#} deg  source swing {srcSwing:0.#} deg");
        }
    }
}

public class UserDieFbxFingerLengths
{
    private readonly Xunit.Abstractions.ITestOutputHelper _out;
    public UserDieFbxFingerLengths(Xunit.Abstractions.ITestOutputHelper o) => _out = o;

    [Xunit.Fact]
    public void Surprised_OnDieExported_FingerLengthAudit()
    {
        var fbx = TestUtil.RepoFile("dev", "corpus", "user_rigs", "die", "source", "DieExported.fbx");
        var clipPath = TestUtil.RepoFile("dev", "corpus", "mixamo", "Surprised.fbx");
        if (!File.Exists(fbx) || !File.Exists(clipPath)) return;

        var tgtScene = HumanoidRetargeter.Core.Formats.Fbx.FbxImporter.Import(File.ReadAllBytes(fbx));
        var (tgtMap, _) = HumanoidRetargeter.Core.Retargeter.ResolveMapping(tgtScene.Skeleton);
        foreach (var b in tgtScene.Skeleton.Bones)
            if (b.Name.Contains("Finger") || b.Name.Contains("Hand") || b.Name.Contains("Forearm"))
                _out.WriteLine($"bone: {b.Name} mapped={tgtMap.RoleToBone.ContainsValue(b.Index)}");

        var rig = HumanoidRetargeter.Core.Target.TargetRig.FromSkeleton(tgtScene.Skeleton, tgtMap);
        var target = new HumanoidRetargeter.Core.RetargetTargetSpec
        { Rig = rig, VmdlScale = 0.3937f, DefaultRootBone = tgtScene.Skeleton[0].Name };
        var batch = HumanoidRetargeter.Core.Retargeter.ConvertBatch(
            new[] { new HumanoidRetargeter.Core.RetargetRequest {
                SourceData = File.ReadAllBytes(clipPath), SourceFileName = "Surprised.fbx" } },
            target, new HumanoidRetargeter.Core.BatchOptions { DmxFolderRelative = "animations" });
        var clip = batch.Clips.Single();
        var f = Math.Min(60, clip.SolvedFrames!.Count - 1);
        var world = new HumanoidRetargeter.Core.Skeleton.Pose(clip.SolvedFrames[f]).ToWorld(rig.Skeleton);
        var rest = rig.Skeleton.RestWorld;
        var hand = rig.BoneForRole(HumanoidRetargeter.Core.Mapping.BoneRole.HandR)!.Value;
        foreach (var role in new[] {
            HumanoidRetargeter.Core.Mapping.BoneRole.IndexDistR, HumanoidRetargeter.Core.Mapping.BoneRole.MiddleDistR,
            HumanoidRetargeter.Core.Mapping.BoneRole.ThumbDistR, HumanoidRetargeter.Core.Mapping.BoneRole.PinkyDistR })
        {
            if (rig.BoneForRole(role) is not { } tip) { _out.WriteLine($"{role}: unmapped"); continue; }
            var restD = (rest[tip].Pos - rest[hand].Pos).Length();
            var frameD = (world[tip].Pos - world[hand].Pos).Length();
            _out.WriteLine($"{role}: rest {restD:0.##}  frame {frameD:0.##}  ratio {frameD / restD:0.###}");
        }
    }
}

public class UserSimpsonRigDiagnostics
{
    private readonly Xunit.Abstractions.ITestOutputHelper _out;
    public UserSimpsonRigDiagnostics(Xunit.Abstractions.ITestOutputHelper o) => _out = o;

    [Xunit.Fact]
    public void Simpson_ImportAudit()
    {
        var fbx = TestUtil.RepoFile("dev", "corpus", "user_rigs", "simpson", "source", "C8V7NUC53TF8TTF3BXDMVGQIQ.fbx");
        if (!File.Exists(fbx)) return;
        var scene = HumanoidRetargeter.Core.Formats.Fbx.FbxImporter.Import(File.ReadAllBytes(fbx));
        _out.WriteLine($"bones={scene.Skeleton.Count} unitScaleCm={scene.UnitScaleCm} upAxis={scene.UpAxis} clips={scene.Clips.Count}");
        var (map, report) = HumanoidRetargeter.Core.Retargeter.ResolveMapping(scene.Skeleton);
        _out.WriteLine($"profile={map.ProfileName} conf={map.Confidence:0.00} roles={map.RoleToBone.Count} needsUser={report.NeedsUserDecision}");
        // Character extents + suspicious rest locals
        var rest = scene.Skeleton.RestWorld;
        float maxY = float.MinValue, minY = float.MaxValue;
        foreach (var w in rest) { maxY = MathF.Max(maxY, w.Pos.Y); minY = MathF.Min(minY, w.Pos.Y); }
        _out.WriteLine($"rest height span Y: {minY:0.#}..{maxY:0.#} cm");
        foreach (var role in new[] { HumanoidRetargeter.Core.Mapping.BoneRole.Hips, HumanoidRetargeter.Core.Mapping.BoneRole.Head,
            HumanoidRetargeter.Core.Mapping.BoneRole.HandR, HumanoidRetargeter.Core.Mapping.BoneRole.FootL })
        {
            if (map.RoleToBone.TryGetValue(role, out var i))
                _out.WriteLine($"{role} -> {scene.Skeleton[i].Name} world {rest[i].Pos}");
            else _out.WriteLine($"{role}: UNMAPPED");
        }
        foreach (var b in scene.Skeleton.Bones.Take(12))
            _out.WriteLine($"  [{b.Index}] {b.Name} parent={b.ParentIndex} localPos={b.RestLocal.Pos}");
    }
}
using HumanoidRetargeter.Core.Target;
using Xunit;
using HumanoidRetargeter.Core;

namespace HumanoidRetargeter.Tests.Target;

/// <summary>
/// RenderMeshList embedding in standalone vmdls (user report 2026-07-04): a custom FBX
/// target has no compiled base model, so its generated vmdl used to compile into an EMPTY
/// model — 0 bones, 0 sequences — and playing any converted animation did nothing. With
/// <see cref="RetargetTargetSpec.MeshFilePath"/> set, the vmdl must embed the mesh source
/// so the compiled model carries the skeleton and skin the sequences play on.
/// </summary>
public class VmdlMeshEmbedTests
{
    private static readonly AnimEntry[] OneAnim =
    {
        new() { Name = "clip", SourceFilename = "animations/clip.dmx" },
    };

    [Fact]
    public void GenerateStandalone_WithMeshFile_EmbedsRenderMeshNode()
    {
        var vmdl = VmdlWriter.GenerateStandalone(
            "", OneAnim, 0.3937f, "pelvis",
            meshFilePath: "animations/retargeted/fox.fbx", meshImportScale: 100f);

        Assert.Contains("\"RenderMeshList\"", vmdl);
        Assert.Contains("\"RenderMeshFile\"", vmdl);
        Assert.Contains("animations/retargeted/fox.fbx", vmdl);
        Assert.Contains("import_scale = 100", vmdl);
        Assert.Contains("name = \"fox\"", vmdl);
        // The cm→inch modifier and the animation list still ride along.
        Assert.Contains("ModelModifier_ScaleAndMirror", vmdl);
        Assert.Contains("\"AnimationList\"", vmdl);
        // Round-trips through the kv3 parser (augmentation reads these files back).
        Kv3.Parse(vmdl);
    }

    [Fact]
    public void GenerateStandalone_WithoutMeshFile_OmitsRenderMeshNode()
    {
        var vmdl = VmdlWriter.GenerateStandalone(
            RetargetTargetSpec.SboxHumanMalePath, OneAnim, 0.3937f, "pelvis");

        Assert.DoesNotContain("RenderMeshList", vmdl);
        Assert.DoesNotContain("RenderMeshFile", vmdl);
    }

    /// <summary>The accumulate path augments an existing standalone vmdl IN PLACE, so a
    /// pipeline-owned output generated before mesh embedding existed (empty
    /// base_model_name, no mesh) would stay an empty unplayable model forever - user
    /// report: "convert onto the custom FBX, open the new vmdl, the animation doesn't
    /// play". EnsureMeshFile heals it.</summary>
    [Fact]
    public void EnsureMeshFile_HealsPreMeshEmbedVmdl_AndIsIdempotent()
    {
        // A pre-fix standalone output: no base model, no mesh, one accumulated animation.
        var old = VmdlWriter.GenerateStandalone("", OneAnim, 0.3937f, "pelvis");
        Assert.DoesNotContain("RenderMeshFile", old);

        var healed = VmdlAugmenter.EnsureMeshFile(old, "animations/retargeted/die.fbx", 2.54f);
        Assert.Contains("\"RenderMeshFile\"", healed);
        Assert.Contains("animations/retargeted/die.fbx", healed);
        Assert.Contains("import_scale = 2.54", healed);
        // The accumulated animation survives.
        Assert.Contains("\"clip\"", healed);
        Kv3.Parse(healed);

        // Same mesh again: no change at all.
        Assert.Equal(healed, VmdlAugmenter.EnsureMeshFile(healed, "animations/retargeted/die.fbx", 2.54f));

        // Switched target FBX: the pipeline-owned mesh node is replaced, not duplicated.
        var switched = VmdlAugmenter.EnsureMeshFile(healed, "animations/retargeted/fox.fbx", 100f);
        Assert.Contains("animations/retargeted/fox.fbx", switched);
        Assert.DoesNotContain("animations/retargeted/die.fbx", switched);
        Kv3.Parse(switched);
    }

    [Fact]
    public void EnsureMeshFile_EmptyMeshPath_IsNoOp()
    {
        var vmdl = VmdlWriter.GenerateStandalone("", OneAnim, 0.3937f, "pelvis");
        Assert.Same(vmdl, VmdlAugmenter.EnsureMeshFile(vmdl, "", 1f));
    }

    /// <summary>FBX materials are bare names the compiler cannot resolve as resource paths
    /// ("Trying to load an illegal resource name X.vmat") - generated vmdls must remap them
    /// to the real vmat files via MaterialGroupList (the shipped citizen mechanism).</summary>
    [Fact]
    public void GenerateStandalone_WithMaterialRemaps_EmitsMaterialGroup()
    {
        var remaps = new Dictionary<string, string>
        {
            ["mi_dante_head.vmat"] = "animations/retargeted/mi_dante_head.vmat",
        };
        var vmdl = VmdlWriter.GenerateStandalone(
            "", OneAnim, 0.3937f, "pelvis",
            meshFilePath: "animations/retargeted/die.fbx", meshImportScale: 2.54f,
            materialRemaps: remaps);

        Assert.Contains("\"MaterialGroupList\"", vmdl);
        Assert.Contains("\"DefaultMaterialGroup\"", vmdl);
        Assert.Contains("from = \"mi_dante_head.vmat\"", vmdl);
        Assert.Contains("to = \"animations/retargeted/mi_dante_head.vmat\"", vmdl);
        Kv3.Parse(vmdl);

        // And the healing path adds the same node to pre-remap outputs (idempotent).
        var old = VmdlWriter.GenerateStandalone("", OneAnim, 0.3937f, "pelvis");
        var healed = VmdlAugmenter.EnsureMeshFile(
            old, "animations/retargeted/die.fbx", 2.54f, remaps);
        Assert.Contains("\"MaterialGroupList\"", healed);
        Assert.Contains("from = \"mi_dante_head.vmat\"", healed);
        Assert.Equal(healed, VmdlAugmenter.EnsureMeshFile(
            healed, "animations/retargeted/die.fbx", 2.54f, remaps));
    }

    /// <summary>An output vmdl whose remap table predates a texture-matching improvement
    /// must LEARN new entries on the next conversion (observed: textured preview but
    /// untextured ModelDoc forever) - while existing entries stay untouched.</summary>
    [Fact]
    public void EnsureMeshFile_MergesNewMaterialRemaps()
    {
        var oldRemaps = new Dictionary<string, string> { ["a.vmat"] = "out/a.vmat" };
        var old = VmdlWriter.GenerateStandalone("", OneAnim, 0.3937f, "pelvis",
            meshFilePath: "out/m.fbx", meshImportScale: 1f, materialRemaps: oldRemaps);

        var merged = VmdlAugmenter.EnsureMeshFile(old, "out/m.fbx", 1f,
            new Dictionary<string, string>
            {
                ["a.vmat"] = "IGNORED/other.vmat",
                ["homer.vmat"] = "out/homer.vmat",
            });
        Assert.Contains("to = \"out/a.vmat\"", merged);       // existing entry untouched
        Assert.DoesNotContain("IGNORED", merged);
        Assert.Contains("from = \"homer.vmat\"", merged);     // new entry learned
        Assert.Contains("to = \"out/homer.vmat\"", merged);
        Kv3.Parse(merged);
        Assert.Equal(merged, VmdlAugmenter.EnsureMeshFile(merged, "out/m.fbx", 1f,
            new Dictionary<string, string> { ["homer.vmat"] = "out/homer.vmat" }));
    }
}
using System.Numerics;
using System.Text;
using HumanoidRetargeter.Core.Cleanup;
using HumanoidRetargeter.Core.Formats.Bvh;
using HumanoidRetargeter.Core.Formats.Fbx;
using HumanoidRetargeter.Core.Skeleton;
using HumanoidRetargeter.Core.Target;
using Xunit;
using HumanoidRetargeter.Core;

namespace HumanoidRetargeter.Tests.Solve;

public class PosedLocomotionTests
{
    [Theory]
    [InlineData(1, 500)]
    [InlineData(1, -500)]
    [InlineData(2, 500)]
    [InlineData(2, -500)]
    public void InPlaceCentersClipsWhoseStaticReferenceIsFarFromTheTake(int upAxis, float restTravel)
    {
        var target = Target(upAxis);
        var frames = Convert(Fixture(upAxis, restTravel: restTravel), target, RootMotionMode.InPlace);
        var hips = target.Rig.BoneForRole(HumanoidRetargeter.Core.Mapping.BoneRole.Hips)!.Value;
        var center = frames.Aggregate(Vector3.Zero, (sum, frame) => sum + new Pose(frame).ToWorld(target.Rig.Skeleton)[hips].Pos) / frames.Count;
        var delta = center - target.Rig.Skeleton.RestWorld[hips].Pos;
        if (upAxis == 1) delta.Y = 0; else delta.Z = 0;
        Assert.True(delta.Length() < .01f, $"In-place clip is displaced from the target bind by {delta}.");
    }

    [Theory]
    [InlineData(1, false, 0)]
    [InlineData(1, true, -90)]
    [InlineData(2, false, 90)]
    [InlineData(2, true, 90)]
    public void SerializedRootYawMatchesTargetImportConvention(int upAxis, bool embedsMesh, float degrees)
    {
        var original = Target(upAxis);
        var target = new RetargetTargetSpec
        {
            Rig = original.Rig, UpAxis = original.UpAxis, VmdlScale = 1,
            MeshFilePath = embedsMesh ? "test.fbx" : "",
        };
        var frame = target.Rig.Skeleton.Bones.Select(b => b.RestLocal).ToArray();
        var before = frame.ToArray();
        var serialized = Retargeter.TestHook_CompensateEmbeddedMeshRootYaw(new[] { frame }, target).Single();
        var yaw = Quaternion.CreateFromAxisAngle(upAxis == 1 ? Vector3.UnitY : Vector3.UnitZ, degrees * MathF.PI / 180);
        for (var i = 0; i < frame.Length; i++)
        {
            var root = target.Rig.Skeleton[i].ParentIndex < 0;
            var expectedPosition = root ? Vector3.Transform(before[i].Pos, yaw) : before[i].Pos;
            var expectedRotation = root ? Quaternion.Normalize(yaw * before[i].Rot) : before[i].Rot;
            Assert.True(Vector3.Distance(expectedPosition, serialized[i].Pos) < .0001f);
            Assert.True(MathF.Abs(Quaternion.Dot(expectedRotation, serialized[i].Rot)) > .99999f);
            Assert.Equal(before[i], frame[i]); // Serialization must not mutate solved poses.
        }
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    public void LeaningSourceRestDoesNotTurnForwardTravelIntoVerticalMotion(int upAxis)
    {
        var target = Target(upAxis);
        var result = Convert(Fixture(upAxis, torsoLean: 70), target, RootMotionMode.InPlace);
        var hip = target.Rig.BoneForRole(HumanoidRetargeter.Core.Mapping.BoneRole.Hips)!.Value;
        var heights = result.Select(frame => Vertical(new Pose(frame).ToWorld(target.Rig.Skeleton)[hip].Pos, upAxis)).ToArray();
        Assert.True(heights.Max() - heights.Min() < .05f,
            $"Horizontal travel changed pelvis height by {heights.Max() - heights.Min()}.");
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    public void RemovingRootTravelPreservesGroundedHeightsOnLeaningTarget(int upAxis)
    {
        var target = Target(upAxis, torsoLean: 20);
        var bytes = Fixture(upAxis);
        var moving = Convert(bytes, target, RootMotionMode.Off);
        var inPlace = Convert(bytes, target, RootMotionMode.InPlace);
        for (var f = 0; f < moving.Count; f++)
        {
            var before = new Pose(moving[f]).ToWorld(target.Rig.Skeleton);
            var after = new Pose(inPlace[f]).ToWorld(target.Rig.Skeleton);
            for (var b = 0; b < before.Length; b++)
                Assert.True(MathF.Abs(Vertical(before[b].Pos - after[b].Pos, upAxis)) < .005f,
                    $"Frame {f}, bone {b}: root-motion removal changed ground clearance.");
        }
    }

    [Fact]
    public void LiftedFootInExportedRestDoesNotPushPlantedFootBelowGround()
    {
        var target = Target(1);
        var result = Convert(Fixture(1, liftedRest: 35, travel: 0), target, RootMotionMode.Off);
        var toe = target.Rig.BoneForRole(HumanoidRetargeter.Core.Mapping.BoneRole.ToeR)!.Value;
        var floor = target.Rig.Skeleton.RestWorld[toe].Pos.Y;
        foreach (var frame in result)
            Assert.True(new Pose(frame).ToWorld(target.Rig.Skeleton)[toe].Pos.Y >= floor - .1f,
                "A mid-step rest was incorrectly used as the planted-foot height reference.");
    }

    static float Vertical(Vector3 value, int axis) => axis == 1 ? value.Y : value.Z;

    static RetargetTargetSpec Target(int upAxis, float torsoLean = 0)
    {
        var skeleton = FbxImporter.Import(Fixture(upAxis, torsoLean)).Skeleton;
        var (map, _) = Retargeter.ResolveMapping(skeleton);
        return new RetargetTargetSpec
        {
            Rig = TargetRig.FromSkeleton(skeleton, map), VmdlScale = 1,
            UpAxis = upAxis == 1 ? TargetUpAxis.YUpCm : TargetUpAxis.ZUpEngine,
        };
    }

    static List<HumanoidRetargeter.Core.Maths.XForm[]> Convert(byte[] bytes, RetargetTargetSpec target, RootMotionMode mode)
    {
        var result = Retargeter.Convert(new RetargetRequest
        {
            SourceData = bytes, SourceFileName = "posed-locomotion.fbx",
            RootMotion = mode, FootPlantCleanup = true,
        }, target);
        Assert.True(result.Success, string.Join("; ", result.Clips.Select(c => c.Error)));
        return result.Clips[0].SolvedFrames!;
    }

    // A tiny animation-only FBX: no proprietary pack files or optional corpus needed.
    // The static rig can be bent over or caught mid-step; the take moves horizontally.
    static byte[] Fixture(int upAxis, float torsoLean = 0, float liftedRest = 0, float travel = 100, float restTravel = 0)
    {
        var skeleton = BvhImporter.Import(Encoding.UTF8.GetBytes(PosedSkeletonFixture.SyntheticWalkBvh())).Skeleton;
        var objects = new StringBuilder();
        var connections = new StringBuilder();
        foreach (var bone in skeleton.Bones)
        {
            var p = bone.RestLocal.Pos;
            if (bone.ParentIndex < 0) { p.Y = 93; p.Z = restTravel; }
            if (upAxis == 2) p = new Vector3(p.X, -p.Z, p.Y);
            var angle = bone.Name == "mixamorig:Spine" ? torsoLean
                : bone.Name == "mixamorig:RightUpLeg" ? liftedRest : 0;
            objects.AppendLine(FormattableString.Invariant($$"""
                Model: {{bone.Index + 1}}, "Model::{{bone.Name}}", "LimbNode" {
                    Properties70: {
                        P: "Lcl Translation", "Lcl Translation", "", "A",{{p.X}},{{p.Y}},{{p.Z}}
                        P: "Lcl Rotation", "Lcl Rotation", "", "A",{{angle}},0,0
                    }
                }
                """));
            connections.AppendLine($"C: \"OO\",{bone.Index + 1},{bone.ParentIndex + 1}");
        }
        void Curve(int id, int bone, string property, string axis, float end)
        {
            objects.AppendLine(FormattableString.Invariant($$"""
                AnimationCurveNode: {{id}}, "AnimCurveNode::curve", "" { }
                AnimationCurve: {{id + 1}}, "AnimCurve::value", "" {
                    KeyTime: *2 { a: 0,46186158000 }
                    KeyValueFloat: *2 { a: 0,{{end}} }
                }
                """));
            connections.AppendLine($"C: \"OO\",{id},10001\nC: \"OP\",{id},{bone},\"{property}\"\nC: \"OP\",{id + 1},{id},\"d|{axis}\"");
        }
        Curve(10002, 1, "Lcl Translation", upAxis == 1 ? "Z" : "Y", upAxis == 1 ? travel : -travel);
        Curve(10004, skeleton.IndexOf("mixamorig:RightUpLeg") + 1, "Lcl Rotation", "X", 0);
        return Encoding.UTF8.GetBytes(FormattableString.Invariant($$"""
            GlobalSettings: { Properties70: {
                P: "UpAxis", "int", "Integer", "",{{upAxis}}
                P: "UpAxisSign", "int", "Integer", "",1
                P: "UnitScaleFactor", "double", "Number", "",1
            } }
            Objects: {
                {{objects}}
                AnimationStack: 10000, "AnimStack::walk", "" { }
                AnimationLayer: 10001, "AnimLayer::base", "" { }
            }
            Connections: {
                {{connections}}
                C: "OO",10001,10000
            }
            """));
    }
}
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using Editor;
using Sandbox;

// A custom rig without stock attachments must receive calibrated copied sockets.
// Run only in an explicitly opted-in isolated project, never a user's active editor.
public static class SmartPortCopiedSocketEngineTest
{
    static bool started;
    [EditorEvent.Frame]
    public static void Tick()
    {
        var root = Environment.GetEnvironmentVariable("HR_SMART_PORT_COPIED_SOCKET_PROJECT");
        if (started || Project.Current is null || string.IsNullOrEmpty(root)
            || !string.Equals(Path.GetFullPath(Project.Current.GetRootPath()).TrimEnd('/', '\\'), Path.GetFullPath(root).TrimEnd('/', '\\'), StringComparison.OrdinalIgnoreCase)
            || !AssetSystem.All.Any()) return;
        started = true;
        _ = Run(root);
    }
    static async Task Run(string root)
    {
        var data = new Dictionary<string, object>();
        void Save() => File.WriteAllText(Path.Combine(root, "copied-socket-result.json"),
            JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }));
        try
        {
            data["started"] = DateTime.UtcNow; Save();
            var source = AssetSystem.FindByPath("models/citizen/citizen.vmdl") ?? throw new Exception("Missing Citizen source");
            var target = AssetSystem.FindByPath("humanoid_retargeter_smoke/customfbx/catgirl_2_preview_bind_9e13ad1f.vmdl") ?? throw new Exception("Missing Catgirl target fixture");
            var type = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetType("HumanoidRetargeter.EditorTools.SmartPortModels")).First(t => t != null);
            var name = "copied_socket_" + DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
            Action<string> progress = s => { data["stage"] = s; Save(); };
            var task = (Task)type.GetMethod("CreateAsync", BindingFlags.Static | BindingFlags.NonPublic)
                .Invoke(null, new object[] { source, target, "smart_port", name, progress, CancellationToken.None });
            await task;
            var result = task.GetType().GetProperty("Result").GetValue(task);
            data["compiled"] = result.GetType().GetProperty("Compiled").GetValue(result);
            data["errors"] = result.GetType().GetProperty("Errors").GetValue(result);
            data["model"] = "smart_port/" + name + ".vmdl"; Save();
            if (!(bool)data["compiled"]) throw new Exception("Compile failed");
            var session = SceneEditorSession.CreateDefault(); session.MakeActive();
            using (session.Scene.Push())
                foreach (var path in new[] { source.Path, (string)data["model"] })
                {
                    var go = session.Scene.CreateObject(); go.Name = path;
                    var actor = go.Components.Create<SkinnedModelRenderer>();
                    actor.Model = Model.Load(path); actor.UseAnimGraph = true;
                }
            EditorScene.Play(false, session); await Task.Delay(1500);
            if (!Game.IsPlaying || Game.ActiveScene is null || Game.ActiveScene.IsEditor) throw new Exception("Did not enter play mode");
            var actors = Game.ActiveScene.GetAllComponents<SkinnedModelRenderer>().ToArray();
            var observations = new List<object>();
            const int hold = 2; // Source graph's rifle hold type.
            foreach (var weight in new[] { 0f, 1f })
            foreach (var pitch in new[] { 0f, -25f, 25f })
            {
                for (var settle = 0; settle < 50; settle++)
                {
                    foreach (var actor in actors)
                    {
                        actor.Set("b_grounded", true); actor.Set("holdtype", hold); actor.Set("weapon_pose", 0);
                        actor.Set("aim_body", Rotation.From(pitch, 0, 0).Forward); actor.Set("aim_body_weight", weight);
                        actor.Set("aim_head", Rotation.From(pitch, 0, 0).Forward); actor.Set("aim_head_weight", 1f);
                    }
                    await Task.Delay(100);
                }
                var sourceActor = actors.Single(a => a.Model.Name == source.Path);
                var targetActor = actors.Single(a => a.Model.Name == (string)data["model"]);
                var sourceGrip = sourceActor.SceneModel.GetAttachment("hold_R", true).Value;
                var targetGrip = targetActor.SceneModel.GetAttachment("hold_R", true).Value;
                var a = sourceGrip.Rotation; var b = targetGrip.Rotation;
                var error = 2 * MathF.Acos(Math.Clamp(MathF.Abs(a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w), 0, 1)) * 180 / MathF.PI;
                if (!float.IsFinite(error) || error > 3) throw new Exception($"Rifle attachment diverged {error} degrees (aim weight {weight}, pitch {pitch})");
                foreach (var actor in actors)
                {
                    var att = actor.SceneModel.GetAttachment("hold_R", true) ?? throw new Exception("Missing grip");
                    observations.Add(new { model = actor.Model.Name, hold, weight, pitch, error,
                        rotation = att.Rotation.Angles().ToString(), forward = att.Rotation.Forward.ToString(), position = att.Position.ToString() });
                }
            }
            data["observations"] = observations; data["passed"] = true;
        }
        catch (Exception e) { data["error"] = e.ToString(); data["passed"] = false; }
        finally
        {
            data["completed"] = true; Save();
            if (Game.IsPlaying) EditorScene.Stop();
            await Task.Delay(1000); EditorUtility.Quit(true);
        }
    }
}
using HumanoidRetargeter.EditorTools;
using HumanoidRetargeter.EditorTools.Embedded.ValveResourceFormat;
using HumanoidRetargeter.EditorTools.Embedded.ValveResourceFormat.ResourceTypes;
using Xunit;

namespace SmartPort.Parser.Tests;

public class SmartPortHandSocketMetadataTests
{
    [CompiledFixtureFact]
    public void AttachmentFittingUsesCompiledParentBones()
    {
        var path = Path.Combine(Environment.GetEnvironmentVariable("HR_SMART_PORT_FIXTURE")!, "models/player/human/frank_mp.vmdl_c");
        var names = SmartPortAttachments.BoneNames(path);
        using var resource = new Resource();
        resource.Read(path);
        var model = Assert.IsType<Model>(resource.DataBlock);
        Assert.Contains("hold_R", names);
        Assert.Contains("hold_L", names);
        Assert.Equal(names.Length, names.Distinct().Count());
        Assert.All(names.Where(n => !string.IsNullOrEmpty(n)), n => Assert.Contains(model.Skeleton.Bones, b => b.Name == n));
    }
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	[AssemblyInitialize]
	public static void ClassInitialize( TestContext context )
	{
		Sandbox.Application.InitUnitTest();
	}
}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	public static Sandbox.TestAppSystem AppSystem;

	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		AppSystem = new Sandbox.TestAppSystem();
		AppSystem.Init();
	}

	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		AppSystem.Shutdown();
	}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace WackyLib.Tests;

[TestClass]
public class TestInit
{
	private static Sandbox.TestAppSystem s_appSystem;
	
	[AssemblyInitialize]
	public static void AssemblyInitialize( TestContext context )
	{
		s_appSystem = new Sandbox.TestAppSystem();
		s_appSystem.Init();
	}
	
	[AssemblyCleanup]
	public static void AssemblyCleanup()
	{
		s_appSystem.Shutdown();
	}
}
using Sandbox;

[TestClass]
public class SyncToolYamlRendererTests
{
	[TestMethod]
	public void EmptyInputProducesEmptyOutput()
	{
		Assert.AreEqual( "", SyncToolYamlRenderer.RenderFromJson( null ) );
		Assert.AreEqual( "", SyncToolYamlRenderer.RenderFromJson( "" ) );
	}

	[TestMethod]
	public void EmptyObjectAndArrayRenderAsFlowStyle()
	{
		Assert.AreEqual( "{}\n", SyncToolYamlRenderer.RenderFromJson( "{}" ) );
		Assert.AreEqual( "[]\n", SyncToolYamlRenderer.RenderFromJson( "[]" ) );
	}

	[TestMethod]
	public void InvalidJsonReturnsInputUnchanged()
	{
		var notJson = "not: real: json: ::";
		Assert.AreEqual( notJson, SyncToolYamlRenderer.RenderFromJson( notJson ) );
	}

	[TestMethod]
	public void TopLevelKeysAreSortedAlphabetically()
	{
		var json = "{\"zeta\":1,\"alpha\":2,\"mu\":3}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "alpha: 2\nmu: 3\nzeta: 1\n", yaml );
	}

	[TestMethod]
	public void NestedObjectsAreSortedRecursively()
	{
		var json = "{\"outer\":{\"zeta\":1,\"alpha\":2}}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "outer:\n  alpha: 2\n  zeta: 1\n", yaml );
	}

	[TestMethod]
	public void DifferentKeyOrdersProduceIdenticalOutput()
	{
		var a = "{\"slug\":\"hello\",\"method\":\"POST\",\"enabled\":true}";
		var b = "{\"enabled\":true,\"method\":\"POST\",\"slug\":\"hello\"}";

		Assert.AreEqual(
			SyncToolYamlRenderer.RenderFromJson( a ),
			SyncToolYamlRenderer.RenderFromJson( b )
		);
	}

	[TestMethod]
	public void OutputUsesYamlSyntaxNotJsonSyntax()
	{
		var json = "{\"name\":\"hooked\",\"enabled\":true,\"max\":42}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		// Smoke check: the rendered text must look like YAML, not JSON.
		// This is the regression we're guarding: prior to the fix the diff
		// view rendered structured data as JSON.
		StringAssert.DoesNotMatch( yaml, new System.Text.RegularExpressions.Regex( @"^\s*\{" ) );
		StringAssert.Contains( yaml, "enabled: true" );
		StringAssert.Contains( yaml, "max: 42" );
		StringAssert.Contains( yaml, "name: \"hooked\"" );
	}

	[TestMethod]
	public void StringValuesAreQuotedAndEscapedSafely()
	{
		var json = "{\"text\":\"a:b\\nc\"}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		// Strings go through JSON quoting which is also valid YAML.
		// The colon/newline must not leak as YAML structure.
		Assert.AreEqual( "text: \"a:b\\nc\"\n", yaml );
	}

	[TestMethod]
	public void BooleanAndNullAreUnquoted()
	{
		var json = "{\"a\":true,\"b\":false,\"c\":null}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "a: true\nb: false\nc: null\n", yaml );
	}

	[TestMethod]
	public void IntegerAndDoubleAreUnquoted()
	{
		var json = "{\"i\":7,\"d\":1.5}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "d: 1.5\ni: 7\n", yaml );
	}

	[TestMethod]
	public void ArraysOfObjectsRenderAsBlockSequence()
	{
		var json = "{\"steps\":[{\"name\":\"first\",\"id\":1},{\"name\":\"second\",\"id\":2}]}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		var expected =
			"steps:\n" +
			"  -\n" +
			"    id: 1\n" +
			"    name: \"first\"\n" +
			"  -\n" +
			"    id: 2\n" +
			"    name: \"second\"\n";

		Assert.AreEqual( expected, yaml );
	}

	[TestMethod]
	public void ArraysOfScalarsRenderAsBlockSequence()
	{
		var json = "{\"tags\":[\"a\",\"b\",\"c\"]}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "tags:\n  - \"a\"\n  - \"b\"\n  - \"c\"\n", yaml );
	}

	[TestMethod]
	public void TopLevelArrayRenders()
	{
		var json = "[1,2,3]";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "- 1\n- 2\n- 3\n", yaml );
	}

	[TestMethod]
	public void KeysWithNonIdentifierCharactersAreQuoted()
	{
		var json = "{\"weird key\":1,\"x:y\":2}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		// Bare YAML keys must not contain spaces or colons, so the renderer
		// quotes them. Sort order is by raw key (Ordinal).
		StringAssert.Contains( yaml, "\"weird key\": 1" );
		StringAssert.Contains( yaml, "\"x:y\": 2" );
	}

	[TestMethod]
	public void EmptyNestedObjectAndArrayUseFlowStyle()
	{
		var json = "{\"obj\":{},\"arr\":[]}";
		var yaml = SyncToolYamlRenderer.RenderFromJson( json );

		Assert.AreEqual( "arr: []\nobj: {}\n", yaml );
	}
}
#nullable enable

using Hexagon.V2.Application;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Kernel.Events;
using Hexagon.V2.Persistence;

namespace Hexagon.V2.Tests.Application;

[TestClass]
public sealed class SceneAndAggregateMutationTests
{
	[TestMethod]
	public async Task SceneStateUsesRegisteredTypesAndPublishesOnlyCommittedReplacement()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var service = new SceneEntityStateService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<SceneEntityStateMutationContext>());
		var sceneEntityId = SceneEntityId.New();
		var actor = new AccountId(9101);
		var initial = ApplicationServiceTestEnvironment.StatePayload("closed");

		var ensured = await service.EnsureAsync(sceneEntityId, "door", initial);
		var replaced = await service.ReplaceStateAsync(
			actor, null, sceneEntityId, ApplicationServiceTestEnvironment.StatePayload("open"));
		var incompatible = await service.ReplaceStateAsync(
			actor, null, sceneEntityId, ApplicationServiceTestEnvironment.StatePayload("future", version: 2));
		environment.Provider.FailNextCommit();
		var failedCommit = await service.ReplaceStateAsync(
			actor, null, sceneEntityId, ApplicationServiceTestEnvironment.StatePayload("unpublished"));

		Assert.IsTrue(ensured.Succeeded, ensured.Error?.Message);
		Assert.IsTrue(replaced.Succeeded, replaced.Error?.Message);
		Assert.AreEqual(ErrorCode.PersistedTypeInvalid, incompatible.Error!.Code);
		Assert.AreEqual(ErrorCode.InternalError, failedCommit.Error!.Code);
		var stored = service.Find(sceneEntityId)!;
		Assert.AreEqual("open", stored.State.Data.GetProperty("name").GetString());
		Assert.AreEqual(ApplicationServiceTestEnvironment.StateTypeId, stored.State.TypeId.Value);
		Assert.AreEqual(1, stored.State.TypeVersion);
	}

	[TestMethod]
	public async Task TouchLastPlayedReturnsOnlyProviderIssuedDurableReceipt()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId( 9103 );
		var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch
		};
		await environment.SeedAsync( unitOfWork =>
			unitOfWork.Create( environment.Repositories.Characters, DomainKeys.Character( character.Id ), character ) );
		var service = new AggregateMutationService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
			ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>() );

		var timestamp = DateTimeOffset.UnixEpoch.AddMinutes( 1 );
		var touched = await service.TouchLastPlayedAsync( account, character.Id, timestamp );

		Assert.IsTrue( touched.Succeeded, touched.Error?.Message );
		Assert.AreEqual( CharacterMutationKind.TouchLastPlayed, touched.Value.Kind );
		Assert.AreEqual( character.LastPlayedAt, touched.Value.Before.LastPlayedAt );
		Assert.AreEqual( timestamp, touched.Value.After.LastPlayedAt );
		Assert.IsGreaterThan( 0L, touched.Value.Commit.Sequence );
		Assert.IsTrue( touched.Value.Commit.Documents.Any( document =>
			document.Address.Collection == DomainCollections.Characters &&
			document.Address.Key == DomainKeys.Character( character.Id ) ) );

		environment.Provider.FailNextCommit();
		var failed = await service.TouchLastPlayedAsync(
			account, character.Id, timestamp.AddMinutes( 1 ) );

		Assert.IsTrue( failed.Failed );
		Assert.AreEqual( ErrorCode.InternalError, failed.Error!.Code );
		Assert.AreEqual( timestamp, environment.Repositories.Characters.Find(
			DomainKeys.Character( character.Id ) )!.Value.LastPlayedAt );
	}

	[TestMethod]
	public async Task PreparedTouchStagesIntoCombinedCommitAndPublishesOnlyOnMatchingCompletion()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId( 9104 );
		var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch
		};
		var item = ApplicationServiceTestEnvironment.Item();
		await environment.SeedAsync( unitOfWork =>
		{
			unitOfWork.Create(
				environment.Repositories.Characters, DomainKeys.Character( character.Id ), character );
			unitOfWork.Create( environment.Repositories.Items, DomainKeys.Item( item.Id ), item );
		} );
		var events = new RecordingCharacterChangedHandler();
		var service = new AggregateMutationService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
			ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>(),
			new PostCommitEventBus<CharacterChangedEvent>( new[]
			{
				new EventHandlerRegistration<CharacterChangedEvent>( "record", events )
			} ) );

		var timestamp = DateTimeOffset.UnixEpoch.AddMinutes( 1 );
		var prepared = service.PrepareTouchLastPlayed( account, character.Id, timestamp );
		Assert.IsTrue( prepared.Succeeded, prepared.Error?.Message );
		var unitOfWork = environment.Repositories.Provider.BeginUnitOfWork();
		Assert.IsTrue( service.StageTouchLastPlayed( unitOfWork, prepared.Value ).Succeeded );
		var itemDocument = environment.Repositories.Items.Find( DomainKeys.Item( item.Id ) )!;
		var itemEditor = unitOfWork.Edit( environment.Repositories.Items, itemDocument )!;
		itemEditor.Replace( itemEditor.Value with { Revision = 1 } );
		unitOfWork.Save( itemEditor );
		var committed = await unitOfWork.CommitAsync();
		await unitOfWork.DisposeAsync();
		Assert.IsTrue( committed.Succeeded, committed.Error?.Message );
		Assert.HasCount( 2, committed.Value!.Documents );
		Assert.IsEmpty( events.Events );

		var premature = service.CompleteTouchLastPlayed(
			prepared.Value, new CommitReceipt( committed.Value.Sequence, Array.Empty<CommittedDocumentVersion>() ) );
		Assert.AreEqual( ErrorCode.InvariantViolation, premature.Error!.Code );
		Assert.IsEmpty( events.Events );

		var completed = service.CompleteTouchLastPlayed( prepared.Value, committed.Value );
		Assert.IsTrue( completed.Succeeded, completed.Error?.Message );
		Assert.AreSame( committed.Value, completed.Value.Commit );
		Assert.AreEqual( timestamp, completed.Value.After.LastPlayedAt );
		Assert.HasCount( 1, events.Events );
		Assert.AreEqual( committed.Value.Sequence, events.Events[0].CommitSequence );
	}

	[TestMethod]
	public async Task CompletionSucceedsFromTheReceiptAloneAfterAnInterleavedCommit()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId( 9106 );
		var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch
		};
		await environment.SeedAsync( unitOfWork =>
			unitOfWork.Create(
				environment.Repositories.Characters, DomainKeys.Character( character.Id ), character ) );
		var events = new RecordingCharacterChangedHandler();
		var service = new AggregateMutationService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
			ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>(),
			new PostCommitEventBus<CharacterChangedEvent>( new[]
			{
				new EventHandlerRegistration<CharacterChangedEvent>( "record", events )
			} ) );

		var timestamp = DateTimeOffset.UnixEpoch.AddMinutes( 1 );
		var prepared = service.PrepareTouchLastPlayed( account, character.Id, timestamp );
		Assert.IsTrue( prepared.Succeeded, prepared.Error?.Message );
		var unitOfWork = environment.Repositories.Provider.BeginUnitOfWork();
		Assert.IsTrue( service.StageTouchLastPlayed( unitOfWork, prepared.Value ).Succeeded );
		var committed = await unitOfWork.CommitAsync();
		await unitOfWork.DisposeAsync();
		Assert.IsTrue( committed.Succeeded, committed.Error?.Message );

		var current = environment.Repositories.Characters.Find( DomainKeys.Character( character.Id ) )!;
		var competingUnit = environment.Repositories.Provider.BeginUnitOfWork();
		var competingEditor = competingUnit.Edit( environment.Repositories.Characters, current )!;
		competingEditor.Replace( competingEditor.Value with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch.AddMinutes( 2 )
		} );
		competingUnit.Save( competingEditor );
		Assert.IsTrue( (await competingUnit.CommitAsync()).Succeeded );
		await competingUnit.DisposeAsync();

		var completed = service.CompleteTouchLastPlayed( prepared.Value, committed.Value! );

		Assert.IsTrue( completed.Succeeded,
			"A durably committed mutation must complete from its receipt even after an interleaved commit." );
		Assert.AreEqual( timestamp, completed.Value.After.LastPlayedAt );
		Assert.HasCount( 1, events.Events );
	}

	[TestMethod]
	public async Task PreparedTouchCommitFailureAndRevisionConflictEmitNoEvent()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId( 9105 );
		var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch
		};
		await environment.SeedAsync( unitOfWork =>
			unitOfWork.Create(
				environment.Repositories.Characters, DomainKeys.Character( character.Id ), character ) );
		var events = new RecordingCharacterChangedHandler();
		var service = new AggregateMutationService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
			ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>(),
			new PostCommitEventBus<CharacterChangedEvent>( new[]
			{
				new EventHandlerRegistration<CharacterChangedEvent>( "record", events )
			} ) );

		var failedPlan = service.PrepareTouchLastPlayed(
			account, character.Id, DateTimeOffset.UnixEpoch.AddMinutes( 1 ) );
		var failedUnit = environment.Repositories.Provider.BeginUnitOfWork();
		Assert.IsTrue( service.StageTouchLastPlayed( failedUnit, failedPlan.Value ).Succeeded );
		environment.Provider.FailNextCommit();
		var failedCommit = await failedUnit.CommitAsync();
		await failedUnit.DisposeAsync();
		Assert.IsFalse( failedCommit.Succeeded );
		Assert.IsEmpty( events.Events );
		Assert.AreEqual( DateTimeOffset.UnixEpoch, environment.Repositories.Characters.Find(
			DomainKeys.Character( character.Id ) )!.Value.LastPlayedAt );

		var conflictedPlan = service.PrepareTouchLastPlayed(
			account, character.Id, DateTimeOffset.UnixEpoch.AddMinutes( 2 ) );
		var staleUnit = environment.Repositories.Provider.BeginUnitOfWork();
		Assert.IsTrue( service.StageTouchLastPlayed( staleUnit, conflictedPlan.Value ).Succeeded );
		var current = environment.Repositories.Characters.Find( DomainKeys.Character( character.Id ) )!;
		var competingUnit = environment.Repositories.Provider.BeginUnitOfWork();
		var competingEditor = competingUnit.Edit( environment.Repositories.Characters, current )!;
		competingEditor.Replace( competingEditor.Value with
		{
			LastPlayedAt = DateTimeOffset.UnixEpoch.AddMinutes( 3 )
		} );
		competingUnit.Save( competingEditor );
		var competingCommit = await competingUnit.CommitAsync();
		await competingUnit.DisposeAsync();
		Assert.IsTrue( competingCommit.Succeeded, competingCommit.Error?.Message );
		var conflict = await staleUnit.CommitAsync();
		await staleUnit.DisposeAsync();
		Assert.IsFalse( conflict.Succeeded );
		Assert.AreEqual( PersistenceErrorCode.RevisionConflict, conflict.Error!.Code );
		Assert.IsEmpty( events.Events );

		// Completion validates from the receipt alone; a receipt that does not contain the
		// prepared revision is rejected without publishing.
		var wrongCompletion = service.CompleteTouchLastPlayed(
			conflictedPlan.Value,
			new CommitReceipt( competingCommit.Value!.Sequence, Array.Empty<CommittedDocumentVersion>() ) );
		Assert.AreEqual( ErrorCode.InvariantViolation, wrongCompletion.Error!.Code );
		Assert.IsEmpty( events.Events );
	}

	[TestMethod]
	public async Task AggregateMutationFailuresLeaveTimestampBanBalanceStateAndTraitsUntouched()
	{
		await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
		var account = new AccountId(9102);
		var character = ApplicationServiceTestEnvironment.Character(account, 0) with
		{
			Balance = 10,
			LastPlayedAt = DateTimeOffset.UnixEpoch.AddHours(2)
		};
		var item = ApplicationServiceTestEnvironment.Item() with
		{
			Traits = new Dictionary<string, TypedPayload>(StringComparer.Ordinal)
			{
				["state"] = ApplicationServiceTestEnvironment.StatePayload("old")
			}
		};
		await environment.SeedAsync(unitOfWork =>
		{
			unitOfWork.Create(environment.Repositories.Characters, DomainKeys.Character(character.Id), character);
			unitOfWork.Create(environment.Repositories.Items, DomainKeys.Item(item.Id), item);
		});
		var service = new AggregateMutationService(
			environment.Repositories,
			environment.Schema,
			ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
			ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>());

		var oldTimestamp = await service.TouchLastPlayedAsync(
			account, character.Id, DateTimeOffset.UnixEpoch);
		var nonUtcBan = await service.SetBanAsync(
			account,
			character.Id,
			true,
			new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.FromHours(1)));
		var insufficientFunds = await service.DebitAsync(account, character.Id, 11);
		var unknownState = await service.ReplaceSchemaStateAsync(
			account,
			character.Id,
			ApplicationServiceTestEnvironment.StatePayload(typeId: "unknown.state"));
		var unknownTrait = await service.ReplaceTraitAsync(
			account,
			character.Id,
			item.Id,
			"state",
			ApplicationServiceTestEnvironment.StatePayload(typeId: "unknown.trait"));

		environment.Provider.FailNextCommit();
		var failedCredit = await service.CreditAsync(account, character.Id, 1);
		environment.Provider.FailNextCommit();
		var failedTraitCommit = await service.ReplaceTraitAsync(
			account,
			character.Id,
			item.Id,
			"state",
			ApplicationServiceTestEnvironment.StatePayload("new"));

		Assert.AreEqual(ErrorCode.InvalidArgument, oldTimestamp.Error!.Code);
		Assert.AreEqual(ErrorCode.InvalidArgument, nonUtcBan.Error!.Code);
		Assert.AreEqual(ErrorCode.Conflict, insufficientFunds.Error!.Code);
		Assert.AreEqual(ErrorCode.PersistedTypeInvalid, unknownState.Error!.Code);
		Assert.AreEqual(ErrorCode.PersistedTypeInvalid, unknownTrait.Error!.Code);
		Assert.AreEqual(ErrorCode.InternalError, failedCredit.Error!.Code);
		Assert.AreEqual(ErrorCode.InternalError, failedTraitCommit.Error!.Code);

		var storedCharacter = environment.Repositories.Characters.Find(DomainKeys.Character(character.Id))!.Value;
		var storedItem = environment.Repositories.Items.Find(DomainKeys.Item(item.Id))!.Value;
		Assert.AreEqual(character.LastPlayedAt, storedCharacter.LastPlayedAt);
		Assert.IsFalse(storedCharacter.IsBanned);
		Assert.IsNull(storedCharacter.BanExpiresAt);
		Assert.AreEqual(10L, storedCharacter.Balance);
		Assert.AreEqual("citizen", storedCharacter.SchemaState.Data.GetProperty("name").GetString());
		Assert.AreEqual("old", storedItem.Traits["state"].Data.GetProperty("name").GetString());
	}

	private sealed class RecordingCharacterChangedHandler : IEventHandler<CharacterChangedEvent>
	{
		public List<CharacterChangedEvent> Events { get; } = new();
		public void Handle( CharacterChangedEvent @event ) => Events.Add( @event );
	}
}
using Hexagon.V2.Application;
using Hexagon.V2.Domain;

namespace Hexagon.V2.Tests.Application;

[TestClass]
public sealed class SceneIdentityValidatorTests
{
	[TestMethod]
	public void ProvenanceClassifier_TrustsOnlyCapturedNonNetworkComponents()
	{
		var authored = Guid.NewGuid();
		var laterRuntime = Guid.NewGuid();
		var classifier = new SceneIdentityProvenanceClassifier();

		Assert.AreEqual(
			SceneIdentityProvenance.RuntimeOrNetwork,
			classifier.Classify( authored, hasActiveNetworkRoot: false ) );

		classifier.CaptureAuthoredSnapshot( new[] { authored } );

		Assert.IsTrue( classifier.HasAuthoredSnapshot );
		Assert.AreEqual( 1, classifier.AuthoredComponentCount );
		Assert.AreEqual(
			SceneIdentityProvenance.EditorAuthored,
			classifier.Classify( authored, hasActiveNetworkRoot: false ) );
		Assert.AreEqual(
			SceneIdentityProvenance.RuntimeOrNetwork,
			classifier.Classify( authored, hasActiveNetworkRoot: true ) );
		Assert.AreEqual(
			SceneIdentityProvenance.RuntimeOrNetwork,
			classifier.Classify( laterRuntime, hasActiveNetworkRoot: false ) );
	}

	[TestMethod]
	public void ProvenanceClassifier_NewSceneSnapshotReplacesPriorAuthority()
	{
		var firstScene = Guid.NewGuid();
		var secondScene = Guid.NewGuid();
		var classifier = new SceneIdentityProvenanceClassifier();
		classifier.CaptureAuthoredSnapshot( new[] { firstScene } );

		classifier.CaptureAuthoredSnapshot( new[] { secondScene } );

		Assert.AreEqual(
			SceneIdentityProvenance.RuntimeOrNetwork,
			classifier.Classify( firstScene, hasActiveNetworkRoot: false ) );
		Assert.AreEqual(
			SceneIdentityProvenance.EditorAuthored,
			classifier.Classify( secondScene, hasActiveNetworkRoot: false ) );
	}

	[TestMethod]
	public void EditorRepair_FillsMissingAndRepairsOnlyLaterDuplicate()
	{
		var duplicate = new SceneEntityId( Guid.Parse( "11111111-1111-1111-1111-111111111111" ) );
		var generated = new Queue<SceneEntityId>( new[]
		{
			new SceneEntityId( Guid.Parse( "22222222-2222-2222-2222-222222222222" ) ),
			new SceneEntityId( Guid.Parse( "33333333-3333-3333-3333-333333333333" ) )
		} );
		var result = SceneIdentityValidator.RepairForEditor( new[]
		{
			new SceneIdentityCandidate( "a", duplicate ),
			new SceneIdentityCandidate( "b", duplicate ),
			new SceneIdentityCandidate( "c", null )
		}, () => generated.Dequeue() );

		Assert.IsFalse( result[0].Repaired );
		Assert.IsTrue( result[1].Repaired );
		Assert.IsTrue( result[2].Repaired );
		Assert.AreNotEqual( result[0].EffectiveId, result[1].EffectiveId );
	}

	[TestMethod]
	public void RuntimeValidation_DisablesEveryAmbiguousOrBlankEntity()
	{
		var duplicate = new SceneEntityId( Guid.Parse( "11111111-1111-1111-1111-111111111111" ) );
		var result = SceneIdentityValidator.ValidateRuntime( new[]
		{
			new SceneIdentityCandidate( "a", duplicate ),
			new SceneIdentityCandidate( "b", duplicate ),
			new SceneIdentityCandidate( "c", null )
		} );

		Assert.IsTrue( result.All( value => !value.Enabled && value.FatalDiagnostic is not null ) );
	}

	[TestMethod]
	public void PersistentIndexEnumeratesTenThousandCandidatesOnceAndProvidesConstantTimeLookups()
	{
		var ids = Enumerable.Range( 0, 10_000 ).Select( _ => SceneEntityId.New() ).ToArray();
		var enumerated = 0;
		IEnumerable<SceneIdentityCandidate> Candidates()
		{
			for ( var index = 0; index < ids.Length; index++ )
			{
				enumerated++;
				yield return new SceneIdentityCandidate( $"entity/{index}", ids[index] );
			}
		}

		var index = PersistentSceneIdentityIndex.Build( Candidates() );

		Assert.AreEqual( 10_000, enumerated );
		Assert.AreEqual( 10_000, index.Count );
		for ( var candidate = 0; candidate < ids.Length; candidate++ )
		{
			Assert.IsTrue( index.TryResolveId( ids[candidate], out var byId ) );
			Assert.IsTrue( index.TryResolvePath( $"entity/{candidate}", out var byPath ) );
			Assert.AreSame( byId, byPath );
		}
	}

	[TestMethod]
	public void RuntimeNetworkDuplicateCannotPoisonEditorAuthoredIdentity()
	{
		var duplicate = SceneEntityId.New();
		var initial = PersistentSceneIdentityIndex.Build( new[]
		{
			new SceneIdentityCandidate( "first", duplicate )
		} );
		Assert.IsTrue( initial.TryResolveId( duplicate, out _ ) );

		var rebuilt = PersistentSceneIdentityIndex.Build( new[]
		{
			new SceneIdentityCandidate( "first", duplicate ),
			new SceneIdentityCandidate(
				"first", duplicate, SceneIdentityProvenance.RuntimeOrNetwork )
		} );

		Assert.IsTrue( rebuilt.TryResolveId( duplicate, out var resolved ) );
		Assert.AreEqual( "first", resolved.StablePath );
		Assert.IsTrue( rebuilt.Resolutions[0].Enabled );
		Assert.IsFalse( rebuilt.Resolutions[1].Enabled );
		Assert.AreEqual( SceneIdentityProvenance.RuntimeOrNetwork, rebuilt.Resolutions[1].Provenance );
		Assert.IsNull( rebuilt.Resolutions[1].EffectiveId );
		StringAssert.Contains( rebuilt.Resolutions[1].FatalDiagnostic, "runtime/network root" );
	}

	[TestMethod]
	public void TwoEditorAuthoredDuplicatesStillFailClosed()
	{
		var duplicate = SceneEntityId.New();
		var rebuilt = PersistentSceneIdentityIndex.Build( new[]
		{
			new SceneIdentityCandidate( "first", duplicate ),
			new SceneIdentityCandidate( "second", duplicate )
		} );

		Assert.IsFalse( rebuilt.TryResolveId( duplicate, out _ ) );
		Assert.IsTrue( rebuilt.Resolutions.All( resolution => !resolution.Enabled ) );
	}
}
using System.IO;

namespace Hexagon.V2.Tests.Foundation;

[TestClass]
public sealed class SandboxCompatibilityTests
{
	private static readonly string[] ForbiddenSourceTokens =
	[
		".ConfigureAwait(",
		"Volatile.",
		"ReaderWriterLockSlim",
		"CryptographicOperations.FixedTimeEquals",
		".IsInterface",
		".IsByRef",
		".IsPointer",
		".IsInstanceOfType",
		"ExceptionDispatchInfo",
		"System.Reflection",
		"RuntimeHelpers.",
		"Activator.CreateInstance",
		".GetProperties(",
		".GetFields(",
		".MakeGenericType(",
		".GetGenericArguments(",
		"await using",
		"Task.WhenAll(",
		"TaskScheduler",
		"Environment.ProcessId"
	];

	private static readonly (string Name, string Pattern)[] ForbiddenRawIoSignatures =
	[
		("File static API", @"\bFile\s*\."),
		("Directory static API", @"\bDirectory\s*\."),
		("Path.GetFullPath", @"\bPath\s*\.\s*GetFullPath\s*\("),
		("FileStream", @"\bFileStream\b"),
		("FileInfo", @"\bFileInfo\b"),
		("DirectoryInfo", @"\bDirectoryInfo\b"),
		("FileMode", @"\bFileMode\b"),
		("FileAccess", @"\bFileAccess\b"),
		("FileShare", @"\bFileShare\b"),
		("FileOptions", @"\bFileOptions\b"),
		("SearchOption", @"\bSearchOption\b")
	];

	[TestMethod]
	public void SandboxIndependentV2CodeAvoidsKnownWhitelistViolations()
	{
		var root = FindHexagonRoot();
		var sourceRoots = new[] { Path.Combine( root, "Code", "V2" ) }
			.Where( Directory.Exists )
			.ToArray();

		var violations = new List<string>();
		foreach ( var path in sourceRoots.SelectMany( sourceRoot =>
			Directory.GetFiles( sourceRoot, "*.cs", SearchOption.AllDirectories ) )
			.Where( path => !path.Contains(
				$"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}",
				StringComparison.OrdinalIgnoreCase ) ) )
		{
			var source = File.ReadAllText( path );
			if ( System.Text.RegularExpressions.Regex.IsMatch(
				source,
				@"^\s*(?:private|protected|internal|public)\s+(?:static\s+)?volatile\s+",
				System.Text.RegularExpressions.RegexOptions.Multiline ) )
				violations.Add(
					$"{Path.GetRelativePath( root, path )} declares a volatile field (lowers to forbidden IsVolatile)" );
			if ( System.Text.RegularExpressions.Regex.IsMatch(
				source,
				@"finally\s*\{[^{}]*\bawait\b",
				System.Text.RegularExpressions.RegexOptions.Singleline ) )
				violations.Add(
					$"{Path.GetRelativePath( root, path )} awaits inside a finally block (lowers to forbidden ExceptionDispatchInfo)" );
			if ( System.Text.RegularExpressions.Regex.IsMatch(
				source,
				@"catch(?:\s*\([^)]*\))?\s*\{[^{}]*\bawait\b[^{}]*\bthrow\s*;",
				System.Text.RegularExpressions.RegexOptions.Singleline ) )
				violations.Add(
					$"{Path.GetRelativePath( root, path )} awaits before a bare catch rethrow (lowers to forbidden ExceptionDispatchInfo)" );
			var lines = File.ReadAllLines( path );
			for ( var index = 0; index < lines.Length; index++ )
			{
				foreach ( var token in ForbiddenSourceTokens )
				{
					if ( lines[index].Contains( token, StringComparison.Ordinal ) )
						violations.Add( $"{Path.GetRelativePath( root, path )}:{index + 1} contains '{token}'" );
				}

				foreach ( var signature in FindForbiddenRawIoSignatures( lines[index] ) )
					violations.Add(
						$"{Path.GetRelativePath( root, path )}:{index + 1} uses forbidden raw I/O signature '{signature}'" );
			}
		}

		Assert.HasCount(
			0,
			violations,
			"Known s&box whitelist violations were found:" + Environment.NewLine + string.Join( Environment.NewLine, violations ) );
	}

	[TestMethod]
	public void RawIoSignatureGuardCoversOriginalSb1000SurfaceWithoutFalsePositives()
	{
		// Keep fixtures split so the production-source scanner can never match this test file if its roots expand.
		var separator = string.Empty;
		var forbiddenFixtures = new[]
		{
			"File" + separator + ".Exists( path )",
			"Path" + separator + ".GetFullPath( path )",
			"File" + separator + "Stream? lease",
			"File" + separator + "Info info",
			"Directory" + separator + "Info directory",
			"File" + separator + "Mode.OpenOrCreate",
			"File" + separator + "Access.ReadWrite",
			"File" + separator + "Share.None",
			"File" + separator + "Options.WriteThrough",
			"Search" + separator + "Option.AllDirectories",
			"Directory" + separator + " . EnumerateFiles( root )"
		};

		foreach ( var fixture in forbiddenFixtures )
			Assert.IsNotEmpty(
				FindForbiddenRawIoSignatures( fixture ),
				$"Expected raw I/O fixture to be rejected: {fixture}" );

		var allowedFixtures = new[]
		{
			"var path = issue.Path;",
			"var fileName = record.FileName;",
			"IReadOnlyList<string> directories",
			"storage.ExistsAsync( path, cancellationToken )"
		};

		foreach ( var fixture in allowedFixtures )
			Assert.IsEmpty(
				FindForbiddenRawIoSignatures( fixture ),
				$"Expected non-I/O fixture to remain allowed: {fixture}" );
	}

	private static string[] FindForbiddenRawIoSignatures( string sourceLine ) => ForbiddenRawIoSignatures
		.Where( signature => System.Text.RegularExpressions.Regex.IsMatch(
			sourceLine,
			signature.Pattern,
			System.Text.RegularExpressions.RegexOptions.CultureInvariant ) )
		.Select( signature => signature.Name )
		.ToArray();

	private static string FindHexagonRoot()
	{
		var directory = new DirectoryInfo( AppContext.BaseDirectory );
		while ( directory is not null && !File.Exists( Path.Combine( directory.FullName, "hexagon.sbproj" ) ) )
			directory = directory.Parent;

		Assert.IsNotNull( directory, "Could not locate the Hexagon repository root." );
		return directory.FullName;
	}
}
#nullable enable

using System;
using System.IO;
using System.Text.RegularExpressions;
using Hexagon.V2.Networking;
using Hexagon.V2.Tests.Foundation;

namespace Hexagon.V2.Tests.Networking;

/// <summary>
/// Sustained-run guards for the per-connection command budget, and a pin tying the numbers
/// <c>docs/security.md</c> publishes to the constants that enforce them.
/// <para>
/// The movement validator's failure was that a per-tick allowance was never checked as a rate, so prose
/// describing a bound and code granting one every tick could both read as true while disagreeing by
/// fifty times. Every other rate-limited surface therefore gets the same treatment: the published number
/// is read out of the document and asserted against the constant, and the constant is asserted against
/// what a client can actually extract over a run.
/// </para>
/// </summary>
[TestClass]
public sealed class CommandAdmissionSustainedTests
{
	private const long Frequency = 1000;          // timestamps in milliseconds
	private const int Hz = 50;
	private const int TickMilliseconds = 1000 / Hz;
	private const int Ticks = 10 * Hz;            // ten seconds
	private const double ElapsedSeconds = 10.0;

	[TestMethod]
	public void TheSecurityDocumentPublishesTheBudgetThatIsActuallyEnforced()
	{
		// docs/security.md: "a weighted per-connection token bucket before payload construction:
		// 16-unit burst, 8 units/second refill, and at most 16 active requests."
		// Reading the numbers out of the document makes the prose a claim this suite owns: editing
		// either side without the other fails here rather than shipping a document that describes a
		// system nobody built.
		var document = File.ReadAllText( Path.Combine( RepositoryRoot(), "docs", "security.md" ) );

		var match = Regex.Match( document,
			@"(?<burst>\d+)-unit burst,\s*(?<refill>\d+)\s*units?/second refill,\s*and at most\s*(?<active>\d+)\s*active requests" );
		Assert.IsTrue( match.Success,
			"docs/security.md no longer states the command budget in the form this guard reads. " +
			"Update the guard deliberately rather than letting the published numbers go unchecked." );

		Assert.AreEqual( CommandAdmissionController.BurstUnits, int.Parse( match.Groups["burst"].Value ),
			"Documented burst does not match the enforced constant." );
		Assert.AreEqual( CommandAdmissionController.RefillUnitsPerSecond, int.Parse( match.Groups["refill"].Value ),
			"Documented refill rate does not match the enforced constant." );
		Assert.AreEqual( CommandAdmissionController.MaximumActiveRequests, int.Parse( match.Groups["active"].Value ),
			"Documented active-request cap does not match the enforced constant." );
	}

	[TestMethod]
	public void SustainedAdmissionConvergesOnTheRefillRateNotTheBurst()
	{
		// The burst is a one-off; the refill rate is the ceiling. A bucket that re-granted its burst
		// the way the movement validator re-granted its skin would admit 800 units here, not 96.
		var controller = new CommandAdmissionController( Frequency );
		var admittedCount = 0;

		var run = SustainedEnvelope.Measure( Ticks, 1.0 / Hz, tick =>
		{
			var timestamp = (long)tick * TickMilliseconds;
			var requestId = new CommandRequestId( Guid.NewGuid() );
			var result = controller.TryBegin( requestId, cost: 1, timestamp );
			if ( !result.Accepted ) return SustainedEnvelope.StepOutcome.Rejected;
			controller.Finish( requestId );   // keep the active-request bound out of the way
			admittedCount++;
			return SustainedEnvelope.StepOutcome.Allowed( 1 );
		} );

		var bound = (CommandAdmissionController.RefillUnitsPerSecond * ElapsedSeconds)
			+ CommandAdmissionController.BurstUnits;

		Assert.IsLessThanOrEqualTo( bound, run.TotalWork, SustainedEnvelope.Describe(
			$"Sustained command admission over 10s ({run.WorkPerSecond:F1}/s)",
			run.TotalWork, bound, " units" ) );
		Assert.IsGreaterThan( 0, admittedCount,
			"The run admitted nothing, so the bound above proves nothing about the bucket." );
	}

	[TestMethod]
	public void RejectedAttemptsAreNotRefundedAcrossASustainedRun()
	{
		// docs/security.md: "Rejected, duplicate, and malformed attempts are not refunded." A refund on
		// rejection would make the bucket a no-op under exactly the load it exists to bound.
		var controller = new CommandAdmissionController( Frequency );
		var duplicate = new CommandRequestId( Guid.NewGuid() );

		// Drain the burst with duplicates, which are charged and then refused.
		for ( var i = 0; i < CommandAdmissionController.BurstUnits; i++ )
			controller.TryBegin( duplicate, cost: 1, 0 );

		Assert.IsLessThanOrEqualTo( 0.001, controller.AvailableUnits,
			$"Duplicates were refunded: {controller.AvailableUnits:F2} units remain after draining the burst." );
	}

	[TestMethod]
	public void ACostlierCommandDrainsTheBudgetProportionally()
	{
		// The bucket is weighted, so an expensive command must consume its weight rather than one slot.
		var controller = new CommandAdmissionController( Frequency );
		var admitted = 0;
		for ( var i = 0; i < 100; i++ )
		{
			var requestId = new CommandRequestId( Guid.NewGuid() );
			if ( !controller.TryBegin( requestId, CommandAdmissionController.BurstUnits, 0 ).Accepted ) break;
			controller.Finish( requestId );
			admitted++;
		}

		Assert.AreEqual( 1, admitted,
			"A full-burst-cost command should be admissible exactly once from a full bucket." );
	}

	private static string RepositoryRoot()
	{
		var directory = new DirectoryInfo( AppContext.BaseDirectory );
		while ( directory is not null && !File.Exists( Path.Combine( directory.FullName, "hexagon.sbproj" ) ) )
			directory = directory.Parent;
		Assert.IsNotNull( directory, "Could not locate the Hexagon repository root." );
		return directory!.FullName;
	}
}
#nullable enable

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Networking;

namespace Hexagon.V2.Tests.Networking;

[TestClass]
public sealed class CommandDispatchOrchestratorTests
{
	[TestMethod]
	public void UnauthenticatedCallerIsRejectedBeforeAnyChargeOrResolution()
	{
		var host = new FakeDispatchHost { Authenticated = false };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.IsEmpty( host.Calls, "Unauthenticated traffic must never reach the per-connection bucket." );
		Assert.HasCount( 1, host.Sent );
		Assert.AreEqual( "caller", host.Sent[0].Target );
		Assert.AreEqual( ErrorCode.Unauthorized, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void ChargeHappensBeforeResolutionAndCarriesTheDeclaredCost()
	{
		var host = new FakeDispatchHost();

		CommandDispatchOrchestrator.Dispatch( host, Header(), 3, static () => new RequestCharacterListCommand() );

		Assert.AreEqual( "charge:3", host.Calls[0], "The caller is charged before its scope is inspected." );
		Assert.AreEqual( "resolve:initial", host.Calls[1] );
	}

	[TestMethod]
	public void RateLimitedAdmissionCarriesRetryAfterAndSkipsResolution()
	{
		var host = new FakeDispatchHost
		{
			Admission = CommandAdmissionResult.Reject(
				CommandAdmissionFailure.RateLimited, TimeSpan.FromSeconds( 2 ) )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 4, static () => new RequestCharacterListCommand() );

		CollectionAssert.AreEqual( new[] { "charge:4" }, host.Calls );
		Assert.HasCount( 1, host.Sent );
		Assert.AreEqual( ErrorCode.RateLimited, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void DuplicateRequestIdsConflict()
	{
		var host = new FakeDispatchHost
		{
			Admission = CommandAdmissionResult.Reject( CommandAdmissionFailure.Duplicate )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.AreEqual( ErrorCode.Conflict, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void ResolutionFailureIsForwardedAndTheAcceptedChargeIsClosed()
	{
		var host = new FakeDispatchHost
		{
			Resolution = OperationResult<string>.Failure( ErrorCode.Unauthorized, "scope mismatch" )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		CollectionAssert.AreEqual(
			new[] { "charge:1", "resolve:initial", "finish-rejected" },
			host.Calls );
		Assert.AreEqual( "scope mismatch", host.Sent[0].Result.Error!.Message );
	}

	[TestMethod]
	public void EmptyRequestIdCompletesWithInvalidArgument()
	{
		var host = new FakeDispatchHost();
		// Wire deserialization can materialize a default header; the constructor itself
		// rejects empty request ids, so the guard is only reachable through default structs.
		var header = default( ClientCommandHeader );

		CommandDispatchOrchestrator.Dispatch( host, header, 1, static () => new RequestCharacterListCommand() );

		CollectionAssert.Contains( host.Calls, "complete" );
		Assert.HasCount( 1, host.Sent );
		Assert.AreEqual( "actor", host.Sent[0].Target );
		Assert.AreEqual( ErrorCode.InvalidArgument, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void ThrowingCommandFactoryFailsClosedWithoutStartingAHostOperation()
	{
		var host = new FakeDispatchHost();

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1,
			static () => throw new FormatException( "bad wire payload" ) );

		Assert.HasCount( 1, host.MalformedPayloads );
		CollectionAssert.DoesNotContain( host.Calls, "host-operation" );
		Assert.AreEqual( ErrorCode.InvalidArgument, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void OversizedPayloadIsRejectedBeforeStableResolutionAndExecution()
	{
		var host = new FakeDispatchHost();

		CommandDispatchOrchestrator.Dispatch( host, Header(), 2,
			static () => new SendChatCommand( "ic", new string( 'a', 1_000_000 ) ) );

		CollectionAssert.DoesNotContain( host.Calls, "resolve:stable" );
		CollectionAssert.DoesNotContain( host.Calls, "host-operation" );
		Assert.HasCount( 1, host.Sent );
		Assert.IsFalse( host.Sent[0].Result.Succeeded );
	}

	[TestMethod]
	public void StableCharacterCommandsAreReResolvedAfterPayloadValidation()
	{
		var host = new FakeDispatchHost();

		CommandDispatchOrchestrator.Dispatch( host, Header(), 2,
			static () => new SendChatCommand( "ic", "hello" ) );

		CollectionAssert.AreEqual(
			new[]
			{
				"charge:2", "resolve:initial", "resolve:stable",
				"host-operation", "lease-check", "execute", "complete"
			},
			host.Calls );
		Assert.AreEqual( "stable-actor", host.Sent[0].Target,
			"Execution and completion must use the re-resolved stable actor." );
	}

	[TestMethod]
	public void StableResolutionFailureCompletesWithoutExecuting()
	{
		var host = new FakeDispatchHost
		{
			StableResolution = OperationResult<string>.Failure(
				ErrorCode.Unauthorized, "character changed" )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 2,
			static () => new SendChatCommand( "ic", "hello" ) );

		CollectionAssert.DoesNotContain( host.Calls, "execute" );
		Assert.AreEqual( "character changed", host.Sent[0].Result.Error!.Message );
	}

	[TestMethod]
	public void CharacterLifecycleCommandsSkipTheStableCharacterResolution()
	{
		foreach ( var factory in new Func<ClientCommand>[]
		{
			static () => new RequestCharacterListCommand(),
			static () => new LoadCharacterCommand( CharacterId.New() ),
			static () => new DeleteCharacterCommand( CharacterId.New() ),
			static () => new UnloadCharacterCommand()
		} )
		{
			var host = new FakeDispatchHost();
			CommandDispatchOrchestrator.Dispatch( host, Header(), 1, factory );
			CollectionAssert.DoesNotContain( host.Calls, "resolve:stable",
				$"{factory().GetType().Name} must dispatch without an active character." );
			Assert.AreEqual( "actor", host.Sent[0].Target );
		}
	}

	[TestMethod]
	public void HostOperationRefusalCompletesWithDrainingConflict()
	{
		var host = new FakeDispatchHost { AcceptHostOperation = false };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.AreEqual( ErrorCode.Conflict, host.Sent[0].Result.Error!.Code );
		StringAssert.Contains( host.StartedOperationName!, "command:" );
	}

	[TestMethod]
	public void StaleLeaseInsideTheHostOperationRejectsBeforeExecution()
	{
		var host = new FakeDispatchHost { LeaseCurrent = false };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		CollectionAssert.DoesNotContain( host.Calls, "execute" );
		Assert.AreEqual( ErrorCode.Unauthorized, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void MissingHostApplicationCompletesWithInternalError()
	{
		var host = new FakeDispatchHost { ExecutionAvailable = false };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		CollectionAssert.DoesNotContain( host.Calls, "execute" );
		Assert.AreEqual( ErrorCode.InternalError, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void ExecutionFaultBecomesInternalError()
	{
		var host = new FakeDispatchHost
		{
			Execution = static () => throw new InvalidOperationException( "handler exploded" )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.HasCount( 1, host.ExecutionFaults );
		Assert.AreEqual( ErrorCode.InternalError, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void ExecutionFaultUnderCancellationBecomesUnauthorized()
	{
		using var cancellation = new CancellationTokenSource();
		cancellation.Cancel();
		var host = new FakeDispatchHost
		{
			Cancellation = cancellation.Token,
			Execution = static () => throw new OperationCanceledException()
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.AreEqual( ErrorCode.Unauthorized, host.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void DisconnectedCompletionSendsNothing()
	{
		var host = new FakeDispatchHost { CompletionStatus = CommandCompletionStatus.Disconnected };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.IsEmpty( host.Sent );
	}

	[TestMethod]
	public void StaleCompletionKeepsASuccessfulExecutionResultButRejectsAFailedOne()
	{
		var success = new FakeDispatchHost { CompletionStatus = CommandCompletionStatus.Stale };
		CommandDispatchOrchestrator.Dispatch( success, Header(), 1, static () => new RequestCharacterListCommand() );
		Assert.IsTrue( success.Sent[0].Result.Succeeded,
			"A successful application result stays authoritative even under a stale lease." );

		var failure = new FakeDispatchHost
		{
			CompletionStatus = CommandCompletionStatus.Stale,
			Execution = static () => ValueTask.FromResult(
				OperationResult.Failure( ErrorCode.NotFound, "gone" ) )
		};
		CommandDispatchOrchestrator.Dispatch( failure, Header(), 1, static () => new RequestCharacterListCommand() );
		Assert.AreEqual( ErrorCode.Unauthorized, failure.Sent[0].Result.Error!.Code );
	}

	[TestMethod]
	public void SuccessfulExecutionForwardsTheApplicationResult()
	{
		var host = new FakeDispatchHost
		{
			Execution = static () => ValueTask.FromResult(
				OperationResult.Failure( ErrorCode.PolicyDenied, "application said no" ) )
		};

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.AreEqual( ErrorCode.PolicyDenied, host.Sent[0].Result.Error!.Code );
		Assert.AreEqual( "application said no", host.Sent[0].Result.Error!.Message );
	}

	[TestMethod]
	public void ThrowingCompletionHookIsObservedWithoutEscaping()
	{
		var host = new FakeDispatchHost { CompleteThrows = true };

		CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );

		Assert.HasCount( 1, host.CompletionFaults );
		Assert.IsEmpty( host.Sent );
	}

	[TestMethod]
	public void CommandCostTableIsPinned()
	{
		var table = new Dictionary<string, int>( StringComparer.Ordinal )
		{
			[nameof( ClientCommandCosts.CharacterList )] = ClientCommandCosts.CharacterList,
			[nameof( ClientCommandCosts.CreateCharacter )] = ClientCommandCosts.CreateCharacter,
			[nameof( ClientCommandCosts.LoadCharacter )] = ClientCommandCosts.LoadCharacter,
			[nameof( ClientCommandCosts.DeleteCharacter )] = ClientCommandCosts.DeleteCharacter,
			[nameof( ClientCommandCosts.UnloadCharacter )] = ClientCommandCosts.UnloadCharacter,
			[nameof( ClientCommandCosts.MoveItem )] = ClientCommandCosts.MoveItem,
			[nameof( ClientCommandCosts.ItemAction )] = ClientCommandCosts.ItemAction,
			[nameof( ClientCommandCosts.DropItem )] = ClientCommandCosts.DropItem,
			[nameof( ClientCommandCosts.PickupItem )] = ClientCommandCosts.PickupItem,
			[nameof( ClientCommandCosts.Chat )] = ClientCommandCosts.Chat,
			[nameof( ClientCommandCosts.CancelAction )] = ClientCommandCosts.CancelAction,
			[nameof( ClientCommandCosts.BeginInteraction )] = ClientCommandCosts.BeginInteraction,
			[nameof( ClientCommandCosts.ContinueInteraction )] = ClientCommandCosts.ContinueInteraction,
			[nameof( ClientCommandCosts.CloseInteraction )] = ClientCommandCosts.CloseInteraction,
			[nameof( ClientCommandCosts.SchemaCommandFallback )] = ClientCommandCosts.SchemaCommandFallback
		};
		var expected = new Dictionary<string, int>( StringComparer.Ordinal )
		{
			["CharacterList"] = 1,
			["CreateCharacter"] = 4,
			["LoadCharacter"] = 4,
			["DeleteCharacter"] = 4,
			["UnloadCharacter"] = 1,
			["MoveItem"] = 4,
			["ItemAction"] = 4,
			["DropItem"] = 4,
			["PickupItem"] = 4,
			["Chat"] = 2,
			["CancelAction"] = 1,
			["BeginInteraction"] = 2,
			["ContinueInteraction"] = 1,
			["CloseInteraction"] = 1,
			["SchemaCommandFallback"] = 8
		};
		CollectionAssert.AreEquivalent( expected, table );
	}

	[TestMethod]
	public void OnlyCharacterLifecycleCommandsAreExemptFromTheStableCharacterRequirement()
	{
		Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter( new RequestCharacterListCommand() ) );
		Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter(
			new LoadCharacterCommand( CharacterId.New() ) ) );
		Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter(
			new DeleteCharacterCommand( CharacterId.New() ) ) );
		Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter( new UnloadCharacterCommand() ) );
		Assert.IsTrue( CommandDispatchOrchestrator.RequiresStableCharacter( new SendChatCommand( "ic", "hi" ) ) );
		Assert.IsTrue( CommandDispatchOrchestrator.RequiresStableCharacter(
			new CloseInteractionCommand( InteractionSessionId.New() ) ) );
	}

	private static ClientSessionScope Scope() =>
		new( ClientSessionNonce.New(), ConnectionEpoch.New() );

	private static ClientCommandHeader Header() =>
		new( Scope(), new CommandRequestId( Guid.NewGuid() ) );

	private sealed class FakeDispatchHost : ICommandDispatchHost<string>
	{
		public List<string> Calls { get; } = new();
		public bool Authenticated { get; set; } = true;
		public bool ExecutionAvailable { get; set; } = true;
		public CommandAdmissionResult Admission { get; set; } = CommandAdmissionResult.Success();
		public OperationResult<string> Resolution { get; set; } = OperationResult<string>.Success( "actor" );
		public OperationResult<string> StableResolution { get; set; } = OperationResult<string>.Success( "stable-actor" );
		public bool LeaseCurrent { get; set; } = true;
		public bool AcceptHostOperation { get; set; } = true;
		public bool CompleteThrows { get; set; }
		public Func<ValueTask<OperationResult>>? Execution { get; set; }
		public CancellationToken Cancellation { get; set; }
		public CommandCompletionStatus CompletionStatus { get; set; } = CommandCompletionStatus.Current;
		public List<(string Target, OperationResult Result)> Sent { get; } = new();
		public List<Exception> MalformedPayloads { get; } = new();
		public List<Exception> ExecutionFaults { get; } = new();
		public List<Exception> CompletionFaults { get; } = new();
		public string? StartedOperationName { get; private set; }

		public bool CallerIsAuthenticated => Authenticated;
		public bool IsExecutionAvailable => ExecutionAvailable;

		public CommandAdmissionResult TryBeginCommand( CommandRequestId requestId, int cost )
		{
			Calls.Add( $"charge:{cost}" );
			return Admission;
		}

		public OperationResult<string> ResolveActor( ClientSessionScope scope, bool requireStableCharacter )
		{
			Calls.Add( requireStableCharacter ? "resolve:stable" : "resolve:initial" );
			return requireStableCharacter ? StableResolution : Resolution;
		}

		public void FinishRejectedCommand( CommandRequestId requestId ) => Calls.Add( "finish-rejected" );

		public bool IsCommandLeaseCurrent( string actor )
		{
			Calls.Add( "lease-check" );
			return LeaseCurrent;
		}

		public bool TryStartHostOperation( string name, Func<Task> operation )
		{
			Calls.Add( "host-operation" );
			StartedOperationName = name;
			if ( !AcceptHostOperation ) return false;
			operation().GetAwaiter().GetResult();
			return true;
		}

		public CancellationToken CommandCancellation( string actor ) => Cancellation;

		public ValueTask<OperationResult> ExecuteAsync(
			string actor,
			ClientCommand command,
			CancellationToken cancellationToken )
		{
			Calls.Add( "execute" );
			return Execution?.Invoke() ?? ValueTask.FromResult( OperationResult.Success() );
		}

		public CommandCompletionStatus CompleteCommand( string actor, CommandRequestId requestId )
		{
			Calls.Add( "complete" );
			if ( CompleteThrows ) throw new InvalidOperationException( "completion registry exploded" );
			return CompletionStatus;
		}

		public void SendResultToCaller( ClientSessionScope scope, CommandRequestId requestId, OperationResult result ) =>
			Sent.Add( ("caller", result) );

		public void SendResult( string actor, CommandRequestId requestId, OperationResult result ) =>
			Sent.Add( (actor, result) );

		public void OnMalformedPayload( Exception exception ) => MalformedPayloads.Add( exception );
		public void OnExecutionFault( CommandRequestId requestId, Exception exception ) => ExecutionFaults.Add( exception );
		public void OnCompletionFault( CommandRequestId requestId, Exception exception ) => CompletionFaults.Add( exception );
	}
}
#nullable enable

using System;
using Hexagon.V2.Runtime;
using static Hexagon.V2.Tests.Foundation.SustainedEnvelope.EngineDefaults;

namespace Hexagon.V2.Tests.Runtime;

/// <summary>
/// Guards for the windowed movement audit.
/// <para>
/// The host observes an INTERPOLATED proxy transform, not what the client reported, so its per-tick
/// deltas are artifacts of network buffering. The audit's soundness rests on one property —
/// <b>summing deltas over a window is invariant to how the travel is chunked</b> — and
/// <see cref="AnIdenticalPathIsMeasuredTheSameHoweverItIsChunked"/> is the test that pins it. Everything
/// else here is a bound stated over a window; nothing is stated per tick, because per tick the signal
/// carries no truth.
/// </para>
/// </summary>
[TestClass]
public sealed class HexMovementValidatorTests
{
	private static readonly HexMovementEnvelope Envelope = HexMovementEnvelope.Default;

	private static MovementSample At( float x, float y, float z ) => new( x, y, z );

	/// <summary>
	/// Walks a straight horizontal path of <paramref name="totalDistance"/> over
	/// <paramref name="seconds"/>, delivered in <paramref name="samples"/> chunks, and returns the
	/// decision of the window that closes.
	/// </summary>
	private static HexMovementValidator.Decision WalkHorizontally(
		float totalDistance, double seconds, int samples, bool frozen = false )
	{
		var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
		var last = default( HexMovementValidator.Decision );
		for ( var i = 1; i <= samples; i++ )
		{
			var t = seconds * i / samples;
			var x = totalDistance * i / samples;
			(last, audit) = HexMovementValidator.Observe(
				audit, At( (float)x, 0, 0 ), t, RunSpeed, JumpSpeed, frozen, Envelope );
		}
		return last;
	}

	[TestMethod]
	public void AnIdenticalPathIsMeasuredTheSameHoweverItIsChunked()
	{
		// THE property the whole design rests on. The interpolation buffer decides how travel is sliced
		// — smoothly, in bursts of three ticks, or irregularly — and the audit must be blind to that.
		// Per-tick checking is not: measured live, an honest sprint arrived as 19.2-unit bursts against
		// a ~15-unit per-tick budget and was refused twelve times.
		var smooth = WalkHorizontally( 300f, 1.0, samples: 60 );
		var bursty = WalkHorizontally( 300f, 1.0, samples: 20 );
		var coarse = WalkHorizontally( 300f, 1.0, samples: 5 );

		Assert.AreEqual( smooth.HorizontalPath, bursty.HorizontalPath, 0.01f,
			"Chunking changed the measured path; the audit is not sampling-invariant." );
		Assert.AreEqual( smooth.HorizontalPath, coarse.HorizontalPath, 0.01f,
			"Chunking changed the measured path; the audit is not sampling-invariant." );
	}

	[TestMethod]
	public void AnHonestSprintIsAcceptedHoweverItIsSampled()
	{
		// A player at exactly run speed for a full window, delivered every way the network might.
		foreach ( var samples in new[] { 60, 20, 12, 5 } )
		{
			var decision = WalkHorizontally( RunSpeed, 1.0, samples );
			Assert.IsTrue( decision.WindowClosed, $"Window did not close for {samples} samples." );
			Assert.IsFalse( decision.Corrected,
				$"Honest sprint flagged at {samples} samples/window: " +
				$"path {decision.HorizontalPath:F1} vs budget {decision.HorizontalBudget:F1}." );
		}
	}

	[TestMethod]
	public void ASpeedCheatIsFlagged()
	{
		// Twice run speed for a window: 640 units against a 496-unit budget.
		var decision = WalkHorizontally( RunSpeed * 2f, 1.0, samples: 60 );
		Assert.AreEqual( HexMovementValidator.Verdict.WindowExceeded, decision.Verdict,
			$"path {decision.HorizontalPath:F1} vs budget {decision.HorizontalBudget:F1}" );
	}

	[TestMethod]
	public void SustainedClimbBeyondTheGroundAngleIsFlagged()
	{
		// Rising far faster than the steepest standable surface would permit for the distance walked.
		var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
		var decision = default( HexMovementValidator.Decision );
		for ( var i = 1; i <= 60; i++ )
		{
			// 100 units of horizontal travel, 900 units of rise, in one window.
			(decision, audit) = HexMovementValidator.Observe(
				audit, At( 100f * i / 60f, 0, 900f * i / 60f ), i / 60.0,
				RunSpeed, JumpSpeed, false, Envelope );
		}
		Assert.AreEqual( HexMovementValidator.Verdict.WindowExceeded, decision.Verdict,
			$"rise {decision.NetRise:F1} vs budget {decision.RiseBudget:F1}" );
	}

	[TestMethod]
	public void AJumpAndLandIsNotAClimb()
	{
		// Net rise over a window is what "climbing" means. Jumping repeatedly nets nothing, and must
		// not accumulate into a violation the way summed positive rise would.
		var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
		var decision = default( HexMovementValidator.Decision );
		for ( var i = 1; i <= 60; i++ )
		{
			var t = i / 60.0;
			var phase = Math.Sin( t * Math.PI * 4 );           // two full jump arcs in the window
			var z = (float)(Math.Max( phase, 0 ) * 56.0);      // apex ~ one jump
			(decision, audit) = HexMovementValidator.Observe(
				audit, At( 200f * i / 60f, 0, z ), t, RunSpeed, JumpSpeed, false, Envelope );
		}
		Assert.IsFalse( decision.Corrected,
			$"Jumping while running was flagged: rise {decision.NetRise:F1} vs {decision.RiseBudget:F1}" );
	}

	[TestMethod]
	public void ATeleportIsRefusedImmediatelyWithoutWaitingForTheWindow()
	{
		var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
		var (decision, _) = HexMovementValidator.Observe(
			audit, At( 5000, 0, 0 ), 0.016, RunSpeed, JumpSpeed, false, Envelope );

		Assert.AreEqual( HexMovementValidator.Verdict.Teleport, decision.Verdict );
		Assert.IsFalse( decision.WindowClosed, "A teleport is caught on the sample, not at window end." );
	}

	[TestMethod]
	public void AnInterpolationSizedBurstIsNotMistakenForATeleport()
	{
		// The bursts that broke per-tick checking (~19 units) must be nowhere near the teleport guard.
		var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
		var (decision, _) = HexMovementValidator.Observe(
			audit, At( 19.2f, 0, 0 ), 0.016, RunSpeed, JumpSpeed, false, Envelope );
		Assert.AreEqual( HexMovementValidator.Verdict.Accepted, decision.Verdict );
	}

	[TestMethod]
	public void FrozenPlayersAreHeldToTheDriftAllowance()
	{
		var moved = WalkHorizontally( 200f, 1.0, samples: 60, frozen: true );
		Assert.AreEqual( HexMovementValidator.Verdict.WindowExceeded, moved.Verdict );

		var jitter = WalkHorizontally( Envelope.FrozenDriftAllowance * 0.5f, 1.0, samples: 60, frozen: true );
		Assert.IsFalse( jitter.Corrected, "Jitter within the frozen allowance must not be flagged." );
	}

	[TestMethod]
	public void CorrectsNonFinitePosition()
	{
		var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
		var (decision, _) = HexMovementValidator.Observe(
			audit, At( float.NaN, 0, 0 ), 0.016, RunSpeed, JumpSpeed, false, Envelope );
		Assert.AreEqual( HexMovementValidator.Verdict.Teleport, decision.Verdict );
	}

	[TestMethod]
	public void AnUnprimedAuditAdoptsTheFirstSampleWithoutFlagging()
	{
		var (decision, audit) = HexMovementValidator.Observe(
			MovementAudit.Unprimed, At( 500, 500, 500 ), 0, RunSpeed, JumpSpeed, false, Envelope );
		Assert.IsFalse( decision.Corrected );
		Assert.IsTrue( audit.Primed );
		Assert.AreEqual( 500f, audit.Anchor.X );
	}

	[TestMethod]
	public void AMalformedEnvelopeIsRejectedRatherThanAuditingNothing()
	{
		Assert.IsTrue( HexMovementEnvelope.Default.IsWellFormed( out _ ) );

		var permissive = HexMovementEnvelope.Default with { HorizontalTolerance = 50f };
		Assert.IsFalse( permissive.IsWellFormed( out var error ) );
		Assert.IsNotEmpty( error );

		var blind = HexMovementEnvelope.Default with { AuditWindowSeconds = 999f };
		Assert.IsFalse( blind.IsWellFormed( out _ ) );
	}
}
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
using HumanoidRetargeter.Core.Target;
using Xunit;

namespace HumanoidRetargeter.Tests;

public class Kv3ModelDocNumberTests
{
    [Theory]
    [InlineData(1.0380962e-05)]
    [InlineData(-1.2e-12)]
    [InlineData(1.25e25)]
    [InlineData(double.Epsilon)]
    [InlineData(double.MaxValue)]
    public void ModelDocNumbersHaveNoExponentAndRoundTripExactly(double value)
    {
        var doc = Kv3.Parse(VmdlWriter.Kv3Header + "\n{ value = 0.0 }");
        ((KvObject)doc.Root)["value"] = new KvDouble(value);
        var text = Kv3.Serialize(doc);
        var number = text[(text.IndexOf("value = ", StringComparison.Ordinal) + 8)..].Trim().TrimEnd('}').Trim();
        Assert.DoesNotContain("E", number);
        Assert.DoesNotContain("e", number);
        Assert.Equal(value, ((KvDouble)((KvObject)Kv3.Parse(text).Root)["value"]).Value);
    }
}
using HumanoidRetargeter.Core.Target;
using Xunit;

namespace HumanoidRetargeter.Tests.Target;

/// <summary>Bone renaming that follows a graph into its subgraphs (the stock Citizen graph keeps its logic there).</summary>
public class SmartPortGraphsTests
{
    static string Graph(string cls, string body) => VmdlWriter.Kv3Header + "{ _class = \"" + cls + "\" " + body + " }";

    static readonly Dictionary<string, string> Renames = new()
    {
        ["pelvis"] = "ValveBiped_Bip01_Pelvis", ["hand_R"] = "ValveBiped_Bip01_R_Hand", ["hold_R"] = "hold_R",
    };

    static Dictionary<string, string> Files(params (string Path, string Text)[] files)
        => files.ToDictionary(f => f.Path, f => f.Text, StringComparer.OrdinalIgnoreCase);

    [Fact]
    public void SubgraphsNamingRenamedBonesAreCopiedAndReferenced()
    {
        var entry = Graph("CAnimationGraph", """
            m_nodes = [
              { m_subGraphFilename = "models/citizen/subgraphs/core.vsubgrph" },
              { m_subGraphFilename = "models\\citizen\\subgraphs\\face.vsubgrph" },
              { m_bone = "hand_R" }
            ]
            """);
        var files = Files(
            ("models/citizen/subgraphs/core.vsubgrph", Graph("CAnimationSubGraph", "m_hipBoneName = \"pelvis\" m_attachmentName = \"hold_R\"")),
            ("models/citizen/subgraphs/face.vsubgrph", Graph("CAnimationSubGraph", "m_sequenceName = \"Eyes_Blink\"")));
        var copy = SmartPortGraphs.Rebase(entry, Renames, p => files.GetValueOrDefault(p), "models/x_src/subgraphs");

        var core = Assert.Single(copy.Subgraphs);
        Assert.Equal("models/x_src/subgraphs/core.vsubgrph", core.Key);
        Assert.Contains("m_hipBoneName = \"ValveBiped_Bip01_Pelvis\"", core.Value);
        Assert.Contains("m_attachmentName = \"hold_R\"", core.Value);
        Assert.Contains("\"models/x_src/subgraphs/core.vsubgrph\"", copy.Graph);
        Assert.Contains("face.vsubgrph", copy.Graph); // unchanged: still the shipped one
        Assert.DoesNotContain("x_src/subgraphs/face", copy.Graph);
        Assert.Contains("\"ValveBiped_Bip01_R_Hand\"", copy.Graph);
        Assert.Empty(copy.Unread);
    }

    [Fact]
    public void AChangeDeepInsideCopiesEveryGraphAboveIt()
    {
        var entry = Graph("CAnimationGraph", "m_subGraphFilename = \"a.vsubgrph\"");
        var files = Files(
            ("a.vsubgrph", Graph("CAnimationSubGraph", "m_subGraphFilename = \"b.vsubgrph\"")),
            ("b.vsubgrph", Graph("CAnimationSubGraph", "m_hipBoneName = \"pelvis\" m_subGraphFilename = \"a.vsubgrph\"")));
        var copy = SmartPortGraphs.Rebase(entry, Renames, p => files.GetValueOrDefault(p), "out");
        Assert.Equal(new[] { "out/a.vsubgrph", "out/b.vsubgrph" }, copy.Subgraphs.Keys.OrderBy(k => k, StringComparer.Ordinal));
        Assert.Contains("\"out/b.vsubgrph\"", copy.Subgraphs["out/a.vsubgrph"]);
        Assert.Contains("\"out/a.vsubgrph\"", copy.Graph);
        Assert.Contains("\"a.vsubgrph\"", copy.Subgraphs["out/b.vsubgrph"]); // a reference back up the chain is not followed again
    }

    [Fact]
    public void UnreadableSubgraphsAreListedAndIdentityNamesChangeNothing()
    {
        var entry = Graph("CAnimationGraph", "m_subGraphFilename = \"missing.vsubgrph\" m_bone = \"hold_R\"");
        var copy = SmartPortGraphs.Rebase(entry, Renames, _ => null, "out");
        Assert.Empty(copy.Subgraphs);
        Assert.Equal(new[] { "missing.vsubgrph" }, copy.Unread);
        Assert.Contains("\"missing.vsubgrph\"", copy.Graph);
    }

    [Fact]
    public void SameNamedSubgraphsFromDifferentFoldersGetDistinctCopies()
    {
        var entry = Graph("CAnimationGraph", "m_nodes = [ { m_subGraphFilename = \"one/core.vsubgrph\" }, { m_subGraphFilename = \"two/core.vsubgrph\" } ]");
        var files = Files(
            ("one/core.vsubgrph", Graph("CAnimationSubGraph", "m_bone = \"pelvis\"")),
            ("two/core.vsubgrph", Graph("CAnimationSubGraph", "m_bone = \"hand_R\"")));
        var copy = SmartPortGraphs.Rebase(entry, Renames, p => files.GetValueOrDefault(p), "out");
        Assert.Equal(new[] { "out/core.vsubgrph", "out/core_2.vsubgrph" }, copy.Subgraphs.Keys.OrderBy(k => k, StringComparer.Ordinal));
    }
}
using System.Numerics;
using HumanoidRetargeter.Core.Mapping;
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Skeleton;
using HumanoidRetargeter.Core.Target;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Core.Skeleton.Skeleton;
using HumanoidRetargeter.Core;

namespace HumanoidRetargeter.Tests.Target;

public class StockBindPoseTests
{
    private const string RigJson = """
    { "name": "stock", "bones": [
        { "name": "pelvis", "parent": null, "class": "Animated", "role": "Hips", "local_pos": [0, 100, 0], "local_rot_xyzw": [0, 0, 0, 1], "tail_world": [0, 110, 0] },
        { "name": "helper", "parent": "pelvis", "class": "ConstraintDriven", "local_pos": [1, 0, 0], "local_rot_xyzw": [0, 0, 0, 1] },
        { "name": "root_IK", "parent": null, "class": "IkBaked", "local_pos": [0, 0, 0], "local_rot_xyzw": [0, 0, 0, 1] }
    ] }
    """;

    [Fact]
    public void RebindingPreservesCuratedHelpersAndAdoptsCompiledRest()
    {
        var stock = TargetRig.Load(RigJson);
        var rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2);
        var definitions = stock.Skeleton.Bones.Select(b => new BoneDefinition(b.Name,
            b.ParentIndex < 0 ? null : stock.Skeleton[b.ParentIndex].Name,
            b.ParentIndex < 0 ? new XForm(Vector3.Transform(b.RestLocal.Pos, rotation), rotation) : b.RestLocal)).ToList();
        definitions.Add(new BoneDefinition("custom_extra", "pelvis", XForm.Identity));
        var bind = SkeletonModel.Create(definitions);
        var rig = stock.WithBindPose(bind);
        Assert.Same(bind, rig.Skeleton);
        Assert.True(rig.HelpersAreConstraintDriven);
        Assert.Equal(BoneClass.ConstraintDriven, rig.ClassOf(bind.IndexOf("helper")));
        Assert.Equal(BoneClass.IkBaked, rig.ClassOf(bind.IndexOf("root_IK")));
        Assert.Equal(BoneClass.Animated, rig.ClassOf(bind.IndexOf("custom_extra")));
        Assert.Equal(bind.IndexOf("pelvis"), rig.BoneForRole(BoneRole.Hips));
        Assert.True(Vector3.Distance(new Vector3(-110, 0, 0), rig.TailWorldOf(bind.IndexOf("pelvis"))!.Value) < .001f);
        Assert.Equal(new Vector3(0, 100, 0), stock.Skeleton.RestWorld[stock.Skeleton.IndexOf("pelvis")].Pos);
    }

    [Fact]
    public void MissingMappedBonesFailRatherThanDroppingMotion()
        => Assert.Throws<ArgumentException>(() => TargetRig.Load(RigJson).WithBindPose(
            SkeletonModel.Create(new[] { new BoneDefinition("unrelated", null, XForm.Identity) })));

    [Fact]
    public void PrefabMeshUsesTheSameDmxRootCorrectionAsAnExplicitMesh()
    {
        var rig = TargetRig.Load(RigJson);
        var frames = new[] { rig.Skeleton.Bones.Select(b => b.RestLocal).ToArray() };
        var prefab = new RetargetTargetSpec { Rig = rig, VmdlScale = .3937f, CompensateDmxRootYaw = true };
        var explicitMesh = new RetargetTargetSpec { Rig = rig, VmdlScale = .3937f, MeshFilePath = "body.fbx" };
        var expected = Retargeter.TestHook_CompensateEmbeddedMeshRootYaw(frames, explicitMesh);
        var actual = Retargeter.TestHook_CompensateEmbeddedMeshRootYaw(frames, prefab);
        Assert.Equal(expected[0], actual[0]);
        Assert.NotEqual(frames[0][0].Rot, actual[0][0].Rot);
        Assert.Equal(rig.Skeleton[0].RestLocal, frames[0][0]);
    }
}
using System.Numerics;
using System.Text;
using System.Text.Json;
using HumanoidRetargeter.Core.Formats.Bvh;
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Skeleton;
using Xunit;
using Skel = HumanoidRetargeter.Core.Skeleton.Skeleton;

namespace HumanoidRetargeter.Tests.Formats;

// =============================================================================================
// SPACE / CONVENTION RESOLUTION (determined from Blender 5.1 io_anim_bvh/import_bvh.py and
// verified empirically against fixtures/anim_truth/freebvh.json, produced by
// research/extract_anim.py = import_anim.bvh(axis_forward='-Z', axis_up='Y', global_scale=1)):
//
//  1. AXES. Blender's BVH importer builds global_matrix = Rx(+90 deg) (file Y-up -> Blender
//     Z-up) and — unlike the FBX importer — BAKES it into the armature data
//     (`arm_ob.matrix_world = global_matrix; transform_apply(rotation=True)`, import_bvh.py).
//     The object ends up with identity transform (object_scale == 1). Therefore BOTH the
//     truth's rest data (`bones[].rest_world_*`, armature space) and its frame data
//     (`frames[][].wp/wr`, world space) are in Blender Z-up space, and converting EITHER back
//     to native BVH space is the same fixed rotation applied on the left:
//     p_bvh = Rx(-90 deg) * p_blender, q_bvh = Rx(-90 deg) * q_blender.
//     (Verified: file's Spine OFFSET (0, 9.9235, -1.2273) appears in the truth as rest world
//     (0, 1.2273, 9.9235) = Rx(+90 deg) of the native offset.)
//
//  2. BONE ORIENTATION. Blender bones are oriented along head->tail with the joint's world
//     position at the HEAD; BVH joints have identity rest rotation. The importer keys pose
//     rotations as basis = rest^-1 * R_bvh * rest, which makes the pose bone's world rotation
//     q_pose(t) = Q_joint(t) * q_rest (column-vector quaternion product, our a*b-applies-b-
//     first convention). The BVH joint world rotation is therefore recovered per bone as
//     Q_joint(t) = q_pose_bvh(t) * conj(q_rest_bvh); positions compare directly (heads).
//
//  3. END SITES. Blender turns End Sites into bone TAILS (no extra bones; the "<name>_end"
//     naming exists only in its non-armature OBJECT import path). Our importer synthesizes a
//     leaf bone "<parent>_end" per End Site, so: our joint count == truth bone count + number
//     of End Sites, and every extra bone name ends with "_end".
//
//  4. FRAME ALIGNMENT. Blender places BVH motion line 0 at action frame 1 (it prepends and
//     then skips an internal rest frame), and extract_anim.py samples from frame_start = 1 on
//     the same 30 fps grid our importer uses, so truth frame i == our clip frame i (the file's
//     30.000003 fps vs 30 fps grid mismatch is ~1e-7 s per frame — negligible over 40 frames;
//     Blender fcurves are keyed LINEAR so subframe evaluation is well-defined).
//
//  5. UNITS. global_scale=1, so truth values are in raw file units. freebvh is Mixamo-derived
//     (centimeters, rest height ~185) -> our meters-vs-cm heuristic keeps scale 1 and values
//     compare 1:1.
//
// TOLERANCES per the task gate: rest offsets ~exact (0.02 cm); animation world rotation
// within 1 deg and world position within 0.5% of skeleton height across 10 sampled frames.
// Observed residuals are float-precision noise (max 0.0002 cm / 0.0004 deg over all 40
// truth frames x 55 joints), so the gates have ~2500x margin. A deliberately flipped
// rotation-composition order fails 55/55 joints — the comparison is discriminating.
// =============================================================================================
public class BvhImporterTests
{
    private const float KDegPerRad = 180f / MathF.PI;

    private static byte[] Fixture(string name)
        => File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "fixtures", "bvh", name));

    private static SourceScene ImportFixture(string name, float fps = 30f)
        => BvhImporter.Import(Fixture(name), new BvhImportOptions { SampleFps = fps });

    // ---- ground truth -----------------------------------------------------------------

    private sealed record TruthBone(
        string Name, string? Parent, Vector3 RestWorldPos, Quaternion RestWorldRot);

    private sealed record TruthFrameBone(Vector3 WorldPos, Quaternion WorldRot);

    private sealed class Truth
    {
        public required int NSamples;
        public required List<TruthBone> Bones;
        public required List<TruthFrameBone[]> Frames;
    }

    private static Truth LoadTruth(string name)
    {
        var path = Path.Combine(AppContext.BaseDirectory, "fixtures", "anim_truth", name);
        using var doc = JsonDocument.Parse(File.ReadAllBytes(path));
        var r = doc.RootElement;

        static Vector3 V3(JsonElement e) => new(
            e[0].GetSingle(), e[1].GetSingle(), e[2].GetSingle());
        static Quaternion Q(JsonElement e) => new(
            e[0].GetSingle(), e[1].GetSingle(), e[2].GetSingle(), e[3].GetSingle());

        var bones = new List<TruthBone>();
        foreach (var b in r.GetProperty("bones").EnumerateArray())
        {
            bones.Add(new TruthBone(
                b.GetProperty("name").GetString()!,
                b.GetProperty("parent").ValueKind == JsonValueKind.Null
                    ? null : b.GetProperty("parent").GetString(),
                V3(b.GetProperty("rest_world_pos")),
                Q(b.GetProperty("rest_world_rot_xyzw"))));
        }

        var frames = new List<TruthFrameBone[]>();
        foreach (var f in r.GetProperty("frames").EnumerateArray())
        {
            var fb = new TruthFrameBone[bones.Count];
            int i = 0;
            foreach (var e in f.EnumerateArray())
                fb[i++] = new TruthFrameBone(V3(e.GetProperty("wp")), Q(e.GetProperty("wr")));
            frames.Add(fb);
        }

        return new Truth
        {
            NSamples = r.GetProperty("n_samples").GetInt32(),
            Bones = bones,
            Frames = frames,
        };
    }

    // Blender Z-up world -> native BVH Y-up space (see header note 1).
    private static readonly Quaternion BlenderToBvh =
        Quaternion.CreateFromAxisAngle(Vector3.UnitX, -MathF.PI / 2f);

    private static Vector3 PosToBvh(Vector3 p) => Vector3.Transform(p, BlenderToBvh);

    private static Quaternion RotToBvh(Quaternion q) => BlenderToBvh * q;

    /// <summary>BVH joint world rotation from a truth pose rotation (see header note 2).</summary>
    private static Quaternion JointRot(Quaternion poseWorldRot, Quaternion restWorldRot)
        => RotToBvh(poseWorldRot) * Quaternion.Conjugate(RotToBvh(restWorldRot));

    private static float SkeletonHeight(Skel skeleton)
    {
        float min = float.MaxValue, max = float.MinValue;
        foreach (var w in skeleton.RestWorld)
        {
            min = MathF.Min(min, w.Pos.Y);
            max = MathF.Max(max, w.Pos.Y);
        }
        return max - min;
    }

    // ---- 1. freebvh vs Blender ground truth ---------------------------------------------

    [Fact]
    public void Freebvh_SkeletonTopology_MatchesBlender()
    {
        var scene = ImportFixture("bvhpython_test_freebvh.bvh");
        var truth = LoadTruth("freebvh.json");

        // Every Blender bone (= BVH joint) exists, with the same parent.
        var truthNames = new HashSet<string>();
        foreach (var b in truth.Bones)
        {
            truthNames.Add(b.Name);
            int idx = scene.Skeleton.IndexOf(b.Name);
            Assert.True(idx >= 0, $"missing joint {b.Name}");
            int parentIdx = scene.Skeleton[idx].ParentIndex;
            string? parent = parentIdx < 0 ? null : scene.Skeleton[parentIdx].Name;
            Assert.Equal(b.Parent, parent);
        }

        // Our extra bones are exactly the synthesized End Site leaves (15 in this file).
        var extras = scene.Skeleton.Bones
            .Where(b => !truthNames.Contains(b.Name))
            .Select(b => b.Name)
            .ToList();
        Assert.Equal(15, extras.Count);
        Assert.All(extras, n => Assert.EndsWith("_end", n, StringComparison.Ordinal));
        Assert.Equal(truth.Bones.Count + 15, scene.Skeleton.Count);
    }

    [Fact]
    public void Freebvh_RestPose_MatchesBlender()
    {
        var scene = ImportFixture("bvhpython_test_freebvh.bvh");
        var truth = LoadTruth("freebvh.json");

        Assert.Equal(1f, scene.UnitScaleCm); // centimeter-scale file, heuristic keeps x1
        Assert.Equal(1, scene.UpAxis);       // BVH convention: Y-up, recorded not converted

        foreach (var b in truth.Bones)
        {
            int idx = scene.Skeleton.IndexOf(b.Name);
            var ours = scene.Skeleton.RestWorld[idx];

            // Rest world position = accumulated OFFSETs (cm).
            TestUtil.AssertVectorEqual(PosToBvh(b.RestWorldPos), ours.Pos, 0.02f);

            // BVH rest rotations are identity by convention (Blender's nontrivial rest
            // rotations are bone head->tail orientations, cancelled in the frame tests).
            float identErr = MathQ.AngleBetween(Quaternion.Identity, ours.Rot) * KDegPerRad;
            Assert.True(identErr < 1e-3f, $"{b.Name}: rest rot not identity ({identErr} deg)");
        }
    }

    [Fact]
    public void Freebvh_Animation_MatchesBlender()
    {
        var scene = ImportFixture("bvhpython_test_freebvh.bvh");
        var truth = LoadTruth("freebvh.json");
        Assert.Single(scene.Clips);
        var clip = scene.Clips[0];

        float height = SkeletonHeight(scene.Skeleton);
        Assert.InRange(height, 100f, 400f); // sanity: a cm-scale humanoid
        float posTol = 0.005f * height;
        const float rotTolDeg = 1f;

        // 10 frames spread over the 40 extracted truth samples.
        int[] sampleFrames = { 0, 4, 8, 12, 16, 20, 24, 28, 32, 36 };
        foreach (int f in sampleFrames)
        {
            Assert.True(f < truth.NSamples && f < clip.FrameCount);
            var worlds = new Pose(clip.Frames[f]).ToWorld(scene.Skeleton);
            var truthFrame = truth.Frames[f];

            var failures = new List<string>();
            for (int i = 0; i < truth.Bones.Count; i++)
            {
                var tb = truth.Bones[i];
                int idx = scene.Skeleton.IndexOf(tb.Name);
                var expectedPos = PosToBvh(truthFrame[i].WorldPos);
                var expectedRot = JointRot(truthFrame[i].WorldRot, tb.RestWorldRot);

                float posErr = Vector3.Distance(worlds[idx].Pos, expectedPos);
                float rotErr = MathQ.AngleBetween(worlds[idx].Rot, expectedRot) * KDegPerRad;
                if (posErr > posTol || rotErr > rotTolDeg)
                    failures.Add($"  {tb.Name}: posErr={posErr:F3} cm (tol {posTol:F3}), rotErr={rotErr:F3} deg");
            }

            Assert.True(failures.Count == 0,
                $"frame {f}: {failures.Count}/{truth.Bones.Count} joints out of tolerance:\n" +
                string.Join("\n", failures.Take(12)));
        }
    }

    // ---- 2. cmu_01_01 basics --------------------------------------------------------------

    [Fact]
    public void Cmu0101_ImportBasics()
    {
        var scene = ImportFixture("cmu_01_01.bvh");

        // 31 joints + 7 End Sites.
        int endBones = scene.Skeleton.Bones.Count(b => b.Name.EndsWith("_end", StringComparison.Ordinal));
        Assert.Equal(7, endBones);
        Assert.Equal(31 + 7, scene.Skeleton.Count);
        Assert.True(scene.Skeleton.IndexOf("Hips") >= 0);

        Assert.Single(scene.Clips);
        var clip = scene.Clips[0];
        Assert.True(clip.Duration > 1f, $"duration {clip.Duration}s"); // 2752 frames at 120 fps ~ 22.9 s
        Assert.Equal(30f, clip.Fps);

        // No NaNs anywhere.
        static bool Finite(XForm x) =>
            float.IsFinite(x.Pos.X) && float.IsFinite(x.Pos.Y) && float.IsFinite(x.Pos.Z) &&
            float.IsFinite(x.Rot.X) && float.IsFinite(x.Rot.Y) && float.IsFinite(x.Rot.Z) &&
            float.IsFinite(x.Rot.W);
        for (int i = 0; i < scene.Skeleton.Count; i++)
            Assert.True(Finite(scene.Skeleton.RestWorld[i]), $"non-finite rest {scene.Skeleton[i].Name}");
        for (int f = 0; f < clip.FrameCount; f++)
            foreach (var x in clip.Frames[f])
                Assert.True(Finite(x), $"non-finite transform at frame {f}");

        // Root has 6 channels (3 position): its local translation is animated.
        int hips = scene.Skeleton.IndexOf("Hips");
        bool rootMoves = false;
        for (int f = 1; f < clip.FrameCount && !rootMoves; f++)
            rootMoves = Vector3.Distance(clip.Frames[f][hips].Pos, clip.Frames[0][hips].Pos) > 0.1f;
        Assert.True(rootMoves, "root translation never changes — position channels not applied");

        // A knee joint has only 3 (rotation) channels: local translation stays at the OFFSET.
        int knee = scene.Skeleton.IndexOf("LeftLeg");
        Assert.True(knee >= 0, "missing LeftLeg");
        var kneeRest = scene.Skeleton[knee].RestLocal.Pos;
        for (int f = 0; f < clip.FrameCount; f++)
            TestUtil.AssertVectorEqual(kneeRest, clip.Frames[f][knee].Pos, 1e-4f);
    }

    // ---- 3. resampling ---------------------------------------------------------------------

    // freebvh: Frames: 69, Frame Time: 0.0333333 -> duration = 68 * 0.0333333 s.
    // Expected frame count on the target grid = round(duration * fps) + 1.
    [Theory]
    [InlineData(30f, 69)]
    [InlineData(60f, 137)]
    [InlineData(15f, 35)]
    public void Freebvh_Resampling_FrameCountMatchesGrid(float fps, int expectedFrames)
    {
        var scene = ImportFixture("bvhpython_test_freebvh.bvh", fps);
        var clip = Assert.Single(scene.Clips);
        Assert.Equal(fps, clip.Fps);
        Assert.Equal(expectedFrames, clip.FrameCount);

        double duration = 68 * 0.0333333;
        Assert.Equal((int)Math.Round(duration * fps) + 1, clip.FrameCount);
    }

    // ---- 4. unit heuristic + channel-order unit check ---------------------------------------

    // Meter-scale skeleton (rest height 1.7 < 10) must be scaled x100 to centimeters,
    // including the root position channels. Frame 1 also pins down the rotation-channel
    // convention independently of Blender: channels "Zrotation Yrotation Xrotation" with
    // values (90, 0, 0) must yield world R = Rz(90 deg), so the Head joint at local offset
    // (0, 0.7, 0) m lands at hips + Rz90*(0, 70, 0) = hips + (-70, 0, 0) cm.
    private const string MeterBvh = @"HIERARCHY
ROOT Hips
{
    OFFSET 0 0.9 0
    CHANNELS 6 Xposition Yposition Zposition Zrotation Yrotation Xrotation
    JOINT Head
    {
        OFFSET 0 0.7 0
        CHANNELS 3 Zrotation Yrotation Xrotation
        End Site
        {
            OFFSET 0 0.1 0
        }
    }
}
MOTION
Frames: 2
Frame Time: 0.0333333
0 0.9 0 0 0 0 0 0 0
0 1.0 0.2 90 0 0 0 0 0
";

    [Fact]
    public void MeterScaleFile_IsConvertedToCentimeters()
    {
        var scene = BvhImporter.Import(Encoding.ASCII.GetBytes(MeterBvh), new BvhImportOptions());

        Assert.Equal(100f, scene.UnitScaleCm);

        int hips = scene.Skeleton.IndexOf("Hips");
        int head = scene.Skeleton.IndexOf("Head");
        int end = scene.Skeleton.IndexOf("Head_end");
        Assert.True(hips >= 0 && head >= 0 && end >= 0);

        TestUtil.AssertVectorEqual(new Vector3(0f, 90f, 0f), scene.Skeleton.RestWorld[hips].Pos, 1e-3f);
        TestUtil.AssertVectorEqual(new Vector3(0f, 160f, 0f), scene.Skeleton.RestWorld[head].Pos, 1e-3f);
        TestUtil.AssertVectorEqual(new Vector3(0f, 10f, 0f), scene.Skeleton[end].RestLocal.Pos, 1e-3f);

        var clip = Assert.Single(scene.Clips);
        Assert.Equal(2, clip.FrameCount);

        var worlds = new Pose(clip.Frames[1]).ToWorld(scene.Skeleton);
        TestUtil.AssertVectorEqual(new Vector3(0f, 100f, 20f), worlds[hips].Pos, 0.01f);
        TestUtil.AssertVectorEqual(new Vector3(-70f, 100f, 20f), worlds[head].Pos, 0.01f);

        var rz90 = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f);
        Assert.True(MathQ.AngleBetween(rz90, worlds[hips].Rot) * KDegPerRad < 0.01f,
            $"hips world rot {worlds[hips].Rot} != Rz(90)");
    }

    // ---- 5. malformed input ------------------------------------------------------------------

    [Fact]
    public void TruncatedFile_Throws()
    {
        var full = Fixture("bvhpython_test_freebvh.bvh");

        // Cut mid-MOTION (file is ~60% hierarchy, so 80% leaves a partial frame block).
        var midMotion = full.AsSpan(0, full.Length * 8 / 10).ToArray();
        Assert.Throws<FormatException>(() => BvhImporter.Import(midMotion, new BvhImportOptions()));

        // Cut mid-HIERARCHY.
        var midHierarchy = full.AsSpan(0, full.Length / 4).ToArray();
        Assert.Throws<FormatException>(() => BvhImporter.Import(midHierarchy, new BvhImportOptions()));
    }

    [Fact]
    public void NonBvhData_Throws()
    {
        Assert.Throws<FormatException>(() => BvhImporter.Import(
            Encoding.ASCII.GetBytes("this is not a bvh file"), new BvhImportOptions()));
        Assert.Throws<FormatException>(() => BvhImporter.Import(
            Array.Empty<byte>(), new BvhImportOptions()));
    }
}
using System.Numerics;
using HumanoidRetargeter.Core.Formats;
using HumanoidRetargeter.Core.Maths;
using Xunit;

namespace HumanoidRetargeter.Tests.Formats;

public class QuaternionContinuityTests
{
    private static Quaternion Rz(float radians)
        => Quaternion.CreateFromAxisAngle(Vector3.UnitZ, radians);

    private static void AssertAligned(IReadOnlyList<Quaternion> track)
    {
        for (var i = 1; i < track.Count; i++)
            Assert.True(Quaternion.Dot(track[i - 1], track[i]) >= 0f,
                $"negative dot between samples {i - 1} and {i}: {track[i - 1]} -> {track[i]}");
    }

    [Fact]
    public void Align_FlippedSample_NegatedBackOntoSameHemisphere()
    {
        var track = new[] { Quaternion.Identity, Quaternion.Negate(Rz(0.1f)) };
        QuaternionContinuity.Align(track);

        AssertAligned(track);
        // Semantically the same rotation: only the sign changed.
        Assert.Equal(Rz(0.1f).X, track[1].X, 6);
        Assert.Equal(Rz(0.1f).W, track[1].W, 6);
    }

    [Fact]
    public void Align_AlternatingFlips_PropagatesCumulatively()
    {
        // Flips must be judged against the ALIGNED predecessor, not the raw one: after the
        // second sample is negated, the third (raw-consistent with the second) must flip too.
        var track = new[]
        {
            Rz(0.0f),
            Quaternion.Negate(Rz(0.1f)),
            Quaternion.Negate(Rz(0.2f)),
            Rz(0.3f),
        };
        QuaternionContinuity.Align(track);
        AssertAligned(track);
    }

    [Fact]
    public void Align_AlreadyContinuous_Unchanged()
    {
        var track = new[] { Rz(0.0f), Rz(0.1f), Rz(0.2f) };
        var expected = (Quaternion[])track.Clone();
        QuaternionContinuity.Align(track);
        Assert.Equal(expected, track);
    }

    [Fact]
    public void AlignFrames_PerBoneTracks_AlignedIndependently()
    {
        var frames = new List<XForm[]>
        {
            new[]
            {
                new XForm(Vector3.Zero, Rz(0.0f)),
                new XForm(Vector3.Zero, Rz(1.0f)),
            },
            new[]
            {
                new XForm(Vector3.Zero, Quaternion.Negate(Rz(0.1f))), // bone 0 flips
                new XForm(Vector3.Zero, Rz(1.1f)),                    // bone 1 stays
            },
            new[]
            {
                new XForm(Vector3.Zero, Quaternion.Negate(Rz(0.2f))),
                new XForm(Vector3.Zero, Quaternion.Negate(Rz(1.2f))), // bone 1 flips
            },
        };

        QuaternionContinuity.AlignFrames(frames);

        for (var bone = 0; bone < 2; bone++)
            AssertAligned(frames.Select(f => f[bone].Rot).ToList());

        // Positions untouched.
        foreach (var frame in frames)
            foreach (var x in frame)
                Assert.Equal(Vector3.Zero, x.Pos);
    }

    [Fact]
    public void AlignFrames_SingleFrame_NoOp()
    {
        var frames = new List<XForm[]> { new[] { new XForm(Vector3.Zero, Rz(0.4f)) } };
        QuaternionContinuity.AlignFrames(frames);
        Assert.Equal(Rz(0.4f), frames[0][0].Rot);
    }
}
using HumanoidRetargeter.Core.Mapping;
using Xunit;
using HumanoidRetargeter.Core;

namespace HumanoidRetargeter.Tests.Mapping;

/// <summary>
/// Local-only preset-detection checks against REAL rig files in <c>dev/corpus/</c> (not
/// committed; every test silently passes when the file is absent):
/// <list type="bullet">
/// <item><c>unknown_rigs/makehuman_cmu_03_03_dazNames.bvh</c> — MakeHuman CMU retarget with
/// Poser/DAZ classic names (hip/abdomen/chest, lShldr/lForeArm, lThigh/lShin, 2-segment
/// fingers, no toes). Previously fell through to the auto-mapper; must now hit the
/// <c>daz_poser</c> preset.</item>
/// <item><c>todo/Defenses.fbx</c> — Auto-Rig Pro export (PunchPerfect family): root.x hips,
/// *_stretch limbs, c_-prefixed fingers, leftover mixamorig:*4 finger-tip markers.
/// Previously auto-mapped at 0.94; must now hit the <c>auto_rig_pro</c> preset with a
/// full mapping.</item>
/// </list>
/// </summary>
public class RealRigPresetTests
{
    /// <summary>Resolves a repo-relative path by walking up to the .sbproj directory.</summary>
    private static string RepoFile(params string[] parts)
    {
        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir is not null)
        {
            if (File.Exists(Path.Combine(dir.FullName, "humanoid-retargeter.sbproj")))
                return Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
            dir = dir.Parent!;
        }
        throw new InvalidOperationException("Repo root (humanoid-retargeter.sbproj) not found.");
    }

    [Fact] // skipped (silently green) when the local corpus file is not present
    public void MakehumanDazNames_DetectedAsDazPoser_LimbsFullyMapped()
    {
        var path = RepoFile("dev", "corpus", "unknown_rigs", "makehuman_cmu_03_03_dazNames.bvh");
        if (!File.Exists(path))
            return;

        var scene = Retargeter.ImportSource(File.ReadAllBytes(path), Path.GetFileName(path));
        var (map, report) = Retargeter.ResolveMapping(scene.Skeleton);

        Assert.Equal(MappingSource.Preset, map.Source);
        Assert.Equal("daz_poser", map.ProfileName);
        Assert.True(map.Confidence >= 0.8f, $"Confidence {map.Confidence} < 0.8");
        Assert.False(report.NeedsUserDecision);

        string BoneOf(BoneRole role) => scene.Skeleton[map.RoleToBone[role]].Name;
        Assert.Equal("hip", BoneOf(BoneRole.Hips));
        Assert.Equal("abdomen", BoneOf(BoneRole.Spine0));
        Assert.Equal("chest", BoneOf(BoneRole.Spine1)); // no abdomen2 in this file
        Assert.Equal("neck", BoneOf(BoneRole.Neck));
        Assert.Equal("head", BoneOf(BoneRole.Head));
        Assert.Equal("lCollar", BoneOf(BoneRole.ClavicleL));
        Assert.Equal("lShldr", BoneOf(BoneRole.UpperArmL));
        Assert.Equal("rShldr", BoneOf(BoneRole.UpperArmR));
        Assert.Equal("lForeArm", BoneOf(BoneRole.LowerArmL));
        Assert.Equal("rForeArm", BoneOf(BoneRole.LowerArmR));
        Assert.Equal("lHand", BoneOf(BoneRole.HandL));
        Assert.Equal("rHand", BoneOf(BoneRole.HandR));
        Assert.Equal("lThigh", BoneOf(BoneRole.UpperLegL));
        Assert.Equal("rThigh", BoneOf(BoneRole.UpperLegR));
        Assert.Equal("lShin", BoneOf(BoneRole.LowerLegL));
        Assert.Equal("rShin", BoneOf(BoneRole.LowerLegR));
        Assert.Equal("lFoot", BoneOf(BoneRole.FootL));
        Assert.Equal("rFoot", BoneOf(BoneRole.FootR));
        // 2-segment fingers: Prox+Mid resolve, Dist roles stay unmapped, honestly.
        Assert.Equal("lThumb1", BoneOf(BoneRole.ThumbProxL));
        Assert.Equal("rMid2", BoneOf(BoneRole.MiddleMidR));
        Assert.False(map.RoleToBone.ContainsKey(BoneRole.ThumbDistL));

        // The lButtock/rButtock thigh helpers and eyes carry no role.
        foreach (var (role, boneIndex) in map.RoleToBone)
        {
            var name = scene.Skeleton[boneIndex].Name;
            Assert.DoesNotContain("Buttock", name);
            Assert.DoesNotContain("Eye", name);
        }
    }

    [Fact] // skipped (silently green) when the local corpus file is not present
    public void DefensesFbx_DetectedAsAutoRigPro_FullMappingBeatsAutoMap()
    {
        var path = RepoFile("dev", "corpus", "todo", "Defenses.fbx");
        if (!File.Exists(path))
            return;

        var scene = Retargeter.ImportSource(File.ReadAllBytes(path), Path.GetFileName(path));
        var (map, report) = Retargeter.ResolveMapping(scene.Skeleton);

        Assert.Equal(MappingSource.Preset, map.Source);
        Assert.Equal("auto_rig_pro", map.ProfileName);
        Assert.True(map.Confidence >= 0.9f, $"Confidence {map.Confidence} < 0.9");
        Assert.False(report.NeedsUserDecision);

        // Must beat the auto-mapper that used to handle this rig (0.94).
        var auto = AutoMapper.Map(scene.Skeleton);
        Assert.True(map.Confidence > auto.Confidence,
            $"Preset {map.Confidence} must beat auto-map {auto.Confidence}");

        string BoneOf(BoneRole role) => scene.Skeleton[map.RoleToBone[role]].Name;
        Assert.Equal("root.x", BoneOf(BoneRole.Hips));
        Assert.Equal("spine_01.x", BoneOf(BoneRole.Spine0));
        Assert.Equal("spine_02.x", BoneOf(BoneRole.Spine1));
        Assert.Equal("spine_03.x", BoneOf(BoneRole.Spine2));
        Assert.Equal("neck.x", BoneOf(BoneRole.Neck));
        Assert.Equal("head.x", BoneOf(BoneRole.Head));
        Assert.Equal("shoulder.l", BoneOf(BoneRole.ClavicleL));
        Assert.Equal("arm_stretch.l", BoneOf(BoneRole.UpperArmL));
        Assert.Equal("forearm_stretch.r", BoneOf(BoneRole.LowerArmR));
        Assert.Equal("hand.l", BoneOf(BoneRole.HandL));
        Assert.Equal("thigh_stretch.l", BoneOf(BoneRole.UpperLegL));
        Assert.Equal("leg_stretch.r", BoneOf(BoneRole.LowerLegR));
        Assert.Equal("foot.l", BoneOf(BoneRole.FootL));
        Assert.Equal("toes_01.l", BoneOf(BoneRole.ToeL));
        Assert.Equal("c_thumb1.l", BoneOf(BoneRole.ThumbProxL));
        Assert.Equal("c_middle2.r", BoneOf(BoneRole.MiddleMidR));
        Assert.Equal("c_pinky3.l", BoneOf(BoneRole.PinkyDistL));

        // All 30 finger phalanx roles resolve; the leftover mixamorig:*4 tip markers and
        // the ground bone "root" never carry a role.
        var fingers =
            from finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" }
            from segment in new[] { "Prox", "Mid", "Dist" }
            from side in new[] { "L", "R" }
            select Enum.Parse<BoneRole>(finger + segment + side);
        foreach (var role in fingers)
            Assert.True(map.RoleToBone.ContainsKey(role), $"Finger role {role} unmapped");
        foreach (var (role, boneIndex) in map.RoleToBone)
        {
            var name = scene.Skeleton[boneIndex].Name;
            Assert.False(name == "root" || name.StartsWith("mixamorig:"),
                $"{role} mapped to non-deform bone {name}");
        }
    }
}
using HumanoidRetargeter.Core.Mapping;
using Xunit;
using HumanoidRetargeter.Core;

namespace HumanoidRetargeter.Tests.Mapping;

/// <summary>
/// Local-only mapping/detection checks against the REAL user-reported repro files in
/// <c>dev/corpus/todo/</c> (not committed; every test silently passes when a file is
/// absent):
/// <list type="bullet">
/// <item><c>Neutral_throw_ball_001__A057.bvh</c> — NVIDIA SOMA uniform-proportion skeleton
/// (github.com/NVIDIA/soma-retargeter assets/motions/bvh). Mixamo-identical upper body and
/// fingers but legs <c>LeftLeg→LeftShin</c> ("Leg" is the THIGH). Used to falsely detect
/// "90% mixamo" with both upper legs unmapped.</item>
/// <item><c>Armchair1.bvh</c> — classic MotionBuilder/Character-Studio BVH naming
/// (<c>Chest..Chest4</c>, <c>Collar→Shoulder→Elbow→Wrist</c>, <c>Hip→Knee→Ankle→Toe</c>).</item>
/// <item><c>47_01.bvh</c> — CMU mocap naming (<c>LHipJoint</c> helpers, <c>LowerBack</c>
/// spine start, otherwise mixamo-style limb names).</item>
/// </list>
/// </summary>
public class TodoCorpusRealFileTests
{
    /// <summary>Resolves a repo-relative path by walking up to the .sbproj directory.</summary>
    private static string RepoFile(params string[] parts)
    {
        var dir = new DirectoryInfo(AppContext.BaseDirectory);
        while (dir is not null)
        {
            if (File.Exists(Path.Combine(dir.FullName, "humanoid-retargeter.sbproj")))
                return Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
            dir = dir.Parent!;
        }
        throw new InvalidOperationException("Repo root (humanoid-retargeter.sbproj) not found.");
    }

    private static string TodoFile(string name) => RepoFile("dev", "corpus", "todo", name);

    private static readonly BoneRole[] CoreLimbAndAxisRoles =
    {
        BoneRole.Hips, BoneRole.Spine0, BoneRole.Neck, BoneRole.Head,
        BoneRole.ClavicleL, BoneRole.ClavicleR,
        BoneRole.UpperArmL, BoneRole.UpperArmR, BoneRole.LowerArmL, BoneRole.LowerArmR,
        BoneRole.HandL, BoneRole.HandR,
        BoneRole.UpperLegL, BoneRole.UpperLegR, BoneRole.LowerLegL, BoneRole.LowerLegR,
        BoneRole.FootL, BoneRole.FootR,
    };

    // NOTE: the killed profile-support round (SOMA/classic-BVH presets for
    // Neutral_throw_ball_001__A057.bvh and Armchair1.bvh) lost its Code-side work; its
    // tests were removed with it. Todo.txt still tracks those repros.

    [Fact] // skipped (silently green) when the local corpus file is not present
    public void Cmu4701_LimbsFullyMappedBothSides()
    {
        var path = TodoFile("47_01.bvh");
        if (!File.Exists(path))
            return;

        var scene = Retargeter.ImportSource(File.ReadAllBytes(path), Path.GetFileName(path));
        var (map, _) = Retargeter.ResolveMapping(scene.Skeleton);

        string? BoneOf(BoneRole role)
            => map.RoleToBone.TryGetValue(role, out var i) ? scene.Skeleton[i].Name : null;

        // Mapping path is unpinned (CMU naming is mixamo-adjacent: a preset or the
        // auto-mapper may resolve it) — what matters is the anatomy.
        Assert.Equal("Hips", BoneOf(BoneRole.Hips));
        Assert.Equal("LeftUpLeg", BoneOf(BoneRole.UpperLegL));
        Assert.Equal("RightUpLeg", BoneOf(BoneRole.UpperLegR));
        Assert.Equal("LeftLeg", BoneOf(BoneRole.LowerLegL));
        Assert.Equal("RightLeg", BoneOf(BoneRole.LowerLegR));
        Assert.Equal("LeftFoot", BoneOf(BoneRole.FootL));
        Assert.Equal("LeftToeBase", BoneOf(BoneRole.ToeL));
        Assert.Equal("LeftShoulder", BoneOf(BoneRole.ClavicleL));
        Assert.Equal("LeftArm", BoneOf(BoneRole.UpperArmL));
        Assert.Equal("RightForeArm", BoneOf(BoneRole.LowerArmR));
        Assert.Equal("LeftHand", BoneOf(BoneRole.HandL));
        Assert.NotNull(BoneOf(BoneRole.Head));
        Assert.NotNull(BoneOf(BoneRole.Neck));
        Assert.NotNull(BoneOf(BoneRole.Spine0));
        foreach (var role in CoreLimbAndAxisRoles)
            Assert.True(map.RoleToBone.ContainsKey(role), $"{role} unmapped");

        // The LHipJoint/RHipJoint helper joints carry no role.
        foreach (var (role, boneIndex) in map.RoleToBone)
            Assert.DoesNotContain("HipJoint", scene.Skeleton[boneIndex].Name);
    }
}