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;

/// <summary>
/// Tests for the read-only authoring resolve service (M9/T26a). The resolved
/// inherited member set MUST agree with what <see cref="FlatteningService"/>
/// produces on deploy — the precedence is exercised both directly and via a
/// cross-check against the flattener.
/// </summary>
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<Template>());
        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<Template> { c, b, a }; // most-derived first
        var flat = new FlatteningService().Flatten(
            instance,
            chain,
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        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);
    }

    // ── I1: HiLo per-setpoint merge — the resolved alarm's effective
    //    TriggerConfiguration MUST equal the FlatteningService-merged result for
    //    a derived template that overrides only one setpoint (preview = deploy). ──

    [Fact]
    public void Resolve_AgreesWithFlatteningService_ForPartialHiLoOverride()
    {
        // Base HiLo alarm carries both hi + lo. Derived overrides ONLY hi and
        // inherits lo. The resolver must surface the MERGED effective trigger
        // config (lo inherited, hi overridden) — identical to what deploy
        // produces via the flattener's MergeHiLoConfig.
        var baseT = new Template("Base") { Id = 1 };
        baseT.Alarms.Add(new TemplateAlarm("Level")
        {
            Id = 10,
            TemplateId = 1,
            PriorityLevel = 5,
            TriggerType = AlarmTriggerType.HiLo,
            TriggerConfiguration = "{\"hi\":90,\"lo\":10}"
        });

        var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
        // Partial override: only hi changes; lo must be inherited from base.
        derived.Alarms.Add(new TemplateAlarm("Level")
        {
            Id = 20,
            TemplateId = 2,
            PriorityLevel = 5,
            TriggerType = AlarmTriggerType.HiLo,
            TriggerConfiguration = "{\"hi\":95}",
            IsInherited = false
        });

        var all = new[] { baseT, derived };

        // Resolver view.
        var resolved = TemplateInheritanceResolver.Resolve(2, all);
        var resolvedAlarm = Assert.Single(resolved.Alarms, m => m.Name == "Level");

        // Flattener view: most-derived-first chain, no overrides, no compositions.
        var instance = new Instance("inst") { Id = 1, TemplateId = 2, SiteId = 1 };
        var chain = new List<Template> { derived, baseT }; // most-derived first
        var flat = new FlatteningService().Flatten(
            instance,
            chain,
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(flat.IsSuccess, flat.IsFailure ? flat.Error : null);
        var flatAlarm = Assert.Single(flat.Value.Alarms, a => a.CanonicalName == "Level");

        // The resolver's effective TriggerConfiguration MUST equal the flattener's
        // merged output verbatim — the key regression guard for preview = deploy.
        Assert.Equal(flatAlarm.TriggerConfiguration, resolvedAlarm.EffectiveTriggerConfiguration);

        // And sanity: the merge actually happened (lo inherited, hi overridden).
        Assert.Equal(
            FlatteningService.MergeHiLoConfig("{\"hi\":90,\"lo\":10}", "{\"hi\":95}"),
            resolvedAlarm.EffectiveTriggerConfiguration);
        Assert.Contains("\"lo\":10", resolvedAlarm.EffectiveTriggerConfiguration);
        Assert.Contains("\"hi\":95", resolvedAlarm.EffectiveTriggerConfiguration);
    }

    // ── I1b: base NON-HiLo alarm overridden by a derived HiLo alarm — the resolver
    //    must NOT attempt the per-setpoint merge (guard matches the flattener's
    //    both-HiLo gate). Derived config must win verbatim (whole-replace). ──

    [Fact]
    public void Resolve_BaseNonHiLo_DerivedHiLo_DerivedConfigWinsVerbatim()
    {
        // Base uses a non-HiLo trigger type (ValueMatch, JSON-object config).
        // Derived overrides to HiLo. The resolver must take the same path the
        // flattener takes: derived config wins verbatim (no per-setpoint merge).
        var baseT = new Template("Base") { Id = 1 };
        baseT.Alarms.Add(new TemplateAlarm("Level")
        {
            Id = 10,
            TemplateId = 1,
            PriorityLevel = 5,
            TriggerType = AlarmTriggerType.ValueMatch,
            TriggerConfiguration = "{\"value\":42}"
        });

        var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
        derived.Alarms.Add(new TemplateAlarm("Level")
        {
            Id = 20,
            TemplateId = 2,
            PriorityLevel = 5,
            TriggerType = AlarmTriggerType.HiLo,
            TriggerConfiguration = "{\"hi\":90,\"lo\":10}",
            IsInherited = false
        });

        var all = new[] { baseT, derived };

        // Resolver view.
        var resolved = TemplateInheritanceResolver.Resolve(2, all);
        var resolvedAlarm = Assert.Single(resolved.Alarms, m => m.Name == "Level");

        // Flattener view: no merge expected (base non-HiLo), derived config verbatim.
        var instance = new Instance("inst") { Id = 1, TemplateId = 2, SiteId = 1 };
        var chain = new List<Template> { derived, baseT }; // most-derived first
        var flat = new FlatteningService().Flatten(
            instance,
            chain,
            new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
            new Dictionary<int, IReadOnlyList<Template>>(),
            new Dictionary<int, DataConnection>());

        Assert.True(flat.IsSuccess, flat.IsFailure ? flat.Error : null);
        var flatAlarm = Assert.Single(flat.Value.Alarms, a => a.CanonicalName == "Level");

        // The resolver's effective TriggerConfiguration MUST equal the flattener's
        // result — derived config verbatim, not a merge of base-keys + derived-keys.
        Assert.Equal(flatAlarm.TriggerConfiguration, resolvedAlarm.EffectiveTriggerConfiguration);

        // Sanity: the derived config came through unchanged; the base config key
        // ("value") must NOT appear (it would if the merge hook fired incorrectly).
        Assert.Equal("{\"hi\":90,\"lo\":10}", resolvedAlarm.EffectiveTriggerConfiguration);
        Assert.DoesNotContain("\"value\"", resolvedAlarm.EffectiveTriggerConfiguration ?? "");
    }

    // ── I2: staleness must flag drift on the fields that actually matter, not
    //    just one scalar per type. ──

    [Fact]
    public void Resolve_StalenessSummary_TrueWhenBaseAlarmTriggerConfigChanged_PriorityUnchanged()
    {
        // Base alarm threshold changed (TriggerConfiguration), PriorityLevel is
        // unchanged. The derived placeholder still carries the OLD threshold, so
        // staleness must flag it even though the priority scalar matches.
        var baseT = new Template("Base") { Id = 1 };
        baseT.Alarms.Add(new TemplateAlarm("Level")
        {
            Id = 10,
            TemplateId = 1,
            PriorityLevel = 5,
            TriggerType = AlarmTriggerType.HiLo,
            TriggerConfiguration = "{\"hi\":95,\"lo\":10}"
        });

        var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
        derived.Alarms.Add(new TemplateAlarm("Level")
        {
            Id = 20,
            TemplateId = 2,
            PriorityLevel = 5, // priority matches the base — only the threshold drifted
            TriggerType = AlarmTriggerType.HiLo,
            TriggerConfiguration = "{\"hi\":90,\"lo\":10}", // stale threshold
            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_TrueWhenNativeSourceConnectionNameChanged_SourceReferenceUnchanged()
    {
        var baseT = new Template("Base") { Id = 1 };
        baseT.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
        {
            Id = 10,
            TemplateId = 1,
            ConnectionName = "newConn",
            SourceReference = "ns=2;s=Alarm",
            ConditionFilter = null
        });

        var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
        derived.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
        {
            Id = 20,
            TemplateId = 2,
            ConnectionName = "oldConn", // ConnectionName drifted
            SourceReference = "ns=2;s=Alarm", // SourceReference unchanged
            ConditionFilter = null,
            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_TrueWhenNativeSourceConditionFilterChanged_SourceReferenceUnchanged()
    {
        var baseT = new Template("Base") { Id = 1 };
        baseT.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
        {
            Id = 10,
            TemplateId = 1,
            ConnectionName = "conn",
            SourceReference = "ns=2;s=Alarm",
            ConditionFilter = "severity>500"
        });

        var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
        derived.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
        {
            Id = 20,
            TemplateId = 2,
            ConnectionName = "conn",
            SourceReference = "ns=2;s=Alarm", // unchanged
            ConditionFilter = null, // ConditionFilter drifted
            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_FalseWhenAlarmAndNativeSourceInSync()
    {
        // Guard against false-positives from the widened comparison: when the
        // stored placeholders match the resolved (merged) values, nothing is stale.
        var baseT = new Template("Base") { Id = 1 };
        baseT.Alarms.Add(new TemplateAlarm("Level")
        {
            Id = 10,
            TemplateId = 1,
            PriorityLevel = 5,
            TriggerType = AlarmTriggerType.HiLo,
            TriggerConfiguration = "{\"hi\":90,\"lo\":10}"
        });
        baseT.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
        {
            Id = 11,
            TemplateId = 1,
            ConnectionName = "conn",
            SourceReference = "ns=2;s=Alarm",
            ConditionFilter = "severity>500"
        });

        var derived = new Template("Derived") { Id = 2, ParentTemplateId = 1 };
        derived.Alarms.Add(new TemplateAlarm("Level")
        {
            Id = 20,
            TemplateId = 2,
            PriorityLevel = 5,
            TriggerType = AlarmTriggerType.HiLo,
            TriggerConfiguration = "{\"hi\":90,\"lo\":10}", // in sync
            IsInherited = true
        });
        derived.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
        {
            Id = 21,
            TemplateId = 2,
            ConnectionName = "conn",
            SourceReference = "ns=2;s=Alarm",
            ConditionFilter = "severity>500", // in sync
            IsInherited = true
        });

        var resolved = TemplateInheritanceResolver.Resolve(2, new[] { baseT, derived });

        Assert.False(resolved.Staleness.IsStale);
        Assert.Equal(0, resolved.Staleness.DifferingMemberCount);
    }
}
