using System.Net.Sockets;
using System.Text;
using System.Text.Json;
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.IntegrationTests;
///
/// Fixture for the focas-mock simulator. Probes the Docker mock at
/// collection init; if reachable, exposes helpers that drive the mock's
/// admin surface (mock_load_profile, mock_patch,
/// mock_reset, mock_schedule_alarms) so tests can seed
/// deterministic state before exercising the managed driver.
///
///
/// Single skip gate: is non-null when the
/// localhost:8193 TCP probe fails. Tests call
/// Assert.Skip.
///
public sealed class FocasSimFixture : IAsyncDisposable
{
private const string EndpointEnvVar = "OTOPCUA_FOCAS_SIM_ENDPOINT";
private const string ProfileEnvVar = "OTOPCUA_FOCAS_SIM_PROFILE";
private const string DefaultHost = "localhost";
private const int DefaultPort = 8193;
public string Host { get; }
public int Port { get; }
/// focas-mock profile stem the fixture should load (e.g. fwlib30i64,
/// ThirtyOne_i — both resolve via the mock's alias table). Null when unset.
public string? ExpectedProfile { get; }
/// When the maps to a concrete
/// , this is it. Null otherwise.
public FocasCncSeries? ExpectedSeries { get; }
/// Non-null when the mock probe failed — tests skip with this reason.
public string? SkipReason { get; }
public FocasSimFixture()
{
var endpoint = Environment.GetEnvironmentVariable(EndpointEnvVar) ?? $"{DefaultHost}:{DefaultPort}";
(Host, Port) = ParseEndpoint(endpoint);
ExpectedProfile = Environment.GetEnvironmentVariable(ProfileEnvVar);
ExpectedSeries = ParseSeries(ExpectedProfile);
try
{
using var client = new TcpClient(AddressFamily.InterNetwork);
var addresses = System.Net.Dns.GetHostAddresses(Host);
var ip = addresses.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork)
?? System.Net.IPAddress.Loopback;
var task = client.ConnectAsync(ip, Port);
if (!task.Wait(TimeSpan.FromSeconds(2)) || !client.Connected)
{
SkipReason = $"focas-mock at {Host}:{Port} did not accept a TCP connection within 2s. " +
$"Start it (`docker compose -f Docker/docker-compose.yml up -d`) " +
$"or override {EndpointEnvVar}.";
}
}
catch (Exception ex)
{
SkipReason = $"focas-mock at {Host}:{Port} unreachable: {ex.GetType().Name}: {ex.Message}. " +
$"Start it or override {EndpointEnvVar}.";
}
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
// ---- Admin API helpers ----
///
/// Load a focas-mock profile. Accepts either the raw DLL-stem name
/// (fwlib30i64) or the OtOpcUa-style alias (ThirtyOne_i);
/// focas-mock's PROFILE_ALIASES resolves both.
///
public Task LoadProfileAsync(string profileName, CancellationToken ct = default) =>
SendAdminAsync("mock_load_profile", new { profile = profileName }, ct);
/// Deep-merge into the mock's current state.
public Task PatchStateAsync(object state, CancellationToken ct = default) =>
SendAdminAsync("mock_patch", new { state }, ct);
/// Reset the mock to the selected profile's default state.
public Task ResetAsync(CancellationToken ct = default) =>
SendAdminAsync("mock_reset", new { }, ct);
/// Install a time-scheduled alarm raise / clear sequence.
public Task ScheduleAlarmsAsync(IEnumerable