using System.Collections.Concurrent;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
///
/// Opt-in fake the can register
/// so a deployed driver actually reaches Connected (Healthy) in-process — no Docker sim and
/// no real backend. Materialises a for the "Modbus" driver
/// type (any other type returns null, mirroring ).
///
///
/// The DI wiring resolves from the container; the production
/// AddOtOpcUaRuntime() seeds via TryAddSingleton, so a
/// harness that wants live drivers registers this fake first (its registration wins). Every
/// initialize/connect succeeds, so the wrapping DriverInstanceActor walks
/// Connecting → Connected → Healthy and (on ReconnectDriver) drives
/// ForceReconnect → Reconnecting → re-initialize → Connected without any fault injection.
/// Each created driver is recorded in so a test can reach in and flip its
/// health to just before issuing the reconnect command.
///
public sealed class FakeReconnectDriverFactory : IDriverFactory
{
/// The single driver type this fake factory materialises.
public const string FakeDriverType = "Modbus";
///
/// Drivers this factory has created, keyed by driverInstanceId, so a test can retrieve the
/// live instance (e.g. to call before
/// dispatching a reconnect command). Concurrent because the factory is invoked on the spawning
/// actor thread while the test reads on its own thread.
///
public ConcurrentDictionary Created { get; } = new();
///
/// Returns a when is
/// ; otherwise null (the host logs + skips the row). Records the
/// created driver in keyed by .
///
/// The driver type name from the deployed DriverInstance row.
/// The stable driver-instance identifier.
/// The driver configuration as a JSON string (ignored by the fake).
/// A new , or null for an unsupported type.
public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson)
{
if (driverType != FakeDriverType) return null;
var driver = new FakeReconnectDriver(driverInstanceId, driverType);
Created[driverInstanceId] = driver;
return driver;
}
/// Gets the driver-type names this factory can materialise.
public IReadOnlyCollection SupportedTypes { get; } = new[] { FakeDriverType };
}
///
/// A minimal in-process whose connect/initialize path always succeeds, so the
/// wrapping DriverInstanceActor reaches Connected and publishes a
/// snapshot. Reads/writes/subscribes are benign no-ops; this exists
/// only to let a deployed driver walk the Healthy ↔ Reconnecting FSM in-process for E2E tests.
///
/// The driver's reported health is controllable: a test calls
/// to make return (simulating a lost
/// connection — the realistic trigger for an operator Reconnect). The DriverInstanceActor's
/// ForceReconnect handler POLLS right after entering its Reconnecting
/// state, so that snapshot surfaces on the driver-health topic.
/// The subsequent retry calls , which clears the flag back to
/// , so the next snapshot returns to Healthy.
///
public sealed class FakeReconnectDriver : IDriver
{
///
/// Initializes a new .
///
/// The stable logical driver-instance identifier.
/// The driver type name.
public FakeReconnectDriver(string driverInstanceId, string driverType)
{
DriverInstanceId = driverInstanceId;
DriverType = driverType;
}
/// Gets the stable logical ID of this driver instance.
public string DriverInstanceId { get; }
/// Gets the driver type name (e.g. "Modbus").
public string DriverType { get; }
///
/// When true, reports .
/// volatile because the actor polls from its own thread while the
/// test flips this from another via .
///
private volatile bool _reconnecting;
/// Timestamp of the most recent successful initialize; surfaced as the last successful read.
private DateTime _lastSuccess = DateTime.UtcNow;
/// Backing field for ; mutated via .
private int _initializeCount;
///
/// Number of times has been invoked. Read by the test to prove a
/// reconnect genuinely re-initialised the driver through the full cluster path (≥ 2 means the
/// initial connect plus at least one reconnect retry). The actor's retry path runs on a thread-pool
/// thread, so the increment is and the read is .
///
public int InitializeCount => Volatile.Read(ref _initializeCount);
///
/// Marks the driver as having lost its connection so the next poll reports
/// . The test calls this immediately before dispatching the
/// reconnect command, simulating the realistic operator-Reconnect trigger.
///
public void ReportReconnecting() => _reconnecting = true;
///
/// Connect/initialize path — always succeeds (returns a completed task), so the actor self-Tells
/// InitializeSucceeded and becomes Connected. This is the method that makes connect
/// succeed; the FSM's reconnect path re-invokes it and it succeeds again. Increments
/// and clears the reconnecting flag (initialize succeeded → healthy
/// again).
///
/// The driver configuration JSON (ignored).
/// Cancellation token for the operation.
/// A completed task — initialization always succeeds.
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
Interlocked.Increment(ref _initializeCount);
_lastSuccess = DateTime.UtcNow;
_reconnecting = false;
return Task.CompletedTask;
}
/// Applies a config change in place — a no-op that always succeeds.
/// The driver configuration JSON (ignored).
/// Cancellation token for the operation.
/// A completed task — reinitialization always succeeds.
public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
=> Task.CompletedTask;
/// Stops the driver — a no-op that always succeeds.
/// Cancellation token for the operation.
/// A completed task.
public Task ShutdownAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
///
/// Returns a snapshot when the test has flagged a lost
/// connection via ; otherwise a
/// snapshot. The actor polls this on every observable state change, so the published state tracks
/// this flag.
///
/// A reflecting the controllable connection state.
public DriverHealth GetHealth() => _reconnecting
? new DriverHealth(DriverState.Reconnecting, _lastSuccess, null)
: new DriverHealth(DriverState.Healthy, _lastSuccess, null);
/// Returns a zero memory footprint (the fake holds no driver-attributable caches).
/// Always 0.
public long GetMemoryFootprint() => 0;
/// Flushes optional caches — a no-op (the fake holds none).
/// Cancellation token for the operation.
/// A completed task.
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
}