using System.Text.Json;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
///
/// v3 WP4 byte-parity: the artifact-decode seam (DeploymentArtifact.ParseComposition) and the
/// live compose seam (AddressSpaceComposer.Compose) must resolve {{equip}}/<RefName>
/// script paths to the SAME backing RawPath — both build the per-equipment reference map (effective name
/// → RawPath) from UnsTagReferences + Tags + the raw topology via the shared
/// EquipmentReferenceMap + RawPathResolver. Exercises both a VirtualTag script and a
/// scripted-alarm predicate, plus an override-named reference.
///
public sealed class DeploymentArtifactEquipRefParityTests
{
// Reference effective name "MotorSpeed" (a DisplayNameOverride) backing raw tag leaf "Speed" →
// RawPath "Modbus/Dev1/Speed" (driver at cluster root, no folder / group).
private const string VtScript = "return System.Convert.ToInt32(ctx.GetTag(\"{{equip}}/MotorSpeed\").Value) > 50;";
private const string AlarmScript = "return System.Convert.ToDouble(ctx.GetTag(\"{{equip}}/MotorSpeed\").Value) > 90;";
private const string ExpectedRawPath = "Modbus/Dev1/Speed";
private static DriverInstance Driver() => new()
{
DriverInstanceId = "drv-1", ClusterId = "c1", Name = "Modbus", DriverType = "Modbus",
DriverConfig = "{}", RawFolderId = null,
};
private static Device Dev() => new()
{
DeviceId = "dev-1", DriverInstanceId = "drv-1", Name = "Dev1", DeviceConfig = "{}",
};
private static Tag RawTag() => new()
{
TagId = "tag-1", DeviceId = "dev-1", Name = "Speed", DataType = "Float",
AccessLevel = TagAccessLevel.Read, TagConfig = "{}",
};
private static UnsTagReference Ref() => new()
{
UnsTagReferenceId = "ref-1", EquipmentId = "eq-1", TagId = "tag-1", DisplayNameOverride = "MotorSpeed",
};
[Fact]
public void VirtualTag_equip_ref_resolves_byte_parity()
{
var script = new Script { ScriptId = "s-1", Name = "n", SourceCode = VtScript, SourceHash = "h" };
var vt = new VirtualTag { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Over", DataType = "Boolean", ScriptId = "s-1" };
var composed = AddressSpaceComposer.Compose(
Array.Empty(), Array.Empty(), Array.Empty(),
new[] { Driver() }, Array.Empty(),
virtualTags: new[] { vt }, scripts: new[] { script },
unsTagReferences: new[] { Ref() }, tags: new[] { RawTag() },
rawFolders: Array.Empty(), devices: new[] { Dev() }, tagGroups: Array.Empty());
var blob = JsonSerializer.SerializeToUtf8Bytes(new
{
DriverInstances = new[] { new { DriverInstanceId = "drv-1", DriverType = "Modbus", DriverConfig = "{}", Name = "Modbus", RawFolderId = (string?)null } },
Devices = new[] { new { DeviceId = "dev-1", DriverInstanceId = "drv-1", Name = "Dev1", DeviceConfig = "{}" } },
Tags = new[] { new { TagId = "tag-1", DeviceId = "dev-1", Name = "Speed", DataType = "Float", TagConfig = "{}" } },
UnsTagReferences = new[] { new { UnsTagReferenceId = "ref-1", EquipmentId = "eq-1", TagId = "tag-1", DisplayNameOverride = "MotorSpeed" } },
Scripts = new[] { new { ScriptId = "s-1", SourceCode = VtScript } },
VirtualTags = new[] { new { VirtualTagId = "vt-1", EquipmentId = "eq-1", Name = "Over", DataType = "Boolean", ScriptId = "s-1" } },
});
var decoded = DeploymentArtifact.ParseComposition(blob);
// The token resolved to the backing RawPath on BOTH sides, and the plans are byte-identical.
var cPlan = composed.EquipmentVirtualTags.ShouldHaveSingleItem();
cPlan.Expression.ShouldContain($"ctx.GetTag(\"{ExpectedRawPath}\")");
cPlan.Expression.ShouldNotContain("{{equip}}");
cPlan.DependencyRefs.ShouldBe(new[] { ExpectedRawPath });
var dPlan = decoded.EquipmentVirtualTags.ShouldHaveSingleItem();
dPlan.ShouldBe(cPlan);
}
[Fact]
public void ScriptedAlarm_predicate_equip_ref_resolves_byte_parity()
{
var script = new Script { ScriptId = "s-1", Name = "n", SourceCode = AlarmScript, SourceHash = "h" };
var alarm = new ScriptedAlarm
{
ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "Hot", AlarmType = "LimitAlarm",
Severity = 700, MessageTemplate = "hot", PredicateScriptId = "s-1",
HistorizeToAveva = true, Retain = true, Enabled = true,
};
var composed = AddressSpaceComposer.Compose(
Array.Empty(), Array.Empty(), Array.Empty(),
new[] { Driver() }, new[] { alarm },
virtualTags: Array.Empty(), scripts: new[] { script },
unsTagReferences: new[] { Ref() }, tags: new[] { RawTag() },
rawFolders: Array.Empty(), devices: new[] { Dev() }, tagGroups: Array.Empty());
var blob = JsonSerializer.SerializeToUtf8Bytes(new
{
DriverInstances = new[] { new { DriverInstanceId = "drv-1", DriverType = "Modbus", DriverConfig = "{}", Name = "Modbus", RawFolderId = (string?)null } },
Devices = new[] { new { DeviceId = "dev-1", DriverInstanceId = "drv-1", Name = "Dev1", DeviceConfig = "{}" } },
Tags = new[] { new { TagId = "tag-1", DeviceId = "dev-1", Name = "Speed", DataType = "Float", TagConfig = "{}" } },
UnsTagReferences = new[] { new { UnsTagReferenceId = "ref-1", EquipmentId = "eq-1", TagId = "tag-1", DisplayNameOverride = "MotorSpeed" } },
Scripts = new[] { new { ScriptId = "s-1", SourceCode = AlarmScript } },
ScriptedAlarms = new[]
{
new { ScriptedAlarmId = "al-1", EquipmentId = "eq-1", Name = "Hot", AlarmType = "LimitAlarm", Severity = 700, MessageTemplate = "hot", PredicateScriptId = "s-1", HistorizeToAveva = true, Retain = true, Enabled = true },
},
});
var decoded = DeploymentArtifact.ParseComposition(blob);
var cPlan = composed.EquipmentScriptedAlarms.ShouldHaveSingleItem();
cPlan.PredicateSource.ShouldContain($"ctx.GetTag(\"{ExpectedRawPath}\")");
cPlan.PredicateSource.ShouldNotContain("{{equip}}");
cPlan.DependencyRefs.ShouldBe(new[] { ExpectedRawPath });
var dPlan = decoded.EquipmentScriptedAlarms.ShouldHaveSingleItem();
dPlan.ShouldBe(cPlan);
}
}