tests/CitizenAnimationSetup/SmartPortExtensionTests.cs
using HumanoidRetargeter.Target;
using Xunit;

namespace HumanoidRetargeter.Tests.Target;

public class SmartPortExtensionTests
{
    static string Model(string nodes) => VmdlWriter.Kv3Header + "{ rootNode = { children = [" + nodes + "] custom_setting = 17 } }";
    static string Graph(string parameters = "") => VmdlWriter.Kv3Header + """
        { _class = "CAnimationGraph"
          m_nodeManager = { m_nodes = [
            { key = { m_id = 1 } value = { _class = "CustomGameNode" m_nNodeID = { m_id = 1 } custom_data = "untouched" } },
            { key = { m_id = 2 } value = { _class = "CRootAnimNode" m_nNodeID = { m_id = 2 }
              m_inputConnection = { m_nodeID = { m_id = 1 } m_outputID = { m_id = 4294967295 } } } }
          ] }
          m_pParameterList = { m_Parameters = [
        """ + parameters + "] } custom_settings = { enabled = true } }";

    [Fact]
    public void MergeKeepsExistingClipsSettingsAndNamespacedSourceEvents()
    {
        var target = Model("""
            { _class = "RenderMeshList" children = [{ filename = "target.dmx" }] },
            { _class = "AnimConstraintList" children = [{ name = "MyConstraint" }] },
            { _class = "AnimationList" children = [
              { _class = "AnimFile" name = "walk" source_filename = "original.dmx" },
              { _class = "AnimFile" name = "hr_port_walk" } ] },
            { _class = "WeightListList" children = [{ _class = "WeightList" name = "upper" weight = 1 }] }
            """);
        var source = Model("""
            { _class = "AnimationList" children = [ { _class = "AnimFile" name = "walk"
              source_filename = "source.dmx" weight_list_name = "upper" children = [
                { _class = "AnimEvent" event_class = "AE_FOOTSTEP" event_frame = 7 event_keys = { Foot = "1" } },
                { _class = "AnimSubtract" anim_name = "walk" frame = 0 }
              ] } ] },
            { _class = "WeightListList" children = [{ _class = "WeightList" name = "upper" weight = 0.5 }] }
            """);
        var result = SmartPortExtension.MergeModel(target, source, "own.vanmgrph", out var names);
        Assert.Equal("hr_port_walk_2", names["walk"]);
        foreach (var text in new[] { "target.dmx", "original.dmx", "source.dmx", "MyConstraint", "AE_FOOTSTEP", "custom_setting = 17", "hr_port_upper" })
            Assert.Contains(text, result);
        Assert.Contains("anim_name = \"hr_port_walk_2\"", result);
        Assert.Equal("own.vanmgrph", StockAnimationGraph.GraphName(result));
    }

    [Fact]
    public void AnyGraphKeepsItsNodesParametersAndDefaultOutput()
    {
        var original = Graph("{ _class = \"CBoolAnimParameter\" m_name = \"game_action\" m_id = { m_id = 3 } }");
        var result = SmartPortExtension.ExtendGraph(original, "custom.vmdl", new[] { new SmartPortClip("dance", true, false, false) }, out var parameter);
        Assert.Equal(SmartPortExtension.Parameter, parameter);
        var before = (KvObject)Kv3.Parse(original).Root;
        var after = (KvObject)Kv3.Parse(result).Root;
        Assert.True(KvValue.DeepEquals(Nodes(before).Items[0], Nodes(after).Items[0]));
        Assert.True(KvValue.DeepEquals(Parameters(before).Items[0], Parameters(after).Items[0]));
        Assert.True(KvValue.DeepEquals(before["custom_settings"], after["custom_settings"]));
        var selector = Nodes(after).Items.Cast<KvObject>().Select(n => (KvObject)n["value"]).Single(n => n.GetString("_class") == "CSelectorAnimNode");
        var originalOutput = (KvObject)((KvObject)Nodes(before).Items[1])["value"];
        Assert.True(KvValue.DeepEquals(originalOutput["m_inputConnection"], ((KvArray)selector["m_children"]).Items[0]));
        Assert.Equal(0, ((KvLong)((KvObject)Parameters(after).Items[1])["m_defaultValue"]).Value);
        var ids = Nodes(after).Items.Cast<KvObject>().Select(n => ((KvLong)((KvObject)n["key"])["m_id"]).Value).ToArray();
        Assert.Equal(ids.Length, ids.Distinct().Count());
        Assert.DoesNotContain(3, ids); // Existing parameter IDs are reserved too.
    }

    [Fact]
    public void AdditiveClipsLayerOverTheExistingGraphAndNamesDoNotCollide()
    {
        var input = Graph("{ _class = \"CEnumAnimParameter\" m_name = \"hr_smartport_clip\" m_id = { m_id = 3 } }");
        var result = SmartPortExtension.ExtendGraph(input, "target.vmdl", new[] { new SmartPortClip("breathe", true, true, false) }, out var name);
        Assert.Equal("hr_smartport_clip_2", name);
        Assert.Contains("CAddAnimNode", result);
        Assert.Contains("m_bResetBase = false", result);
        Assert.Contains("m_bLoop = true", result);
        Assert.Contains("m_sequenceName = \"breathe\"", result);
    }

    [Fact]
    public void GraphWithoutParametersCanBeExtendedAndDisconnectedOutputIsRejected()
    {
        var input = Graph();
        var start = input.IndexOf("m_pParameterList", StringComparison.Ordinal);
        var end = input.IndexOf("custom_settings", StringComparison.Ordinal);
        input = input.Remove(start, end - start);
        var clip = new[] { new SmartPortClip("wave", false, false, false) };
        Assert.Contains("CEnumAnimParameter", SmartPortExtension.ExtendGraph(input, "x", clip, out _));
        Assert.Throws<InvalidOperationException>(() => SmartPortExtension.ExtendGraph(
            input.Replace("m_nodeID = { m_id = 1 }", "m_nodeID = { m_id = 4294967295 }"), "x", clip, out _));
    }

    [Fact]
    public void UnsupportedGraphsAndEmptyImportsFailBeforeWriting()
    {
        var clip = new[] { new SmartPortClip("wave", false, false, false) };
        Assert.Throws<InvalidOperationException>(() => SmartPortExtension.ExtendGraph(VmdlWriter.Kv3Header + "{ _class = \"CAnimationGraph\" }", "x", clip, out _));
        Assert.Throws<InvalidOperationException>(() => SmartPortExtension.ExtendGraph(Graph(), "x", Array.Empty<SmartPortClip>(), out _));
        Assert.Throws<InvalidOperationException>(() => SmartPortExtension.MergeModel(Model(""), Model(""), "x", out _));
    }

    static KvArray Nodes(KvObject root) => (KvArray)((KvObject)root["m_nodeManager"])["m_nodes"];
    static KvArray Parameters(KvObject root) => (KvArray)((KvObject)root["m_pParameterList"])["m_Parameters"];
}