using Launcher.Next.Application;
using Launcher.Next.Core;
using Launcher.Next.Infrastructure;
using System.Security.Cryptography;
using Xunit;
namespace Launcher.Next.Tests;
public sealed class RosterAdaptersTests
{
private static readonly RosterContext Context = new("Plant3D", 2026, "x64", true, "legacy-project-a");
[Fact]
public void ProjectRoster_ProducesDeterministicRequirements()
{
var selected = new RosterAdapter().Select(Json(), null, null, Context);
var requirements = new RosterAdapter().Requirements(selected.Roster!);
Assert.Equal(RosterSource.ProjectRosterJson, selected.Source);
Assert.Equal(["P_A", "P_B"], requirements.Select(x => x.PackageId));
Assert.Equal(["1.0.0", "2.0.0"], requirements.Select(x => x.RequiredVersion));
}
[Fact]
public void LegacyDownloadList_PreservesSourceOrderAndUsesContext()
{
const string xml = "";
var selected = new RosterAdapter().Select(null, xml, null, Context);
Assert.Equal(RosterSource.LegacyDownloadListXml, selected.Source);
Assert.Equal("legacy-project-a", selected.Roster!.RosterId);
Assert.Equal(["P_First", "P_Second"], selected.Roster.Packages.Select(x => x.Id));
Assert.False(selected.Roster.Packages[1].Enabled);
}
[Fact]
public void LegacySettingsAndDownloadList_JoinOnlyThroughExplicitOverlay()
{
const string settings = "hcPlant*.dwgrlegacy-axcatload";
const string selection = "legacy-a";
var adapter = new LegacyConfigurationAdapter();
var parsed = adapter.ParseSettings(settings);
var requirements = adapter.Join(parsed.Patterns[0], adapter.ParseDownloadList(selection), new("map-a", "Plant3D", 2026, "x64", false, [new("legacy-a", "PHIM.A", "1.0.0", "pilot")]));
Assert.Single(requirements); Assert.Equal("PHIM.A", requirements[0].PackageId);
}
[Fact]
public void LegacyDownloadList_DoesNotInventPluginMetadata()
{
var selection = new LegacyConfigurationAdapter().ParseDownloadList("legacy-a");
Assert.Equal(["legacy-a"], selection.PluginIds);
}
[Fact]
public void LegacyProbeOverride_RequiresExplicitProbeModeAndStrictSchema()
{
const string valid = """{"schemaVersion":1,"mode":"LegacyConfigurationProbe","settingsXml":"C:/Settings.xml","downloadListXml":"C:/download_list.xml","compatibilityOverlay":"C:/overlay.json","selectedPattern":"Plant","evidenceOutputRoot":"C:/evidence"}""";
Assert.Equal("Plant", LegacyProbeOverrideParser.Parse(valid).SelectedPattern);
Assert.Throws(() => LegacyProbeOverrideParser.Parse(valid.Replace("LegacyConfigurationProbe","Production")));
Assert.Throws(() => LegacyProbeOverrideParser.Parse(valid[..^1] + ",\"unknown\":true}"));
}
[Fact]
public void ProjectRoster_HasPriorityOverLegacy()
{
var selected = new RosterAdapter().Select(Json(), "", null, Context);
Assert.Equal(RosterSource.ProjectRosterJson, selected.Source);
}
[Fact]
public void MalformedProjectRoster_DoesNotFallBackImplicitly()
{
Assert.ThrowsAny(() => new RosterAdapter().Select("{", "", Json(), Context));
}
[Fact]
public void CentralDefault_IsUsedOnlyWhenProjectAndLegacyAreAbsent()
{
var selected = new RosterAdapter().Select(null, null, Json(), Context);
Assert.Equal(RosterSource.CentralDefaultRoster, selected.Source);
Assert.Equal("CENTRAL_DEFAULT_ROSTER", selected.Code);
}
[Fact]
public void DuplicatePackage_IsRejected()
{
const string json = """{"schemaVersion":1,"rosterId":"project-a","product":"Plant3D","year":2026,"architecture":"x64","offlinePolicy":"AllowVerifiedLastKnownGood","packages":[{"id":"P_A","version":"1.0.0","channel":"stable"},{"id":"p_a","version":"2.0.0","channel":"stable"}]}""";
var ex = Assert.Throws(() => new RosterAdapter().ParseJson(json));
Assert.Equal("ROSTER_DUPLICATE_PACKAGE", ex.Message);
}
[Fact]
public void LegacyXml_RejectsDtd()
{
const string xml = "]>";
Assert.ThrowsAny(() => new RosterAdapter().ParseLegacyDownloadList(xml, Context));
}
[Fact]
public async Task FileSystemCatalog_ReturnsDescriptorOnly_ForMatchingRosterRequirement()
{
var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N")); var packageRoot=Path.Combine(root,"P_A","1.0.0"); Directory.CreateDirectory(packageRoot);
try
{
var bytes=new byte[]{1}; await File.WriteAllBytesAsync(Path.Combine(packageRoot,"entry.dll"),bytes);
var hash=Convert.ToHexString(SHA256.HashData(bytes));
await File.WriteAllTextAsync(Path.Combine(packageRoot,"package.manifest.json"), $$"""{"schemaVersion":1,"packageId":"P_A","version":"1.0.0","product":"Plant3D","productYear":2026,"architecture":"x64","entryAssembly":"entry.dll","files":[{"path":"entry.dll","length":1,"sha256":"{{hash}}"}],"startup":{"type":"netload","command":"P_A"},"rolloutChannel":"stable"}""");
var result=await new FileSystemCatalogPackageAcquirer(CatalogRoot(root)).AcquireAsync(new("P_A","Plant3D",2026,"x64","stable","1.0.0","project-a",true),CancellationToken.None);
Assert.NotNull(result); Assert.Equal(packageRoot,result!.SourceRoot); Assert.False(result.SourceRoot.StartsWith(@"\\",StringComparison.Ordinal));
}
finally { Directory.Delete(root,true); }
}
[Fact]
public async Task FileSystemCatalog_RejectsTraversalInManifest()
{
var root=Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString("N")); var packageRoot=Path.Combine(root,"P_A","1.0.0"); Directory.CreateDirectory(packageRoot);
try
{
await File.WriteAllTextAsync(Path.Combine(packageRoot,"package.manifest.json"), """{"schemaVersion":1,"packageId":"P_A","version":"1.0.0","product":"Plant3D","productYear":2026,"architecture":"x64","entryAssembly":"entry.dll","files":[{"path":"../outside.dll","length":1,"sha256":"00"}],"startup":{"type":"netload","command":"P_A"},"rolloutChannel":"stable"}""");
Assert.Null(await new FileSystemCatalogPackageAcquirer(CatalogRoot(root)).AcquireAsync(new("P_A","Plant3D",2026,"x64","stable","1.0.0","project-a",true),CancellationToken.None));
}
finally { Directory.Delete(root,true); }
}
private static string Json() => """{"schemaVersion":1,"rosterId":"project-a","product":"Plant3D","year":2026,"architecture":"x64","offlinePolicy":"AllowVerifiedLastKnownGood","packages":[{"id":"P_B","version":"2.0.0","channel":"stable","loadOrder":200},{"id":"P_A","version":"1.0.0","channel":"stable","loadOrder":100}]}""";
private static ResolvedCatalogRoot CatalogRoot(string root) => new PathResolver().ResolveCatalogRoot(new(new Dictionary { [LauncherPathVariableNames.CorporateDfsRoot] = Path.GetDirectoryName(root)!, [LauncherPathVariableNames.CorporateCatalogRoot] = root }));
}