using Akka.Actor;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
///
/// Gitea #662 — a native-alarm feed must survive a deploy that does NOT respawn its driver, end to end
/// through the real and its real
/// child.
///
///
/// Found live on the docker-dev rig: a deploy that only disabled ANOTHER driver re-pushed the desired set to
/// an unchanged OpcUaClient driver, whose data re-subscribe tore down the alarm event handler too. The
/// condition then froze while upstream kept emitting, and only a respawn brought it back. Every existing
/// end-to-end alarm test drives the driver directly (the host's driver is Enabled=false), so none of
/// them could see the instance actor drop the feed.
///
public sealed class NativeAlarmFeedSurvivesDeployTests : RuntimeActorTestBase
{
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
private static readonly RevisionHash RevB = RevisionHash.Parse(new string('b', 64));
private static readonly NodeId TestNode = NodeId.Parse("node-1:4053");
private const string DriverId = "DI-1";
private const string AlarmXPath = "Plant/Modbus/dev1/alarmX";
private const string AlarmConfig = """{"alarm":{"alarmType":"AlarmCondition","severity":500}}""";
private static readonly (string Driver, string DriverName, string Device, string Tag, string DataType, bool Writable)[]
RawTags =
[
(DriverId, "Modbus", "dev1", "speed", "Float", false),
(DriverId, "Modbus", "dev1", "alarmX", "Float", false),
];
private static readonly Dictionary TagConfigs = new() { ["alarmX"] = AlarmConfig };
[Fact]
public void An_upstream_alarm_still_reaches_its_condition_after_a_non_respawning_deploy()
{
var db = NewInMemoryDbFactory();
var factory = new RecordingSubscribableDriverFactory("Modbus");
var revA = V3ArtifactSeeder.SeedV3Deployment(
db, RevA, RawTags, Array.Empty<(string, string, string, string, string?)>(),
enabled: true, tagConfigByTag: TagConfigs);
var (host, publish, _) = V3ArtifactSeeder.SpawnHostAndApply(this, db, TestNode, revA, RevA, factory);
RecordingSubscribableDriver driver = null!;
AwaitAssert(() =>
{
factory.Created.Count.ShouldBe(1);
driver = factory.Created[0];
driver.SubscribeCount.ShouldBeGreaterThanOrEqualTo(1);
driver.LastSubscribedAlarmRefs.ShouldBe(new[] { AlarmXPath });
driver.AlarmSubscriberCount.ShouldBe(1);
}, duration: PresenceBudget);
var dataSubscribesBefore = driver.SubscribeCount;
// Revision B only ADDS another driver: DI-1's merged DriverConfig is byte-identical, so it is kept,
// and the bulk push re-sends its (unchanged) desired set — which re-subscribes its data.
var revB = V3ArtifactSeeder.SeedV3Deployment(
db, RevB, RawTags, Array.Empty<(string, string, string, string, string?)>(),
enabled: true, tagConfigByTag: TagConfigs, taglessDrivers: ["DI-2"]);
host.Tell(new DispatchDeployment(revB, RevB, CorrelationId.NewId()));
AwaitAssert(() =>
{
driver.SubscribeCount.ShouldBeGreaterThan(dataSubscribesBefore,
"precondition: the deploy must re-subscribe DI-1's data, or this test exercises nothing");
}, duration: PresenceBudget);
driver.InitializeCount.ShouldBe(1, "precondition: DI-1 must NOT have been respawned or re-initialised");
driver.ShutdownCount.ShouldBe(0);
factory.Created[0].ShouldBeSameAs(driver);
driver.AlarmSubscriberCount.ShouldBe(1, "the alarm event handler must survive the data re-subscribe");
driver.FireAlarm(AlarmXPath, AlarmTransitionKind.Raise);
var update = publish.FishForMessage(
m => m.AlarmNodeId == AlarmXPath && m.State.Active, PresenceBudget);
update.State.Acknowledged.ShouldBeFalse();
}
}