using System.Collections.Concurrent;
using Akka.Actor;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Calculation;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
///
/// WP7 register-AND-consume proof: dependency values actually FLOW end-to-end through the REAL
/// → →
/// path (not merely that exists).
/// A source value published to the mux is fed into the calc driver, which computes and publishes a
/// value; and a calc tag reading ANOTHER calc tag's output (calc-of-calc) recomputes when the first
/// tag's output re-enters the mux — the exact re-entry the host's ForwardToMux performs, mirrored
/// here by looping the driver's back into the mux.
///
public sealed class CalculationDependencyFlowTests : RuntimeActorTestBase
{
private static readonly DateTime Ts = new(2026, 7, 16, 10, 0, 0, DateTimeKind.Utc);
private static RawTagEntry CalcTag(string rawPath, string source)
{
var json = $"{{\"scriptId\":\"s-{rawPath}\",\"changeTriggered\":true," +
$"\"scriptSource\":{System.Text.Json.JsonSerializer.Serialize(source)}}}";
return new RawTagEntry(rawPath, json, WriteIdempotent: false);
}
[Fact]
public async Task Source_value_flows_through_mux_and_adapter_into_the_calc_driver_and_propagates_calc_of_calc()
{
// A: out = src/a * 2. B: b = A.out + 1 (calc-of-calc; B depends on A's RawPath).
var driver = new CalculationDriver(
new CalculationDriverOptions
{
RawTags =
[
CalcTag("calc/engine/out", "return (double)ctx.GetTag(\"src/a\").Value * 2.0;"),
CalcTag("calc/engine/b", "return (double)ctx.GetTag(\"calc/engine/out\").Value + 1.0;"),
],
},
"calc-drv");
await driver.InitializeAsync("{}", CancellationToken.None);
// The driver declares interest in both the external source AND the first calc tag's output.
driver.DependencyRefs.OrderBy(r => r).ShouldBe(new[] { "calc/engine/out", "src/a" });
var published = new ConcurrentQueue();
var mux = Sys.ActorOf(DependencyMuxActor.Props(), "dep-mux");
// Loop the driver's outputs back into the mux exactly as DriverHostActor.ForwardToMux does — this
// is what makes calc-of-calc chains work.
driver.OnDataChange += (_, e) =>
{
published.Enqueue(e);
var quality = e.Snapshot.StatusCode == 0u ? OpcUaQuality.Good : OpcUaQuality.Bad;
mux.Tell(new DriverInstanceActor.AttributeValuePublished(
"calc-drv", e.FullReference, e.Snapshot.Value, quality, Ts));
};
// The REAL adapter registers the driver's dependency refs on the mux + forwards changes into it.
Sys.ActorOf(DependencyConsumerMuxAdapter.Props(driver, mux), "dep-adapter");
// Publish the external source into the mux. AwaitAssert re-Tells (idempotent after the first
// compute dedups) until the computed value has flowed all the way through.
AwaitAssert(() =>
{
mux.Tell(new DriverInstanceActor.AttributeValuePublished(
"src-drv", "src/a", 21.0, OpcUaQuality.Good, Ts));
// Leg 1 — dep flow: src/a=21 → A computes 42 and publishes on calc/engine/out.
published.ShouldContain(e => e.FullReference == "calc/engine/out" && Equals(e.Snapshot.Value, 42.0));
// Leg 2 — calc-of-calc: A's 42 re-entered the mux → B computes 43 on calc/engine/b.
published.ShouldContain(e => e.FullReference == "calc/engine/b" && Equals(e.Snapshot.Value, 43.0));
}, TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(100));
}
}