dev/HumanoidRetargeter.Tests/Solve/RuntimePoseRetargeterTests.cs
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Solve;
using HumanoidRetargeter.Target;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;

namespace HumanoidRetargeter.Tests.Solve;

public sealed class RuntimePoseRetargeterTests
{
    [Fact]
    public void RuntimeFrame_MatchesExistingClipSolverIncludingRootMotion()
    {
        var (source, sourceMap, target) = Fixture();
        var moving = RestLocals(source);
        moving[sourceMap.RoleToBone[BoneRole.Hips]] = new XForm(new Vector3(25f, 100f, 8f), Quaternion.Identity);
        moving[sourceMap.RoleToBone[BoneRole.UpperArmL]] = new XForm(
            moving[sourceMap.RoleToBone[BoneRole.UpperArmL]].Pos,
            Quaternion.CreateFromAxisAngle(Vector3.UnitX, 0.4f));

        var clip = new Clip("runtime", 30f, false, new List<XForm[]> { RestLocals(source), moving });
        var scene = new SourceScene(source, new[] { clip }, 1f);
        var expected = new GeometricSolver().Solve(scene, sourceMap, target, new SolveOptions()).Frames[1];

        var runtime = new RuntimePoseRetargeter(source, sourceMap, target, RestLocals(source));
        var actual = new XForm[target.Skeleton.Count];
        runtime.Retarget(moving, actual);

        Assert.Equal(expected.Length, actual.Length);
        for (var i = 0; i < actual.Length; i++)
        {
            Assert.True(Vector3.Distance(expected[i].Pos, actual[i].Pos) < 1e-4f, $"bone {i} position");
            Assert.True(MathQ.AngleBetween(expected[i].Rot, actual[i].Rot) < 1e-4f, $"bone {i} rotation");
        }
    }

    [Fact]
    public void MissingOptionalBones_RetargetsRemainingHumanoid()
    {
        var (source, sourceMap, target) = Fixture(includeToes: false);
        var runtime = new RuntimePoseRetargeter(source, sourceMap, target);
        var destination = new XForm[target.Skeleton.Count];

        Assert.True(runtime.TryRetarget(RestLocals(source), destination, out var error), error);
        Assert.All(destination, x => Assert.True(float.IsFinite(x.Pos.X + x.Rot.W)));
    }

    [Fact]
    public void InvalidSourceSize_FailsWithoutMutatingDestination()
    {
        var (source, sourceMap, target) = Fixture();
        var runtime = new RuntimePoseRetargeter(source, sourceMap, target);
        var sentinel = new XForm(new Vector3(123f), Quaternion.Identity);
        var destination = Enumerable.Repeat(sentinel, target.Skeleton.Count).ToArray();

        Assert.False(runtime.TryRetarget(RestLocals(source).AsSpan(1), destination, out var error));
        Assert.Contains("expected", error, StringComparison.OrdinalIgnoreCase);
        Assert.All(destination, x => Assert.Equal(sentinel, x));
    }

    [Fact]
    public void InvalidMapping_FailsAtPlanBoundary()
    {
        var (source, _, target) = Fixture();
        var invalid = new MappingResult("invalid", MappingSource.Manual);
        invalid.RoleToBone[BoneRole.Hips] = source.Count + 10;

        var ex = Assert.Throws<ArgumentException>(() => new RuntimePoseRetargeter(source, invalid, target));
        Assert.Contains("outside", ex.Message, StringComparison.OrdinalIgnoreCase);
    }

    private static (SkeletonModel Source, MappingResult SourceMap, TargetRig Target) Fixture(bool includeToes = true)
    {
        var definitions = new List<BoneDefinition>
        {
            new("hips", null, new XForm(new Vector3(0, 100, 0), Quaternion.Identity)),
            new("spine", "hips", new XForm(new Vector3(0, 20, 0), Quaternion.Identity)),
            new("head", "spine", new XForm(new Vector3(0, 35, 0), Quaternion.Identity)),
            new("upperarm_l", "spine", new XForm(new Vector3(-15, 15, 0), Quaternion.Identity)),
            new("lowerarm_l", "upperarm_l", new XForm(new Vector3(-25, 0, 0), Quaternion.Identity)),
            new("hand_l", "lowerarm_l", new XForm(new Vector3(-20, 0, 0), Quaternion.Identity)),
            new("upperarm_r", "spine", new XForm(new Vector3(15, 15, 0), Quaternion.Identity)),
            new("lowerarm_r", "upperarm_r", new XForm(new Vector3(25, 0, 0), Quaternion.Identity)),
            new("hand_r", "lowerarm_r", new XForm(new Vector3(20, 0, 0), Quaternion.Identity)),
            new("thigh_l", "hips", new XForm(new Vector3(-10, -10, 0), Quaternion.Identity)),
            new("calf_l", "thigh_l", new XForm(new Vector3(0, -40, 0), Quaternion.Identity)),
            new("foot_l", "calf_l", new XForm(new Vector3(0, -40, 0), Quaternion.Identity)),
            new("thigh_r", "hips", new XForm(new Vector3(10, -10, 0), Quaternion.Identity)),
            new("calf_r", "thigh_r", new XForm(new Vector3(0, -40, 0), Quaternion.Identity)),
            new("foot_r", "calf_r", new XForm(new Vector3(0, -40, 0), Quaternion.Identity)),
        };
        if (includeToes)
        {
            definitions.Add(new BoneDefinition("toe_l", "foot_l", new XForm(new Vector3(0, 0, 15), Quaternion.Identity)));
            definitions.Add(new BoneDefinition("toe_r", "foot_r", new XForm(new Vector3(0, 0, 15), Quaternion.Identity)));
        }

        var source = SkeletonModel.Create(definitions);
        var map = AutoMapper.Map(source);

        var targetDefs = definitions.Select(d => new BoneDefinition(
            "target_" + d.Name,
            d.ParentName is null ? null : "target_" + d.ParentName,
            new XForm(d.RestLocal.Pos * 1.35f, d.RestLocal.Rot))).ToArray();
        var targetSkeleton = SkeletonModel.Create(targetDefs);
        var targetMap = AutoMapper.Map(targetSkeleton);
        var target = TargetRig.FromSkeleton(targetSkeleton, targetMap);
        return (source, map, target);
    }

    private static XForm[] RestLocals(SkeletonModel skeleton)
    {
        var pose = new XForm[skeleton.Count];
        for (var i = 0; i < pose.Length; i++)
            pose[i] = skeleton[i].RestLocal;
        return pose;
    }
}