using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
///
/// Tests for the read-only authoring resolve service (M9/T26a). The resolved
/// inherited member set MUST agree with what
/// produces on deploy — the precedence is exercised both directly and via a
/// cross-check against the flattener.
///
public class TemplateInheritanceResolverTests
{
private static TemplateAttribute Attr(string name, int id, int templateId, string? value = null,
bool isInherited = false, bool isLocked = false, bool lockedInDerived = false) =>
new(name)
{
Id = id,
TemplateId = templateId,
DataType = DataType.String,
Value = value,
IsInherited = isInherited,
IsLocked = isLocked,
LockedInDerived = lockedInDerived
};
// ── (a) A→B→C chain: resolving C returns members inherited transitively from A ──
[Fact]
public void Resolve_ThreeLevelChain_IncludesTransitivelyInheritedGrandparentMember()
{
var a = new Template("A") { Id = 1 };
a.Attributes.Add(Attr("FromGrandparent", id: 10, templateId: 1, value: "gp"));
var b = new Template("B") { Id = 2, ParentTemplateId = 1 };
b.Attributes.Add(Attr("FromParent", id: 20, templateId: 2, value: "p"));
var c = new Template("C") { Id = 3, ParentTemplateId = 2 };
c.Attributes.Add(Attr("FromChild", id: 30, templateId: 3, value: "c"));
var resolved = TemplateInheritanceResolver.Resolve(3, new[] { a, b, c });
Assert.Equal(3, resolved.Attributes.Count);
var fromGp = Assert.Single(resolved.Attributes, m => m.Name == "FromGrandparent");
Assert.True(fromGp.IsInherited);
Assert.Equal(1, fromGp.OriginTemplateId);
Assert.Equal("A", fromGp.OriginTemplateName);
Assert.Equal("gp", fromGp.EffectiveValue);
var fromParent = Assert.Single(resolved.Attributes, m => m.Name == "FromParent");
Assert.True(fromParent.IsInherited);
Assert.Equal("B", fromParent.OriginTemplateName);
var fromChild = Assert.Single(resolved.Attributes, m => m.Name == "FromChild");
Assert.False(fromChild.IsInherited);
Assert.Equal(3, fromChild.OriginTemplateId);
}
// ── (b) a base member ADDED to A after C was created appears in C's resolved set ──
[Fact]
public void Resolve_BaseMemberAddedAfterDerivedCreated_AppearsInResolvedSet()
{
// C was created carrying a single inherited placeholder for "Original".
// "AddedLater" was then added to the grandparent A; it has NO stored row
// on C, yet must surface in C's freshly-resolved set.
var a = new Template("A") { Id = 1 };
a.Attributes.Add(Attr("Original", id: 10, templateId: 1, value: "v1"));
a.Attributes.Add(Attr("AddedLater", id: 11, templateId: 1, value: "new"));
var b = new Template("B") { Id = 2, ParentTemplateId = 1 };
var c = new Template("C") { Id = 3, ParentTemplateId = 2 };
// Stale placeholder for the one member C knew about at creation time.
c.Attributes.Add(Attr("Original", id: 30, templateId: 3, value: "v1", isInherited: true));
var resolved = TemplateInheritanceResolver.Resolve(3, new[] { a, b, c });
Assert.Contains(resolved.Attributes, m => m.Name == "AddedLater" && m.EffectiveValue == "new" && m.IsInherited);
Assert.Contains(resolved.Attributes, m => m.Name == "Original");
}
// ── (c) a locked member is flagged ──
[Fact]
public void Resolve_LockedAndBaseLockedMembers_Flagged()
{
var baseT = new Template("Base") { Id = 1 };
baseT.Attributes.Add(Attr("Locked", id: 10, templateId: 1, value: "x", isLocked: true));
baseT.Attributes.Add(Attr("BaseLocked", id: 11, templateId: 1, value: "y", lockedInDerived: true));
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
var locked = Assert.Single(resolved.Attributes, m => m.Name == "Locked");
Assert.True(locked.IsLocked);
var baseLocked = Assert.Single(resolved.Attributes, m => m.Name == "BaseLocked");
Assert.True(baseLocked.IsBaseLocked);
Assert.True(baseLocked.IsInherited);
}
// ── (d) own override wins over inherited ──
[Fact]
public void Resolve_OwnOverride_WinsOverInherited()
{
var baseT = new Template("Base") { Id = 1 };
baseT.Attributes.Add(Attr("Speed", id: 10, templateId: 1, value: "100"));
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
// Explicit override (IsInherited=false) — must win, value 200, origin = derived.
derived.Attributes.Add(Attr("Speed", id: 20, templateId: 2, value: "200", isInherited: false));
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
var speed = Assert.Single(resolved.Attributes, m => m.Name == "Speed");
Assert.Equal("200", speed.EffectiveValue);
Assert.Equal(2, speed.OriginTemplateId);
Assert.False(speed.IsInherited);
}
[Fact]
public void Resolve_InheritedPlaceholder_DoesNotShadowLiveBaseValue()
{
// Derived carries an IsInherited placeholder whose value is stale ("old").
// The base value has since changed to "fresh"; the live base value wins.
var baseT = new Template("Base") { Id = 1 };
baseT.Attributes.Add(Attr("Speed", id: 10, templateId: 1, value: "fresh"));
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
derived.Attributes.Add(Attr("Speed", id: 20, templateId: 2, value: "old", isInherited: true));
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
var speed = Assert.Single(resolved.Attributes, m => m.Name == "Speed");
Assert.Equal("fresh", speed.EffectiveValue);
Assert.True(speed.IsInherited);
Assert.Equal(1, speed.OriginTemplateId); // base supplied the live value
}
// ── (e) staleness summary true when stored rows differ; false when in sync ──
[Fact]
public void Resolve_StalenessSummary_TrueWhenBaseAddedMemberMissingFromStoredRows()
{
var baseT = new Template("Base") { Id = 1 };
baseT.Attributes.Add(Attr("Original", id: 10, templateId: 1, value: "v"));
baseT.Attributes.Add(Attr("AddedLater", id: 11, templateId: 1, value: "new"));
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
// Only the original placeholder is stored; "AddedLater" is missing → stale.
derived.Attributes.Add(Attr("Original", id: 20, templateId: 2, value: "v", isInherited: true));
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
Assert.True(resolved.Staleness.IsStale);
Assert.Equal(1, resolved.Staleness.DifferingMemberCount);
}
[Fact]
public void Resolve_StalenessSummary_FalseWhenStoredRowsInSync()
{
var baseT = new Template("Base") { Id = 1 };
baseT.Attributes.Add(Attr("Speed", id: 10, templateId: 1, value: "100"));
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
// Stored placeholder matches the live base value exactly.
derived.Attributes.Add(Attr("Speed", id: 20, templateId: 2, value: "100", isInherited: true));
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
Assert.False(resolved.Staleness.IsStale);
Assert.Equal(0, resolved.Staleness.DifferingMemberCount);
}
[Fact]
public void Resolve_StalenessSummary_TrueWhenStoredInheritedValueIsStale()
{
var baseT = new Template("Base") { Id = 1 };
baseT.Attributes.Add(Attr("Speed", id: 10, templateId: 1, value: "fresh"));
var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
derived.Attributes.Add(Attr("Speed", id: 20, templateId: 2, value: "old", isInherited: true));
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
Assert.True(resolved.Staleness.IsStale);
Assert.Equal(1, resolved.Staleness.DifferingMemberCount);
}
// ── (f) a composition-derived template resolves sanely ──
[Fact]
public void Resolve_CompositionDerivedTemplate_ResolvesOwnAndInheritedMembers()
{
// IsDerived templates back a composition slot; they still inherit from a
// base via ParentTemplateId. The resolver must handle them like any
// other derived template (inheritance only — composition is the
// flattener's concern).
var baseT = new Template("ComposedBase") { Id = 1 };
baseT.Attributes.Add(Attr("Pressure", id: 10, templateId: 1, value: "10"));
var derived = new Template("Slot") { Id = 2, ParentTemplateId = 1, IsDerived = true, OwnerCompositionId = 99 };
derived.Attributes.Add(Attr("LocalTweak", id: 20, templateId: 2, value: "t"));
var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });
Assert.Equal(2, resolved.TemplateId);
Assert.Contains(resolved.Attributes, m => m.Name == "Pressure" && m.IsInherited);
Assert.Contains(resolved.Attributes, m => m.Name == "LocalTweak" && !m.IsInherited);
}
[Fact]
public void Resolve_UnknownTemplate_ReturnsEmptySet()
{
var resolved = TemplateInheritanceResolver.Resolve(999, Array.Empty());
Assert.Empty(resolved.Attributes);
Assert.False(resolved.Staleness.IsStale);
}
// ── Cross-member-type coverage: alarms, scripts, native alarm sources ──
[Fact]
public void Resolve_AllMemberTypes_TransitivelyInherited()
{
var a = new Template("A") { Id = 1 };
a.Attributes.Add(Attr("Attr", id: 10, templateId: 1, value: "v"));
a.Alarms.Add(new TemplateAlarm("Alarm") { Id = 11, TemplateId = 1, PriorityLevel = 5, TriggerType = AlarmTriggerType.ValueMatch });
a.Scripts.Add(new TemplateScript("Script", "code") { Id = 12, TemplateId = 1 });
a.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Native") { Id = 13, TemplateId = 1, ConnectionName = "conn", SourceReference = "ns=2;s=Alarm" });
var b = new Template("B") { Id = 2, ParentTemplateId = 1 };
var c = new Template("C") { Id = 3, ParentTemplateId = 2 };
var resolved = TemplateInheritanceResolver.Resolve(3, new[] { a, b, c });
Assert.Contains(resolved.Attributes, m => m.Name == "Attr" && m.IsInherited && m.OriginTemplateName == "A");
Assert.Contains(resolved.Alarms, m => m.Name == "Alarm" && m.IsInherited && m.OriginTemplateName == "A");
Assert.Contains(resolved.Scripts, m => m.Name == "Script" && m.IsInherited && m.OriginTemplateName == "A");
Assert.Contains(resolved.NativeAlarmSources, m => m.Name == "Native" && m.IsInherited && m.OriginTemplateName == "A");
}
// ── Cross-check: the resolved inherited values MUST equal the flattener's
// output for a no-override instance over the same chain. This is the
// guarantee that the editor preview agrees with deploy. ──
[Fact]
public void Resolve_AgreesWithFlatteningService_ForNoOverrideInstance()
{
var a = new Template("A") { Id = 1 };
a.Attributes.Add(Attr("Speed", id: 10, templateId: 1, value: "100"));
a.Attributes.Add(Attr("BaseOnly", id: 11, templateId: 1, value: "b"));
var b = new Template("B") { Id = 2, ParentTemplateId = 1 };
// Override Speed; inherited placeholder for BaseOnly (stale value to prove live base wins).
b.Attributes.Add(Attr("Speed", id: 20, templateId: 2, value: "200", isInherited: false));
b.Attributes.Add(Attr("BaseOnly", id: 21, templateId: 2, value: "stale", isInherited: true));
var c = new Template("C") { Id = 3, ParentTemplateId = 2 };
c.Attributes.Add(Attr("ChildOnly", id: 30, templateId: 3, value: "c"));
var all = new[] { a, b, c };
// Resolver view.
var resolved = TemplateInheritanceResolver.Resolve(3, all);
// Flattener view: most-derived-first chain, no overrides, no compositions.
var instance = new Instance("inst") { Id = 1, TemplateId = 3, SiteId = 1 };
var chain = new List { c, b, a }; // most-derived first
var flat = new FlatteningService().Flatten(
instance,
chain,
new Dictionary>(),
new Dictionary>(),
new Dictionary());
Assert.True(flat.IsSuccess, flat.IsFailure ? flat.Error : null);
// Every flattened attribute value must equal the resolver's effective value.
foreach (var fa in flat.Value.Attributes)
{
var r = Assert.Single(resolved.Attributes, m => m.Name == fa.CanonicalName);
Assert.Equal(fa.Value, r.EffectiveValue);
}
Assert.Equal(flat.Value.Attributes.Count, resolved.Attributes.Count);
}
}