using Akka.Actor;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
///
/// E2E integration coverage for the ReconnectDriver command path through
/// .
///
/// The first two tests verify the message round-trip through the
/// AdminOperationsActor singleton against a non-deployed instance id: the command is
/// accepted, persisted as a ConfigEdit audit row, and the reply carries
/// Ok = true with the matching CorrelationId.
///
///
/// goes the full distance: it deploys a real driver (via the opt-in
/// wired into the harness) so the
/// DriverHostActor spawns a managed DriverInstanceActor, then drives the
/// end-to-end reconnect path —
/// ReconnectDriver → AdminOperationsActor → DriverHostActor.HandleReconnectDriver →
/// DriverInstanceActor.ForceReconnect (FSM) → PublishHealthSnapshot → driver-health DPS topic →
/// DriverStatusSignalRBridge → snapshot store / hub push — and asserts the published health
/// transitions Healthy → Reconnecting → Healthy.
///
[Trait("Category", "Integration")]
public sealed class DriverReconnectE2eTests
{
private static CancellationToken Ct => TestContext.Current.CancellationToken;
///
/// Verifies that a message dispatched through
/// returns a
/// with Ok = true and the matching
/// correlation ID, confirming the cluster-singleton round-trip works end-to-end.
///
/// The instance ID used here ("reconnect-e2e-nonexistent") does not correspond
/// to a deployed driver, so no DriverInstanceActor will act on the DPS
/// broadcast — the test is validating the command ingestion and reply path only.
///
[Fact]
public async Task Reconnect_RoundTrip_ReturnsOk()
{
await using var harness = await TwoNodeClusterHarness.StartAsync();
await using var scope = harness.NodeA.Services.CreateAsyncScope();
var client = scope.ServiceProvider.GetRequiredService();
var correlationId = Guid.NewGuid();
var msg = new ReconnectDriver(
ClusterId: "cluster-e2e-test",
DriverInstanceId: "reconnect-e2e-nonexistent",
ActorByUserName: "e2e-test-runner",
CorrelationId: correlationId);
var result = await client.AskAsync(msg, Ct);
result.CorrelationId.ShouldBe(correlationId);
result.Ok.ShouldBeTrue($"ReconnectDriver round-trip failed: {result.Message}");
result.Message.ShouldBeNull();
}
///
/// Verifies that a second for the same instance ID
/// is also accepted (idempotent at the actor layer — the actor simply re-broadcasts
/// to DPS and writes another ConfigEdit row).
///
[Fact]
public async Task Reconnect_IsIdempotent_SecondCallAlsoReturnsOk()
{
await using var harness = await TwoNodeClusterHarness.StartAsync();
await using var scope = harness.NodeA.Services.CreateAsyncScope();
var client = scope.ServiceProvider.GetRequiredService();
const string instanceId = "reconnect-idempotency-test";
var first = new ReconnectDriver("cluster-1", instanceId, "runner", Guid.NewGuid());
var second = new ReconnectDriver("cluster-1", instanceId, "runner", Guid.NewGuid());
var r1 = await client.AskAsync(first, Ct);
var r2 = await client.AskAsync(second, Ct);
r1.Ok.ShouldBeTrue($"First call failed: {r1.Message}");
r2.Ok.ShouldBeTrue($"Second call failed: {r2.Message}");
r1.CorrelationId.ShouldBe(first.CorrelationId);
r2.CorrelationId.ShouldBe(second.CorrelationId);
}
private const string ClusterId = "RECONNECT-E2E";
private const string DriverId = "drv-modbus";
///
/// Full-stack reconnect: deploys a real driver (the in-process
/// ), proves it reaches Healthy on the driver-health DPS
/// topic, simulates a lost connection (), issues
/// a through , and asserts the
/// published health walks Healthy → Reconnecting → Healthy — captured at the
/// hub-push seam. Confirms the operator Reconnect threads
/// the whole cluster path and genuinely re-initialises the driver (InitializeCount ≥ 2).
///
[Fact]
public async Task Reconnect_DeployedDriver_TransitionsThroughReconnectingBackToHealthy()
{
var factory = new FakeReconnectDriverFactory();
await using var harness = await TwoNodeClusterHarness.StartAsync(driverFactory: factory);
var store = harness.NodeA.Services.GetRequiredService();
// Capture every DriverHealthChanged the bridge pushes to the hub (the first SendCoreAsync arg).
var captured = new List();
var captureLock = new object();
var mockClients = new Mock();
var mockClientProxy = new Mock();
mockClients.Setup(c => c.Group(It.IsAny())).Returns(mockClientProxy.Object);
mockClientProxy
.Setup(p => p.SendCoreAsync(It.IsAny(), It.IsAny