tests/CitizenAnimationSetup/ModelGroundingTests.cs
using System.Numerics;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Target;
using Xunit;

namespace HumanoidRetargeter.Tests.Target;

public class ModelGroundingTests
{
    private static string Model => VmdlWriter.Kv3Header + """
        { rootNode = { children = [
          { _class = "ModelModifierList" children = [ { _class = "ModelModifier_ScaleAndMirror" scale = 0.3937 } ] },
          { _class = "RenderMeshList" children = [ { filename = "custom.dmx" } ] },
          { _class = "AnimationList" children = [ { name = "idle" source_filename = "idle.dmx" } ] }
        ] } }
        """;

    [Theory]
    [InlineData(-37.484535f)]
    [InlineData(-10f)]
    public void GroundingTranslatesWholeModelAfterScaleWithoutChangingSources(float minimumZ)
    {
        var result = ModelGrounding.Apply(Model, minimumZ);
        Assert.Equal(-minimumZ, ModelGrounding.Offset(result));
        CitizenAnimationSetup.ValidateSourceScale(result);
        var before = (KvArray)((KvObject)((KvObject)Kv3.Parse(Model).Root)["rootNode"])["children"];
        var after = (KvArray)((KvObject)((KvObject)Kv3.Parse(result).Root)["rootNode"])["children"];
        Assert.True(KvValue.DeepEquals(before.Items[1], after.Items[1]));
        Assert.True(KvValue.DeepEquals(before.Items[2], after.Items[2]));
        var modifiers = (KvArray)((KvObject)after.Items[0])["children"];
        Assert.Equal("ModelModifier_ScaleAndMirror", ((KvObject)modifiers.Items[0]).GetString("_class"));
        Assert.True(ModelGrounding.IsGrounding((KvObject)modifiers.Items[1]));
        Assert.Equal(0, ModelGrounding.Offset(Model));
    }

    [Theory]
    [InlineData(0f)]
    [InlineData(-0.01f)]
    [InlineData(5f)]
    public void GroundedAndElevatedModelsStayUnchanged(float minimumZ)
        => Assert.Equal(Model, ModelGrounding.Apply(Model, minimumZ));

    [Fact]
    public void RegroundingDoesNotStackDuplicateModifiers()
    {
        var first = ModelGrounding.Apply(Model, -10);
        Assert.Equal(first, ModelGrounding.Apply(first, 0));
        var updated = ModelGrounding.Apply(first, -2);
        Assert.Equal(12, ModelGrounding.Offset(updated));
        Assert.Equal(1, updated.Split(ModelGrounding.ModifierName).Length - 1);
    }

    [Fact]
    public void SourceRecoveryUnshiftsAllRootsButNeverChildOffsetsOrRotations()
    {
        var local = new XForm(new Vector3(1, 2, 30), Quaternion.CreateFromYawPitchRoll(.2f, .3f, .4f));
        var root = ModelGrounding.SourceLocal(local, true, 37);
        Assert.Equal(local.Pos, root.Pos + Vector3.UnitZ * 37);
        Assert.Equal(local.Rot, root.Rot);
        Assert.Equal(local, ModelGrounding.SourceLocal(local, false, 37));
    }

    [Theory]
    [InlineData(float.NaN)]
    [InlineData(float.NegativeInfinity)]
    public void InvalidBoundsAreRejected(float minimumZ)
        => Assert.Throws<ArgumentOutOfRangeException>(() => ModelGrounding.Apply(Model, minimumZ));
}