using Akka.Actor;
using Akka.Event;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;

namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;

/// <summary>
///     Covers WS-4b: <see cref="DriverInstanceActor"/> subscribes a driver's native
///     <see cref="IAlarmSource.OnAlarmEvent"/> (when the driver is an <see cref="IAlarmSource"/>)
///     and forwards every transition to its parent as
///     <see cref="DriverInstanceActor.AttributeAlarmPublished"/> — mirroring the OnDataChange →
///     <see cref="DriverInstanceActor.AttributeValuePublished"/> forward. The driver fires
///     <c>OnAlarmEvent</c> on its OWN thread; the actor marshals it onto the actor thread via Self.
/// </summary>
public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase
{
    /// <summary>
    ///     Driving an <see cref="IAlarmSource"/> driver to Connected then raising a native alarm forwards
    ///     it to the parent as <see cref="DriverInstanceActor.AttributeAlarmPublished"/> carrying the
    ///     driver-instance id + the original <see cref="AlarmEventArgs"/> (SourceNodeId preserved).
    /// </summary>
    [Fact]
    public async Task Native_alarm_is_forwarded_to_parent_as_AttributeAlarmPublished()
    {
        var driver = new AlarmSourceStubDriver();
        var parent = CreateTestProbe();
        // #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
        // disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
        parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
        var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));

        actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
        AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));
        // The alarm handler is attached after Become(Connected); wait for it to be wired before raising.
        AwaitCondition(() => driver.AlarmSubscriberCount == 1, TimeSpan.FromSeconds(2));
        // Also establish a data-change subscription so the OnDataChange path is wired (the alarm attach is
        // independent of Subscribe — this proves both event paths coexist on one driver).
        await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
            new DriverInstanceActor.Subscribe(new[] { "tag-z" }, TimeSpan.FromMilliseconds(100)),
            TimeSpan.FromSeconds(3));

        driver.RaiseAlarm(new AlarmEventArgs(
            new StubAlarmHandle(),
            SourceNodeId: "src-node-7",
            ConditionId: "cond-1",
            AlarmType: "AnalogLimitAlarm.HiHi",
            Message: "level too high",
            Severity: AlarmSeverity.High,
            SourceTimestampUtc: DateTime.UtcNow,
            Kind: AlarmTransitionKind.Raise));

        var published = parent.ExpectMsg<DriverInstanceActor.AttributeAlarmPublished>(TimeSpan.FromSeconds(2));
        published.DriverInstanceId.ShouldBe(driver.DriverInstanceId);
        published.Args.SourceNodeId.ShouldBe("src-node-7");
        published.Args.ConditionId.ShouldBe("cond-1");
        published.Args.Kind.ShouldBe(AlarmTransitionKind.Raise);

        // The same driver's OnDataChange still flows independently — alarm + value events coexist.
        driver.FireDataChange("tag-z", value: 9.0, statusCode: 0u);
        parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>(TimeSpan.FromSeconds(2))
            .FullReference.ShouldBe("tag-z");
    }

    /// <summary>
    ///     The native-alarm path is gated at the driver: an <see cref="IAlarmSource"/> suppresses
    ///     <c>OnAlarmEvent</c> until at least one alarm subscription exists (e.g. GalaxyDriver gates its
    ///     central feed). When the host pushes a <see cref="DriverInstanceActor.SetDesiredSubscriptions"/>
    ///     carrying native-alarm refs to a <c>Connected</c> driver — the live deploy path — the actor must
    ///     call <see cref="IAlarmSource.SubscribeAlarmsAsync"/> with those refs to un-gate the feed, and must
    ///     NOT re-subscribe when the same set is redeployed (the feed is already un-gated).
    /// </summary>
    [Fact]
    public async Task Alarm_subscription_is_established_when_alarm_refs_are_pushed_while_connected()
    {
        var driver = new AlarmSourceStubDriver();
        var parent = CreateTestProbe();
        // #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
        // disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
        parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
        var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));

        actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
        AwaitCondition(() => driver.AlarmSubscriberCount == 1, TimeSpan.FromSeconds(2)); // reached Connected
        driver.SubscribeAlarmsCallCount.ShouldBe(0, "no alarm subscription before any alarm refs are pushed");

        // A deploy delivers the desired set with a native-alarm ref while the driver is already Connected.
        actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(
            Array.Empty<string>(), TimeSpan.FromMilliseconds(100), new[] { "Temp.HiHi" }));

        AwaitCondition(() => driver.SubscribeAlarmsCallCount == 1, TimeSpan.FromSeconds(2));
        driver.LastAlarmRefs.ShouldBe(new[] { "Temp.HiHi" });

        // Redeploying the same alarm set must NOT re-subscribe (idempotent — already un-gated). Round-trip a
        // data Subscribe to flush the mailbox so the second SetDesiredSubscriptions is fully processed first.
        actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(
            Array.Empty<string>(), TimeSpan.FromMilliseconds(100), new[] { "Temp.HiHi" }));
        await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
            new DriverInstanceActor.Subscribe(new[] { "tag-z" }, TimeSpan.FromMilliseconds(100)),
            TimeSpan.FromSeconds(3));
        driver.SubscribeAlarmsCallCount.ShouldBe(1, "an already-established alarm subscription is not re-issued");
    }

    /// <summary>
    ///     A driver that is NOT an <see cref="IAlarmSource"/> (only <see cref="ISubscribable"/>) connects
    ///     and serves data changes normally — <c>AttachAlarmSource</c> is a safe no-op (no crash) and no
    ///     <see cref="DriverInstanceActor.AttributeAlarmPublished"/> is ever produced.
    /// </summary>
    [Fact]
    public async Task Non_alarm_source_driver_serves_data_changes_and_never_publishes_alarms()
    {
        var driver = new SubscribableOnlyStubDriver();
        var parent = CreateTestProbe();
        // #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
        // disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
        parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
        var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));

        actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
        AwaitCondition(() => driver.InitializeCount > 0, TimeSpan.FromSeconds(2));

        await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
            new DriverInstanceActor.Subscribe(new[] { "tag-a" }, TimeSpan.FromMilliseconds(100)),
            TimeSpan.FromSeconds(3));

        driver.FireDataChange("tag-a", value: 1.5, statusCode: 0u);

        // Data-change forwarding still works (no crash on AttachAlarmSource for a non-IAlarmSource driver).
        var dc = parent.ExpectMsg<DriverInstanceActor.AttributeValuePublished>(TimeSpan.FromSeconds(2));
        dc.FullReference.ShouldBe("tag-a");
        // An AttributeAlarmPublished can never be produced for a non-IAlarmSource driver.
        parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
    }

    /// <summary>
    ///     A native alarm transition that races in while the actor is in <c>Reconnecting</c> is silently
    ///     dropped (debug log only) — it NEVER reaches the parent as
    ///     <see cref="DriverInstanceActor.AttributeAlarmPublished"/> AND is NOT dead-lettered (the
    ///     <c>Reconnecting</c> state's explicit <c>Receive&lt;NativeAlarmRaised&gt;</c> handler consumes and
    ///     discards it, so it never becomes unhandled). The feed re-delivers active alarms once the actor
    ///     re-enters <c>Connected</c>, so dropping here is safe.
    ///     (<see cref="DriverInstanceActor"/> line ~345: the <c>Reconnecting</c> state's
    ///     <c>Receive&lt;NativeAlarmRaised&gt;</c> logs a debug message and discards.)
    ///
    ///     <para>
    ///         To reproduce the race deterministically: a <c>ForceReconnect</c> is enqueued first, then
    ///         the driver fires an alarm while its <c>OnAlarmEvent</c> handler is still attached (the actor
    ///         hasn't yet processed <c>ForceReconnect</c>). The handler's <c>self.Tell(NativeAlarmRaised)</c>
    ///         lands second in the mailbox. The actor then processes <c>ForceReconnect</c> → Reconnecting
    ///         (detaches handler), then processes the queued <c>NativeAlarmRaised</c> in Reconnecting
    ///         → drops it. The default 10 s reconnect interval ensures no retry fires during the check.
    ///     </para>
    ///
    ///     <para>
    ///         Both properties are actively asserted: (a) the parent probe receives no
    ///         <see cref="DriverInstanceActor.AttributeAlarmPublished"/>; (b) a dead-letter probe subscribed
    ///         to <see cref="AllDeadLetters"/> on the <see cref="ActorSystem.EventStream"/> also receives
    ///         nothing — proving the drop handler is present (removing it would cause a dead-letter and fail
    ///         this assertion). <c>NativeAlarmRaised</c> is private to the actor, so the dead-letter probe
    ///         subscribes to the unfiltered <see cref="AllDeadLetters"/> channel; this is safe because
    ///         exactly one message is injected and the reconnect timer (10 s) cannot fire in the window.
    ///     </para>
    /// </summary>
    [Fact]
    public void Native_alarm_during_reconnect_is_dropped_not_forwarded()
    {
        // Subscribe a dead-letter probe BEFORE injecting the alarm so we don't miss any early publish.
        // NativeAlarmRaised is private, so we subscribe to the unfiltered AllDeadLetters channel.
        // Only one message is injected and the 10 s reconnect timer can't fire in this window, so
        // a plain "no dead letters at all" assertion is safe and non-flaky.
        var deadLetters = CreateTestProbe();
        Sys.EventStream.Subscribe(deadLetters.Ref, typeof(AllDeadLetters));

        // Long reconnect interval (default 10 s) so the retry doesn't fire during the assertion window.
        var driver = new AlarmSourceStubDriver();
        var parent = CreateTestProbe();
        // #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
        // disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
        parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
        var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver));

        // Drive to Connected; confirm the alarm handler is attached.
        actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
        AwaitCondition(() => driver.AlarmSubscriberCount == 1, TimeSpan.FromSeconds(2));

        // Enqueue ForceReconnect FIRST — the actor hasn't processed it yet, so the handler is
        // still wired. The test thread then immediately fires the alarm on the driver; the handler's
        // self.Tell(NativeAlarmRaised) lands SECOND in the mailbox.
        actor.Tell(new DriverInstanceActor.ForceReconnect());
        driver.RaiseAlarm(new AlarmEventArgs(
            new StubAlarmHandle(),
            SourceNodeId: "src-node-reconnect",
            ConditionId: "cond-reconnect",
            AlarmType: "T",
            Message: "during-reconnect raise",
            Severity: AlarmSeverity.High,
            SourceTimestampUtc: DateTime.UtcNow,
            Kind: AlarmTransitionKind.Raise));

        // (a) The parent must NOT receive AttributeAlarmPublished from that alarm.
        // The actor processes: (1) ForceReconnect → Reconnecting (handler detached);
        //                      (2) NativeAlarmRaised → dropped (debug log, no forward).
        // Wait generously — the default reconnect interval of 10 s means no retry fires here.
        parent.ExpectNoMsg(TimeSpan.FromMilliseconds(500));

        // (b) The alarm must also NOT have dead-lettered — the Reconnecting state's explicit
        // Receive<NativeAlarmRaised> handler must have consumed it.  If that handler were removed the
        // message would become unhandled → AllDeadLetters, and this assertion would catch the regression.
        deadLetters.ExpectNoMsg(TimeSpan.FromMilliseconds(200));

        // The actor must still be alive — Watch + no Terminated.
        Watch(actor);
        parent.ExpectNoMsg(TimeSpan.FromMilliseconds(100));
    }

    /// <summary>
    ///     After a full reconnect cycle (Connected → Reconnecting → Connected), a single raised alarm still
    ///     yields EXACTLY ONE <see cref="DriverInstanceActor.AttributeAlarmPublished"/> — the
    ///     <c>_alarmEventHandler is not null</c> guard in <c>AttachAlarmSource</c> plus the detach on the
    ///     Connected → Reconnecting transition prevent a double-attach (which would publish twice).
    /// </summary>
    [Fact]
    public void Reconnect_does_not_double_attach_alarm_handler()
    {
        var driver = new AlarmSourceStubDriver();
        var parent = CreateTestProbe();
        // #477: DriverInstanceActor now Tells the parent ConnectivityChanged on each connect/
        // disconnect; these tests assert on the data/alarm/discovery forwards, so ignore it.
        parent.IgnoreMessages(m => m is DriverInstanceActor.ConnectivityChanged);
        var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));

        actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
        AwaitCondition(() => driver.AlarmSubscriberCount == 1, TimeSpan.FromSeconds(2));

        // Force Connected → Reconnecting (detaches) → Connected (re-attaches). InitializeCount climbs on
        // the retry; the alarm handler count must settle back to exactly one (no leaked extra handler).
        var initBefore = driver.InitializeCount;
        actor.Tell(new DriverInstanceActor.DisconnectObserved("backend blip"));
        AwaitCondition(() => driver.InitializeCount > initBefore, TimeSpan.FromSeconds(3));
        AwaitCondition(() => driver.AlarmSubscriberCount == 1, TimeSpan.FromSeconds(3));

        driver.RaiseAlarm(new AlarmEventArgs(
            new StubAlarmHandle(),
            SourceNodeId: "src-node-1",
            ConditionId: "cond-x",
            AlarmType: "T",
            Message: "m",
            Severity: AlarmSeverity.Low,
            SourceTimestampUtc: DateTime.UtcNow,
            Kind: AlarmTransitionKind.Raise));

        // Exactly one forward — a double-attach would surface as a second AttributeAlarmPublished.
        parent.ExpectMsg<DriverInstanceActor.AttributeAlarmPublished>(TimeSpan.FromSeconds(2))
            .Args.SourceNodeId.ShouldBe("src-node-1");
        parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
    }

    /// <summary>#477 Layer 2 — the actor announces connectivity to its parent (the host): Connected on
    /// reaching the Connected state, then Connected=false on a <see cref="DriverInstanceActor.DisconnectObserved"/>,
    /// then Connected=true again on the reconnect. This is the signal the host uses to annotate the driver's
    /// native alarm conditions' Quality (comms lost → Bad, restored → Good).</summary>
    [Fact]
    public void ConnectivityChanged_is_announced_to_parent_on_connect_disconnect_and_reconnect()
    {
        var driver = new AlarmSourceStubDriver();
        var parent = CreateTestProbe(); // deliberately does NOT ignore ConnectivityChanged — it's the subject.
        var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));

        actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));

        // Initial connect → Connected=true.
        parent.FishForMessage<DriverInstanceActor.ConnectivityChanged>(m => true, TimeSpan.FromSeconds(3))
            .Connected.ShouldBeTrue();

        // Disconnect → Connected=false (this is what drives the conditions Bad).
        actor.Tell(new DriverInstanceActor.DisconnectObserved("backend blip"));
        parent.FishForMessage<DriverInstanceActor.ConnectivityChanged>(m => true, TimeSpan.FromSeconds(3))
            .Connected.ShouldBeFalse();

        // Reconnect → Connected=true again (drives them back Good).
        parent.FishForMessage<DriverInstanceActor.ConnectivityChanged>(m => true, TimeSpan.FromSeconds(3))
            .Connected.ShouldBeTrue();
    }

    // --- stub drivers ----------------------------------------------------------------------------

    private class StubDriver : IDriver
    {
        /// <summary>Gets the number of times initialization was called.</summary>
        public int InitializeCount;

        /// <summary>Gets the driver instance ID.</summary>
        public string DriverInstanceId => "alarm-stub-driver-1";
        /// <summary>Gets the driver type.</summary>
        public string DriverType => "Stub";

        /// <summary>Initializes the driver.</summary>
        public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
        {
            Interlocked.Increment(ref InitializeCount);
            return Task.CompletedTask;
        }

        /// <summary>Reinitializes the driver.</summary>
        public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) =>
            Task.CompletedTask;

        /// <summary>Shuts down the driver.</summary>
        public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask;
        /// <summary>Gets the health status of the driver.</summary>
        public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
        /// <summary>Gets the memory footprint of the driver.</summary>
        public long GetMemoryFootprint() => 0;
        /// <summary>Flushes optional caches in the driver.</summary>
        public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    }

    /// <summary>A driver that implements <see cref="ISubscribable"/> + <see cref="IAlarmSource"/> and lets the
    /// test raise <see cref="IAlarmSource.OnAlarmEvent"/> on demand (the driver fires it on its own thread in
    /// production; here the test thread stands in for that).</summary>
    private sealed class AlarmSourceStubDriver : StubDriver, ISubscribable, IAlarmSource
    {
        private readonly StubHandle _subHandle = new();

        /// <summary>Occurs when data changes.</summary>
        public event EventHandler<DataChangeEventArgs>? OnDataChange;
        /// <summary>Server-pushed alarm transition.</summary>
        public event EventHandler<AlarmEventArgs>? OnAlarmEvent;

        /// <summary>Number of live subscribers on <see cref="OnAlarmEvent"/> — proves attach/detach.</summary>
        public int AlarmSubscriberCount => OnAlarmEvent?.GetInvocationList().Length ?? 0;

        /// <summary>Subscribes to the specified full references.</summary>
        public Task<ISubscriptionHandle> SubscribeAsync(
            IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) =>
            Task.FromResult<ISubscriptionHandle>(_subHandle);

        /// <summary>Unsubscribes from the specified subscription handle.</summary>
        public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) =>
            Task.CompletedTask;

        private int _subscribeAlarmsCallCount;

        /// <summary>Number of <see cref="SubscribeAlarmsAsync"/> calls — proves the actor established the
        /// native-alarm subscription that un-gates the feed (incremented on the actor dispatcher thread).</summary>
        public int SubscribeAlarmsCallCount => Volatile.Read(ref _subscribeAlarmsCallCount);

        /// <summary>The node set handed to the most recent <see cref="SubscribeAlarmsAsync"/> call.</summary>
        public IReadOnlyList<string>? LastAlarmRefs { get; private set; }

        /// <summary>Subscribes to alarm events for the specified node set.</summary>
        public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
            IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
        {
            LastAlarmRefs = sourceNodeIds;
            Interlocked.Increment(ref _subscribeAlarmsCallCount);
            return Task.FromResult<IAlarmSubscriptionHandle>(new StubAlarmHandle());
        }

        /// <summary>Cancels an alarm subscription.</summary>
        public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) =>
            Task.CompletedTask;

        /// <summary>Acknowledges a batch of alarms.</summary>
        public Task AcknowledgeAsync(
            IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken) =>
            Task.CompletedTask;

        /// <summary>Fires <see cref="OnAlarmEvent"/> — stands in for the driver's native feed.</summary>
        public void RaiseAlarm(AlarmEventArgs args) => OnAlarmEvent?.Invoke(this, args);

        /// <summary>Fires a data change event (keeps OnDataChange exercised; the actor wires both events).</summary>
        public void FireDataChange(string fullRef, object? value, uint statusCode)
        {
            var snapshot = new DataValueSnapshot(value, statusCode, DateTime.UtcNow, DateTime.UtcNow);
            OnDataChange?.Invoke(this, new DataChangeEventArgs(_subHandle, fullRef, snapshot));
        }

        private sealed class StubHandle : ISubscriptionHandle
        {
            /// <summary>Gets the diagnostic ID of the subscription.</summary>
            public string DiagnosticId => "stub-sub";
        }
    }

    /// <summary>A driver that is <see cref="ISubscribable"/> but NOT <see cref="IAlarmSource"/> — proves
    /// <c>AttachAlarmSource</c> is a no-op (no crash) and no alarm forward ever happens.</summary>
    private sealed class SubscribableOnlyStubDriver : StubDriver, ISubscribable
    {
        private readonly StubHandle _subHandle = new();

        /// <summary>Occurs when data changes.</summary>
        public event EventHandler<DataChangeEventArgs>? OnDataChange;

        /// <summary>Subscribes to the specified full references.</summary>
        public Task<ISubscriptionHandle> SubscribeAsync(
            IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) =>
            Task.FromResult<ISubscriptionHandle>(_subHandle);

        /// <summary>Unsubscribes from the specified subscription handle.</summary>
        public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) =>
            Task.CompletedTask;

        /// <summary>Fires a data change event with the specified parameters.</summary>
        public void FireDataChange(string fullRef, object? value, uint statusCode)
        {
            var snapshot = new DataValueSnapshot(value, statusCode, DateTime.UtcNow, DateTime.UtcNow);
            OnDataChange?.Invoke(this, new DataChangeEventArgs(_subHandle, fullRef, snapshot));
        }

        private sealed class StubHandle : ISubscriptionHandle
        {
            /// <summary>Gets the diagnostic ID of the subscription.</summary>
            public string DiagnosticId => "stub-sub";
        }
    }

    /// <summary>Minimal <see cref="IAlarmSubscriptionHandle"/> for building <see cref="AlarmEventArgs"/>.</summary>
    private sealed class StubAlarmHandle : IAlarmSubscriptionHandle
    {
        /// <summary>Gets the diagnostic ID of the alarm subscription.</summary>
        public string DiagnosticId => "stub-alarm-sub";
    }
}
