using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Import;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests.Import;
///
/// End-to-end golden-snapshot test. Loads the canonical CSV fixture from
/// Fixtures/rslogix-canonical.csv, runs it through
/// , then compares the resulting tag list to
/// Fixtures/rslogix-canonical-expected.json.
///
///
///
/// Mismatch path: the test writes the actual JSON to a temp file path and prints the
/// path in the failure message, so the dev can run
/// cp $TEMP/rslogix-canonical-actual.json tests/.../Fixtures/rslogix-canonical-expected.json
/// to bless the new shape. Treats both sides as trees so
/// insignificant whitespace + key-order differences don't false-fail.
///
///
[Trait("Category", "Unit")]
public sealed class RsLogixSymbolImportGoldenTests
{
private const string Device = "ab://192.168.1.20/1,0";
private static string FixturePath(string name) =>
Path.Combine(AppContext.BaseDirectory, "Fixtures", name);
[Fact]
public void Canonical_csv_matches_golden_json()
{
var importer = new RsLogixSymbolImport(NullLogger.Instance);
using var stream = File.OpenRead(FixturePath("rslogix-canonical.csv"));
var result = importer.Parse(stream, Device);
result.ParsedCount.ShouldBe(8);
result.SkippedCount.ShouldBe(0);
result.ErrorCount.ShouldBe(0);
var actualPayload = new
{
Tags = result.Tags.Select(t => new
{
Name = t.Name,
DeviceHostAddress = t.DeviceHostAddress,
Address = t.Address,
DataType = t.DataType.ToString(),
Writable = t.Writable,
}).ToArray()
};
var actualJson = JsonSerializer.Serialize(actualPayload,
new JsonSerializerOptions { WriteIndented = true });
var expectedJson = File.ReadAllText(FixturePath("rslogix-canonical-expected.json"));
var actualNode = JsonNode.Parse(actualJson)!;
var expectedNode = JsonNode.Parse(expectedJson)!;
if (!JsonTreesEqual(actualNode, expectedNode))
{
// Dump the actual JSON to a discoverable temp path so the dev can `cp` it over
// the fixture once they've reviewed the diff. The test message points straight
// at the file.
var dump = Path.Combine(Path.GetTempPath(), "rslogix-canonical-actual.json");
File.WriteAllText(dump, actualJson);
throw new Xunit.Sdk.XunitException(
$"RSLogix golden mismatch. Actual written to: {dump}\n--- Expected ---\n{expectedJson}\n--- Actual ---\n{actualJson}");
}
}
///
/// Structural JSON equality — recursively compares two trees
/// by shape + value, ignoring property-order on objects. Cheaper than pulling in a
/// dedicated diff library for one assertion.
///
private static bool JsonTreesEqual(JsonNode? a, JsonNode? b)
{
if (a is null && b is null) return true;
if (a is null || b is null) return false;
if (a is JsonObject ao && b is JsonObject bo)
{
if (ao.Count != bo.Count) return false;
foreach (var kvp in ao)
{
if (!bo.TryGetPropertyValue(kvp.Key, out var bv)) return false;
if (!JsonTreesEqual(kvp.Value, bv)) return false;
}
return true;
}
if (a is JsonArray aa && b is JsonArray ba)
{
if (aa.Count != ba.Count) return false;
for (var i = 0; i < aa.Count; i++)
{
if (!JsonTreesEqual(aa[i], ba[i])) return false;
}
return true;
}
// Primitives — fall back to canonical JSON form for value equality.
return a.ToJsonString() == b.ToJsonString();
}
}