using System.Text.Json;
using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Fleet;
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.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
///
/// The failure path the Direct-apply artifact read's explicit 30 s command timeout relies on: a
/// read that THROWS is "no answer", so the apply fails and the revision does not advance.
///
///
///
/// A pin, not red-first. The behaviour predates this class — LoadArtifactBlob has
/// always caught its own exceptions and returned , which
/// HandleArtifactForApply turns into a Failed ACK. What was missing was a test saying so, and
/// a timed-out command is exactly this shape (a SqlException out of the EF read), so the
/// timeout's whole safety argument rests on it: the node keeps serving last-known-good and the same
/// revision can be dispatched again (the #485/#486 family).
///
///
/// Sibling coverage reaches the same verdict through EMPTY bytes rather than a throw
/// (DriverHostActorUnreadableArtifactTests), which is the missing-row shape, not the
/// read-failed shape — the two arrive at ArtifactForApply by different routes.
///
///
public sealed class DriverHostActorDirectApplyReadFailureTests : RuntimeActorTestBase
{
private static readonly NodeId TestNode = NodeId.Parse("driver-test");
private static readonly RevisionHash RevA = RevisionHash.Parse(new string('a', 64));
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);
[Fact]
public void A_direct_artifact_read_that_throws_fails_the_apply_and_leaves_the_revision_alone()
{
var inner = NewInMemoryDbFactory();
var db = new FailingReadConfigDbFactory(inner);
var deploymentId = SeedDeployment(inner, RevA);
var coordinator = CreateTestProbe();
var actor = Sys.ActorOf(DriverHostActor.Props(
db, TestNode, coordinator.Ref,
localRoles: new HashSet { "driver" }));
// Barrier: an answered GetDiagnostics proves PreStart (and its bootstrap ConfigDb read) is done,
// so the allowance below counts only the apply's own contexts.
actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
coordinator.ExpectMsg(Timeout);
// Let ONE more context through — the apply's Applying-row upsert — and fail the NEXT one, which is
// the artifact read. Both misconfigurations fail LOUDLY rather than passing vacuously: too small
// fails the upsert instead (which swallows its own exceptions), so the read succeeds and the ACK is
// Applied; too large never fails the read at all, same Applied ACK.
db.FailAfter(allowThrough: 1);
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
var ack = coordinator.ExpectMsg(Timeout);
ack.Outcome.ShouldBe(ApplyAckOutcome.Failed);
ack.FailureReason.ShouldNotBeNullOrWhiteSpace();
db.Throws.ShouldBe(1); // the failure came from the read, not from somewhere incidental
// Never applied ⇒ never claimed: the node stays on what it was already serving.
actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
coordinator.ExpectMsg(Timeout).CurrentRevision.ShouldNotBe(RevA);
// …and because the revision did not advance, re-dispatching THE SAME revision against a healthy DB
// is not waved through by the "already at this rev" short-circuit — it actually applies.
actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
coordinator.ExpectMsg(Timeout).Outcome.ShouldBe(ApplyAckOutcome.Applied);
actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
coordinator.ExpectMsg(Timeout).CurrentRevision.ShouldBe(RevA);
}
/// Seeds a sealed, parseable deployment and returns its id.
private static DeploymentId SeedDeployment(IDbContextFactory db, RevisionHash rev)
{
var artifact = JsonSerializer.SerializeToUtf8Bytes(new { DriverInstances = Array.Empty