tests/SmartPort/Parser/OptionalMaterialTests.cs
using System.Reflection;
using System.Text;
using HumanoidRetargeter.Editor;
using HumanoidRetargeterVrf;
using HumanoidRetargeterVrf.IO;
using Xunit;

namespace SmartPort.Parser.Tests;

public class OptionalMaterialTests
{
    const string Outfit = "models/human_clothes/default_outfit/default_outfit_male_lower.vmat";

    sealed class FailingLoader(Exception error) : IFileLoader
    {
        public Resource LoadFile(string file) => throw error;
        public Resource LoadFileCompiled(string file) => LoadFile(file);
    }

    [Fact]
    public void MissingOutfitMetadataUsesVertexBufferSemantics()
    {
        var loader = new FailingLoader(new FileNotFoundException("Missing material", Outfit));
        Assert.Empty(ModelExtract.ReadMaterialInputSignature(loader, Outfit).Elements);
    }

    [Fact]
    public void CorruptMetadataAndCancellationAreNotHidden()
    {
        Assert.Throws<InvalidDataException>(() => ModelExtract.ReadMaterialInputSignature(
            new FailingLoader(new InvalidDataException("Corrupt material")), Outfit));
        Assert.Throws<OperationCanceledException>(() => ModelExtract.ReadMaterialInputSignature(
            new FailingLoader(new OperationCanceledException()), Outfit));
    }

    [Theory]
    [InlineData("required.vmesh")]
    [InlineData("required.vphys")]
    [InlineData("required.vanim")]
    [InlineData("required.vagrp")]
    [InlineData("required.vmdl")]
    public void RequiredDependenciesStillFailWithTheirFilename(string path)
    {
        var type = typeof(CompiledAssetRecovery).GetNestedType("Loader", BindingFlags.NonPublic)!;
        using var loader = (IDisposable)Activator.CreateInstance(type,
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null,
            new object[] { new Dictionary<string, string>(), CancellationToken.None }, null)!;
        var error = Assert.Throws<FileNotFoundException>(() => ((IFileLoader)loader).LoadFileCompiled(path));
        Assert.Equal(path, error.FileName);
        Assert.Contains(path, error.Message);
    }

    [CompiledFixtureFact]
    public void MissingMaterialFilesDoNotBlockRecoveryOrEraseMaterialAssignments()
    {
        var fixture = Environment.GetEnvironmentVariable("HR_SMART_PORT_FIXTURE")!;
        var paths = Directory.GetFiles(fixture, "*_c", SearchOption.AllDirectories)
            .Where(p => !p.EndsWith(".vmat_c", StringComparison.OrdinalIgnoreCase))
            .ToDictionary(p => Path.GetRelativePath(fixture, p)[..^2].Replace('\\', '/'), p => p, StringComparer.OrdinalIgnoreCase);
        const string model = "models/player/human/frank_mp.vmdl";
        var output = Path.Combine(Path.GetTempPath(), "hr-smart-port-no-materials-" + Guid.NewGuid().ToString("N"));
        var text = CompiledAssetRecovery.Recover(paths[model], model, output, "recovered", paths, default);
        using var resource = new Resource();
        resource.Read(paths[model]);
        Assert.NotNull(resource.ExternalReferences);
        var materials = resource.ExternalReferences.ResourceRefInfoList.Where(r => r.Name.EndsWith(".vmat")).Select(r => r.Name).ToArray();
        Assert.NotEmpty(materials);
        var files = Directory.GetFiles(output, "*.dmx", SearchOption.AllDirectories);
        Assert.Equal(398, files.Length); // Seven meshes and all 391 animation sources.
        var meshText = string.Join("\n", files.Where(f => Path.GetFileName(f).StartsWith("frank_mp_")).Select(f => Encoding.UTF8.GetString(File.ReadAllBytes(f))));
        foreach (var material in materials) Assert.Contains(material, text + meshText);
    }
}