using System.Collections.Concurrent;
using System.Text.Json;
using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
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.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;
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;
///
/// H5d (DI seam): pins that accepts an
/// and threads it through to the REAL spawned
/// (the virtualTagHostOverride is deliberately NOT used,
/// so the production spawn path that wires historyWriter is exercised).
///
///
/// This is a construction/threading smoke test, not a record-behaviour test. The record path —
/// "a Historize=true result is recorded with the IHistoryWriter under the folder-scoped NodeId" —
/// is already covered end-to-end against the host directly by
/// VirtualTagHostActorTests.Historized_vtag_result_is_recorded_with_the_history_writer (H5c).
/// Driving a real spawned child to emit a synthetic
/// from inside would require reaching the auto-named grandchild and a
/// live evaluator/mux feed — heavy and brittle for no extra coverage. So here we assert the writer
/// threads through cleanly: a real apply carrying a Historize=true Equipment VirtualTag is Applied
/// with the real host spawned and the capturing writer wired in, with no construction error.
///
public sealed class DriverHostActorHistoryWriterTests : RuntimeActorTestBase
{
private static readonly NodeId TestNode = NodeId.Parse("driver-hw-test");
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
/// Spawning a real DriverHostActor (no host override, publish actor wired so the real
/// VirtualTagHostActor spawns) with a capturing IHistoryWriter applies a Historize=true VirtualTag
/// deployment cleanly — proving Props/ctor accept the writer and thread it through the real spawn.
[Fact]
public void Props_accepts_history_writer_and_applies_historized_vtag_deployment()
{
var db = NewInMemoryDbFactory();
var deploymentId = SeedDeploymentWithHistorizedVirtualTag(db, RevA);
var coordinator = CreateTestProbe();
// A publish actor MUST be wired for SpawnVirtualTagHost to spawn the real host (its ctor
// requires a non-null sink). No virtualTagHostOverride ⇒ the real VirtualTagHostActor is
// spawned with _historyWriter threaded in.
var publish = CreateTestProbe();
var writer = new CapturingHistoryWriter();
var actor = Sys.ActorOf(DriverHostActor.Props(
db, TestNode, coordinator.Ref,
localRoles: new HashSet { "driver" },
opcUaPublishActor: publish.Ref,
virtualTagEvaluator: NullVirtualTagEvaluator.Instance,
historyWriter: writer));
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
// The deployment Applies — the real VirtualTagHostActor spawned and accepted ApplyVirtualTags
// with the writer threaded through (a construction/threading failure would have faulted PreStart
// or the apply turn). Record behaviour itself is covered by VirtualTagHostActorTests (H5c).
coordinator.ExpectMsg(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
}
private static DeploymentId SeedDeploymentWithHistorizedVirtualTag(
IDbContextFactory db, RevisionHash rev)
{
var artifact = JsonSerializer.SerializeToUtf8Bytes(new
{
Scripts = new[]
{
new
{
ScriptId = "scr-1",
SourceCode = "return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;",
},
},
VirtualTags = new[]
{
new
{
VirtualTagId = "vt-1",
EquipmentId = "eq-1",
Name = "Doubled",
DataType = "Float",
ScriptId = "scr-1",
Historize = true,
},
},
});
var id = DeploymentId.NewId();
using var ctx = db.CreateDbContext();
ctx.Deployments.Add(new Deployment
{
DeploymentId = id.Value,
RevisionHash = rev.Value,
Status = DeploymentStatus.Sealed,
CreatedBy = "test",
SealedAtUtc = DateTime.UtcNow,
ArtifactBlob = artifact,
});
ctx.SaveChanges();
return id;
}
/// Capturing : records every Record call so the threading
/// smoke test can be tightened later if a record assertion becomes feasible here.
private sealed class CapturingHistoryWriter : IHistoryWriter
{
public readonly ConcurrentQueue<(string Path, DataValueSnapshot Value)> Calls = new();
/// Captures the path + snapshot of a Record call.
/// The virtual tag path.
/// The data value snapshot.
public void Record(string path, DataValueSnapshot value) => Calls.Enqueue((path, value));
}
}