using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using ArchestrA.MxAccess; using ZB.MOM.WW.LmxOpcUa.Host.Domain; namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers { /// /// Fake IMxProxy for testing without the MxAccess COM runtime. /// Simulates connections, subscriptions, data changes, and writes. /// public class FakeMxProxy : IMxProxy { private int _nextHandle = 1; private int _connectionHandle; private bool _registered; /// /// Occurs when the fake proxy publishes a simulated runtime data-change callback to the system under test. /// public event MxDataChangeHandler? OnDataChange; /// /// Occurs when the fake proxy publishes a simulated write-complete callback to the system under test. /// public event MxWriteCompleteHandler? OnWriteComplete; /// /// Gets the item-handle to tag-reference map built by the test as attributes are registered with the fake runtime. /// public ConcurrentDictionary Items { get; } = new ConcurrentDictionary(); /// /// Gets the item handles currently marked as advised so tests can assert subscription behavior. /// public ConcurrentDictionary AdvisedItems { get; } = new ConcurrentDictionary(); /// /// Gets the values written through the fake runtime so write scenarios can assert the final payload. /// public List<(string Address, object Value)> WrittenValues { get; } = new List<(string, object)>(); /// /// Gets a value indicating whether the fake runtime is currently considered registered. /// public bool IsRegistered => _registered; /// /// Gets the number of times the system under test attempted to register with the fake runtime. /// public int RegisterCallCount { get; private set; } /// /// Gets the number of times the system under test attempted to unregister from the fake runtime. /// public int UnregisterCallCount { get; private set; } /// /// Gets or sets a value indicating whether registration should fail to exercise connection-error paths. /// public bool ShouldFailRegister { get; set; } /// /// Gets or sets a value indicating whether writes should fail to exercise runtime write-error paths. /// public bool ShouldFailWrite { get; set; } /// /// Gets or sets a value indicating whether the fake should suppress the write-complete callback for timeout scenarios. /// public bool SkipWriteCompleteCallback { get; set; } /// /// Gets or sets the status code returned in the simulated write-complete callback. /// public int WriteCompleteStatus { get; set; } = 0; // 0 = success /// /// Simulates the MXAccess registration handshake and returns a synthetic connection handle. /// /// The client name supplied by the code under test. /// A synthetic connection handle for subsequent fake operations. public int Register(string clientName) { RegisterCallCount++; if (ShouldFailRegister) throw new InvalidOperationException("Register failed (simulated)"); _registered = true; _connectionHandle = Interlocked.Increment(ref _nextHandle); return _connectionHandle; } /// /// Simulates tearing down the fake MXAccess connection. /// /// The connection handle supplied by the code under test. public void Unregister(int handle) { UnregisterCallCount++; _registered = false; _connectionHandle = 0; } /// /// Simulates resolving a tag reference into a fake runtime item handle. /// /// The synthetic connection handle. /// The Galaxy attribute reference being registered. /// A synthetic item handle. public int AddItem(int handle, string address) { var itemHandle = Interlocked.Increment(ref _nextHandle); Items[itemHandle] = address; return itemHandle; } /// /// Simulates removing an item from the fake runtime session. /// /// The synthetic connection handle. /// The synthetic item handle to remove. public void RemoveItem(int handle, int itemHandle) { Items.TryRemove(itemHandle, out _); } /// /// Marks an item as actively advised so tests can assert subscription activation. /// /// The synthetic connection handle. /// The synthetic item handle being monitored. public void AdviseSupervisory(int handle, int itemHandle) { AdvisedItems[itemHandle] = true; } /// /// Marks an item as no longer advised so tests can assert subscription teardown. /// /// The synthetic connection handle. /// The synthetic item handle no longer being monitored. public void UnAdviseSupervisory(int handle, int itemHandle) { AdvisedItems.TryRemove(itemHandle, out _); } /// /// Simulates a runtime write, records the written value, and optionally raises the write-complete callback. /// /// The synthetic connection handle. /// The synthetic item handle to write. /// The value supplied by the system under test. /// The security classification supplied with the write request. public void Write(int handle, int itemHandle, object value, int securityClassification) { if (ShouldFailWrite) throw new InvalidOperationException("Write failed (simulated)"); if (Items.TryGetValue(itemHandle, out var address)) WrittenValues.Add((address, value)); // Simulate async write complete callback var status = new MXSTATUS_PROXY[1]; if (WriteCompleteStatus == 0) { status[0].success = 1; } else { status[0].success = 0; status[0].detail = (short)WriteCompleteStatus; } if (!SkipWriteCompleteCallback) OnWriteComplete?.Invoke(_connectionHandle, itemHandle, ref status); } /// /// Simulates an MXAccess data change event for a specific item handle. /// /// The synthetic item handle that should receive the new value. /// The value to publish to the system under test. /// The runtime quality code to send with the value. /// The optional timestamp to send with the value; defaults to the current UTC time. public void SimulateDataChange(int itemHandle, object value, int quality = 192, DateTime? timestamp = null) { var status = new MXSTATUS_PROXY[1]; status[0].success = 1; OnDataChange?.Invoke(_connectionHandle, itemHandle, value, quality, (object)(timestamp ?? DateTime.UtcNow), ref status); } /// /// Simulates data change for a specific address (finds handle by address). /// /// The Galaxy attribute reference whose registered handle should receive the new value. /// The value to publish to the system under test. /// The runtime quality code to send with the value. /// The optional timestamp to send with the value; defaults to the current UTC time. public void SimulateDataChangeByAddress(string address, object value, int quality = 192, DateTime? timestamp = null) { foreach (var kvp in Items) { if (string.Equals(kvp.Value, address, StringComparison.OrdinalIgnoreCase)) { SimulateDataChange(kvp.Key, value, quality, timestamp); return; } } } } }