using ScadaLink.Commons.Entities.Instances; using ScadaLink.Commons.Entities.Sites; using ScadaLink.Commons.Entities.Templates; using ScadaLink.Commons.Types.Enums; using ScadaLink.TemplateEngine.Flattening; namespace ScadaLink.TemplateEngine.Tests.Flattening; public class FlatteningServiceTests { private readonly FlatteningService _sut = new(); private static Instance CreateInstance(string name = "TestInstance", int templateId = 1, int siteId = 1) => new(name) { Id = 1, TemplateId = templateId, SiteId = siteId }; private static Template CreateTemplate(int id, string name, int? parentId = null) { var t = new Template(name) { Id = id, ParentTemplateId = parentId }; return t; } [Fact] public void Flatten_EmptyTemplateChain_ReturnsFailure() { var instance = CreateInstance(); var result = _sut.Flatten( instance, [], new Dictionary>(), new Dictionary>(), new Dictionary()); Assert.True(result.IsFailure); Assert.Contains("empty", result.Error, StringComparison.OrdinalIgnoreCase); } [Fact] public void Flatten_SingleTemplate_ResolvesAttributes() { var template = CreateTemplate(1, "Base"); template.Attributes.Add(new TemplateAttribute("Temperature") { DataType = DataType.Double, Value = "25.0" }); template.Attributes.Add(new TemplateAttribute("Status") { DataType = DataType.String, Value = "OK" }); var instance = CreateInstance(); var result = _sut.Flatten( instance, [template], new Dictionary>(), new Dictionary>(), new Dictionary()); Assert.True(result.IsSuccess); Assert.Equal(2, result.Value.Attributes.Count); Assert.Equal("Temperature", result.Value.Attributes[1].CanonicalName); // Sorted Assert.Equal("25.0", result.Value.Attributes[1].Value); Assert.Equal("Status", result.Value.Attributes[0].CanonicalName); } [Fact] public void Flatten_InheritanceChain_DerivedOverridesBase() { var baseTemplate = CreateTemplate(2, "Base"); baseTemplate.Attributes.Add(new TemplateAttribute("Speed") { DataType = DataType.Double, Value = "100.0" }); baseTemplate.Attributes.Add(new TemplateAttribute("BaseOnly") { DataType = DataType.String, Value = "base" }); var childTemplate = CreateTemplate(1, "Child", parentId: 2); childTemplate.Attributes.Add(new TemplateAttribute("Speed") { DataType = DataType.Double, Value = "200.0" }); childTemplate.Attributes.Add(new TemplateAttribute("ChildOnly") { DataType = DataType.Int32, Value = "42" }); // Chain: [child, base] — most-derived first var instance = CreateInstance(); var result = _sut.Flatten( instance, [childTemplate, baseTemplate], new Dictionary>(), new Dictionary>(), new Dictionary()); Assert.True(result.IsSuccess); Assert.Equal(3, result.Value.Attributes.Count); var speed = result.Value.Attributes.First(a => a.CanonicalName == "Speed"); Assert.Equal("200.0", speed.Value); // Child's value wins } [Fact] public void Flatten_LockedAttribute_NotOverriddenByDerived() { var baseTemplate = CreateTemplate(2, "Base"); baseTemplate.Attributes.Add(new TemplateAttribute("Locked") { DataType = DataType.String, Value = "locked", IsLocked = true }); var childTemplate = CreateTemplate(1, "Child", parentId: 2); childTemplate.Attributes.Add(new TemplateAttribute("Locked") { DataType = DataType.String, Value = "overridden" }); var instance = CreateInstance(); var result = _sut.Flatten( instance, [childTemplate, baseTemplate], new Dictionary>(), new Dictionary>(), new Dictionary()); Assert.True(result.IsSuccess); var locked = result.Value.Attributes.First(a => a.CanonicalName == "Locked"); Assert.Equal("locked", locked.Value); // Base locked value preserved } [Fact] public void Flatten_InstanceOverride_AppliedToUnlockedAttribute() { var template = CreateTemplate(1, "Base"); template.Attributes.Add(new TemplateAttribute("Threshold") { DataType = DataType.Double, Value = "50.0" }); var instance = CreateInstance(); instance.AttributeOverrides.Add(new InstanceAttributeOverride("Threshold") { OverrideValue = "75.0" }); var result = _sut.Flatten( instance, [template], new Dictionary>(), new Dictionary>(), new Dictionary()); Assert.True(result.IsSuccess); var attr = result.Value.Attributes.First(a => a.CanonicalName == "Threshold"); Assert.Equal("75.0", attr.Value); Assert.Equal("Override", attr.Source); } [Fact] public void Flatten_InstanceOverride_SkippedForLockedAttribute() { var template = CreateTemplate(1, "Base"); template.Attributes.Add(new TemplateAttribute("Locked") { DataType = DataType.String, Value = "original", IsLocked = true }); var instance = CreateInstance(); instance.AttributeOverrides.Add(new InstanceAttributeOverride("Locked") { OverrideValue = "changed" }); var result = _sut.Flatten( instance, [template], new Dictionary>(), new Dictionary>(), new Dictionary()); Assert.True(result.IsSuccess); var attr = result.Value.Attributes.First(a => a.CanonicalName == "Locked"); Assert.Equal("original", attr.Value); // Lock honored } [Fact] public void Flatten_ComposedModule_PathQualifiedNames() { var composedTemplate = CreateTemplate(2, "Pump"); composedTemplate.Attributes.Add(new TemplateAttribute("RPM") { DataType = DataType.Double, Value = "1500" }); composedTemplate.Scripts.Add(new TemplateScript("Start", "// start") { Id = 10 }); var parentTemplate = CreateTemplate(1, "Station"); parentTemplate.Attributes.Add(new TemplateAttribute("StationName") { DataType = DataType.String, Value = "S1" }); var compositions = new Dictionary> { [1] = new List { new("MainPump") { ComposedTemplateId = 2 } } }; var composedChains = new Dictionary> { [2] = [composedTemplate] }; var instance = CreateInstance(); var result = _sut.Flatten( instance, [parentTemplate], compositions, composedChains, new Dictionary()); Assert.True(result.IsSuccess); Assert.Contains(result.Value.Attributes, a => a.CanonicalName == "MainPump.RPM"); Assert.Contains(result.Value.Attributes, a => a.CanonicalName == "StationName"); Assert.Contains(result.Value.Scripts, s => s.CanonicalName == "MainPump.Start"); } [Fact] public void Flatten_ConnectionBindings_ResolvedCorrectly() { var template = CreateTemplate(1, "Base"); template.Attributes.Add(new TemplateAttribute("Temp") { DataType = DataType.Double, DataSourceReference = "ns=2;s=Temperature" }); var instance = CreateInstance(); instance.ConnectionBindings.Add(new InstanceConnectionBinding("Temp") { DataConnectionId = 100 }); var connections = new Dictionary { [100] = new("OPC-Server1", "OpcUa", 1) { Id = 100, PrimaryConfiguration = "opc.tcp://localhost:4840" } }; var result = _sut.Flatten( instance, [template], new Dictionary>(), new Dictionary>(), connections); Assert.True(result.IsSuccess); var attr = result.Value.Attributes.First(a => a.CanonicalName == "Temp"); Assert.Equal(100, attr.BoundDataConnectionId); Assert.Equal("OPC-Server1", attr.BoundDataConnectionName); Assert.Equal("OpcUa", attr.BoundDataConnectionProtocol); } [Fact] public void Flatten_Alarms_ResolvedFromChain() { var template = CreateTemplate(1, "Base"); template.Alarms.Add(new TemplateAlarm("HighTemp") { TriggerType = AlarmTriggerType.RangeViolation, TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":100}", PriorityLevel = 1 }); var instance = CreateInstance(); var result = _sut.Flatten( instance, [template], new Dictionary>(), new Dictionary>(), new Dictionary()); Assert.True(result.IsSuccess); Assert.Single(result.Value.Alarms); Assert.Equal("HighTemp", result.Value.Alarms[0].CanonicalName); Assert.Equal("RangeViolation", result.Value.Alarms[0].TriggerType); } [Fact] public void Flatten_InstanceMetadata_SetCorrectly() { var template = CreateTemplate(1, "Base"); var instance = CreateInstance("MyInstance", templateId: 1, siteId: 5); instance.AreaId = 3; var result = _sut.Flatten( instance, [template], new Dictionary>(), new Dictionary>(), new Dictionary()); Assert.True(result.IsSuccess); Assert.Equal("MyInstance", result.Value.InstanceUniqueName); Assert.Equal(1, result.Value.TemplateId); Assert.Equal(5, result.Value.SiteId); Assert.Equal(3, result.Value.AreaId); } }