using System.Diagnostics.Metrics;
using System.Text.Json;
using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Observability;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.OpcUa;
///
/// archreview 01/S-1: when the applier reports a degraded apply (a rebuild that threw, or per-node
/// materialise failures), must surface it — increment the
/// otopcua.opcua.apply.failed meter and log at Error — instead of the optimistic Info line. The
/// happy path must stay Info-only (zero increments).
///
public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase
{
/// A rebuild whose sink throws increments otopcua.opcua.apply.failed (kind=rebuild).
[Fact]
public void Rebuild_when_sink_throws_increments_apply_failed_meter()
{
using var recorder = new MeterRecorder("otopcua.opcua.apply.failed");
var db = NewInMemoryDbFactory();
var sink = new ThrowOnRebuildSink();
var applier = new AddressSpaceApplier(sink, NullLogger.Instance);
SeedEquipmentDeployment(db, "eq-1");
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink, dbFactory: db, applier: applier));
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
AwaitAssert(() =>
{
recorder.Total.ShouldBeGreaterThanOrEqualTo(1);
recorder.WithTag("kind", "rebuild").ShouldBeGreaterThanOrEqualTo(1);
}, duration: TimeSpan.FromSeconds(2));
}
/// A clean rebuild does NOT increment the apply-failed meter (Info-only happy path).
[Fact]
public void Rebuild_happy_path_does_not_increment_apply_failed_meter()
{
using var recorder = new MeterRecorder("otopcua.opcua.apply.failed");
var db = NewInMemoryDbFactory();
var sink = new NoopSink();
var applier = new AddressSpaceApplier(sink, NullLogger.Instance);
SeedEquipmentDeployment(db, "eq-1");
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(sink: sink, dbFactory: db, applier: applier));
actor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
// Give the actor time to process, then confirm the failed meter never fired.
Thread.Sleep(400);
recorder.Total.ShouldBe(0);
}
private void AwaitAssert(Action assertion, TimeSpan duration)
{
var deadline = DateTime.UtcNow + duration;
Exception? last = null;
while (DateTime.UtcNow < deadline)
{
try { assertion(); return; }
catch (Exception ex) { last = ex; Thread.Sleep(25); }
}
if (last is not null) throw last;
}
private static void SeedEquipmentDeployment(IDbContextFactory dbFactory, params string[] equipmentIds)
{
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
{
Equipment = equipmentIds.Select(id => new
{
EquipmentId = id,
MachineCode = id.ToUpperInvariant(),
UnsLineId = "line-1",
Name = id,
}).ToArray(),
DriverInstances = Array.Empty