using System.Collections.Concurrent; using System.Text.RegularExpressions; using MBN_STOCK_WEBVIEW.Playout.Configuration; using MBN_STOCK_WEBVIEW.Playout.Interop; using MBN_STOCK_WEBVIEW.Playout.Registration; using MBN_STOCK_WEBVIEW.Playout.Runtime; using MBN_STOCK_WEBVIEW.Playout.Safety; namespace MBN_STOCK_WEBVIEW.Playout.Tests; internal sealed class FakeK3dSessionFactory : IK3dSessionFactory { private readonly Func _create; private int _createCount; public FakeK3dSessionFactory(Func? create = null) { _create = create ?? (() => new FakeK3dSession()); } public int CreateCount => Volatile.Read(ref _createCount); public ConcurrentQueue Sessions { get; } = new(); public IK3dSession Create() { Interlocked.Increment(ref _createCount); var session = _create(); Sessions.Enqueue(session); return session; } } internal sealed class FakeK3dSdkDispatchLatch(bool allow = true) : IK3dSdkDispatchLatch { public IDisposable? TryClaim() => allow ? new Claim() : null; private sealed class Claim : IDisposable { public void Dispose() { } } } internal sealed class FakeK3dSession : IK3dGuardedConnectSession { private int _disposed; private int _ktapDispatchGate; private int _ktapDispatchCount; public bool IsConnected { get; private set; } public PlayoutKtapConnectState LastKtapConnectState { get; private set; } public bool SupportsLifecycleCallbacks { get; set; } public bool? KtapHelloObserved { get; set; } public bool HasPendingLifecycleCallbacks { get; set; } public bool HasPendingPlayCallbacks { get; set; } public bool ClearPendingLifecycleCallbacksAfterProcess { get; set; } = true; public ConcurrentQueue Calls { get; } = new(); public ConcurrentQueue ThreadIds { get; } = new(); public ConcurrentQueue ApartmentStates { get; } = new(); public Action? ConnectAction { get; set; } public Action? BeforeKtapDispatchAction { get; set; } public int KtapDispatchCount => Volatile.Read(ref _ktapDispatchCount); public Action? DisconnectAction { get; set; } public Action? PrepareAction { get; set; } public Action? UpdateOnAirAction { get; set; } public Action? PlayAction { get; set; } public Action? TakeOutAction { get; set; } public Func? ProcessPendingCallbacksAction { get; set; } public Action? AbandonAction { get; set; } public Action? CallObserver { get; set; } public void Connect(ValidatedPlayoutOptions options) => ConnectCore(options, finalSafetyCheck: null); public void ConnectGuarded( ValidatedPlayoutOptions options, Func finalSafetyCheck, IK3dSdkDispatchLatch dispatchLatch) { ArgumentNullException.ThrowIfNull(finalSafetyCheck); ArgumentNullException.ThrowIfNull(dispatchLatch); ConnectCore(options, finalSafetyCheck, dispatchLatch); } private void ConnectCore( ValidatedPlayoutOptions options, Func? finalSafetyCheck, IK3dSdkDispatchLatch? dispatchLatch = null) { Record("Connect"); BeforeKtapDispatchAction?.Invoke(); if (finalSafetyCheck is not null && !finalSafetyCheck()) { throw new K3dConnectSafetyException(); } using (var dispatchClaim = dispatchLatch?.TryClaim()) { if (dispatchLatch is not null && dispatchClaim is null) { throw new K3dConnectSafetyException(); } if (Interlocked.CompareExchange(ref _ktapDispatchGate, 1, 0) != 0) { throw new InvalidOperationException("Fake KTAP dispatch was prevented."); } LastKtapConnectState = PlayoutKtapConnectState.Attempted; Interlocked.Increment(ref _ktapDispatchCount); try { ConnectAction?.Invoke(options); } catch { LastKtapConnectState = PlayoutKtapConnectState.Failed; throw; } } LastKtapConnectState = PlayoutKtapConnectState.AcceptedUnconfirmed; IsConnected = true; } public bool TryPreventKtapConnect() { var previous = Interlocked.CompareExchange(ref _ktapDispatchGate, 2, 0); if (previous == 1 && LastKtapConnectState == PlayoutKtapConnectState.NotAttempted) { LastKtapConnectState = PlayoutKtapConnectState.Attempted; } return previous == 0; } public void Disconnect() { Record("Disconnect"); DisconnectAction?.Invoke(); IsConnected = false; } public void Prepare(PlayoutCue cue, int layoutIndex) { Record($"Prepare:{cue.SceneName}"); PrepareAction?.Invoke(cue, layoutIndex); } public void UpdateOnAir(PlayoutCue cue, int layoutIndex) { Record($"UpdateOnAir:{cue.SceneName}"); UpdateOnAirAction?.Invoke(cue, layoutIndex); } public void Play(int layoutIndex) { Record("Play"); PlayAction?.Invoke(layoutIndex); } public void TakeOut(int layoutIndex, PlayoutTakeOutScope scope) { Record($"TakeOut:{scope}"); TakeOutAction?.Invoke(layoutIndex, scope); } public K3dCallbackDrainResult ProcessPendingCallbacks(int layoutIndex) { Record("ProcessPendingCallbacks"); var result = ProcessPendingCallbacksAction?.Invoke(layoutIndex) ?? new K3dCallbackDrainResult(0, 0, KtapHelloObserved == true); if (ClearPendingLifecycleCallbacksAfterProcess) { HasPendingLifecycleCallbacks = false; HasPendingPlayCallbacks = false; } return result; } public void Abandon() { TryPreventKtapConnect(); if (Interlocked.Exchange(ref _disposed, 1) == 0) { Record("Abandon"); AbandonAction?.Invoke(); IsConnected = false; } } public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) == 0) { Record("Dispose"); IsConnected = false; } } private void Record(string name) { Calls.Enqueue(name); ThreadIds.Enqueue(Environment.CurrentManagedThreadId); ApartmentStates.Enqueue(Thread.CurrentThread.GetApartmentState()); CallObserver?.Invoke(name); } } internal sealed class FakeRegistrationProbe : IK3dRegistrationProbe { private int _probeCount; public FakeRegistrationProbe(K3dRegistrationReport? report = null) { Report = report ?? ReadyReport; } public static K3dRegistrationReport ReadyReport { get; } = new( K3dRegistrationIssue.None, Is64BitProcess: true, HasWin64TypeLibrary: true, IsWin64BinaryAmd64: true, IsEngineClassReady: true, IsEventHandlerClassReady: true, Message: "fake K3D registration ready"); public K3dRegistrationReport Report { get; set; } public int ProbeCount => Volatile.Read(ref _probeCount); public K3dRegistrationReport Probe() { Interlocked.Increment(ref _probeCount); return Report; } } internal sealed class FakeTornadoProcessProbe : ITornadoProcessProbe { private readonly object _sync = new(); private readonly Queue _snapshots = new(); private TornadoProcessSnapshot _last; public FakeTornadoProcessProbe(params TornadoProcessSnapshot[] snapshots) { _last = snapshots.LastOrDefault() ?? new TornadoProcessSnapshot(0, 0, 0); foreach (var snapshot in snapshots) { _snapshots.Enqueue(snapshot); } } public int CaptureCount { get; private set; } public Regex? LastPattern { get; private set; } public TornadoProcessSnapshot Capture(Regex? testWindowPattern) { lock (_sync) { CaptureCount++; LastPattern = testWindowPattern; if (_snapshots.Count > 0) { _last = _snapshots.Dequeue(); } return _last; } } } internal sealed class FakeLiveAuthorization : ILiveAuthorization { public FakeLiveAuthorization(bool isAuthorizedForThisLaunch) { IsAuthorizedForThisLaunch = isAuthorizedForThisLaunch; } public bool IsAuthorizedForThisLaunch { get; set; } } internal sealed class TrackingStaDispatcherFactory : IStaDispatcherFactory { private int _createCount; public int CreateCount => Volatile.Read(ref _createCount); public ConcurrentQueue Dispatchers { get; } = new(); public IStaDispatcher Create(int capacity) { Interlocked.Increment(ref _createCount); var dispatcher = new StaDispatcher(capacity); Dispatchers.Enqueue(dispatcher); return dispatcher; } }