using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Tornado3_2026Election.Domain; namespace Tornado3_2026Election.Services; public sealed class MockTornado3Adapter : ITornado3Adapter { private readonly LogService _logService; private TornadoConnectionState _state = TornadoConnectionState.Idle; public MockTornado3Adapter(LogService logService) { _logService = logService; } public string BackendName => "Mock Adapter"; public bool IsLiveCg => false; public bool IsConnected => false; public string ConnectionTarget => "실CG 미연동"; public TornadoConnectionState State { get => _state; private set { if (_state == value) { return; } _state = value; StateChanged?.Invoke(this, value); } } public event EventHandler? StateChanged; public event EventHandler? ConnectionChanged; public async Task EnsureConnectedAsync(CancellationToken cancellationToken) { await ExecuteWithTimeoutAsync(async () => { await Task.Delay(120, cancellationToken).ConfigureAwait(false); State = TornadoConnectionState.Ready; _logService.Info("Tornado3 mock adapter connected."); }, cancellationToken).ConfigureAwait(false); } public async Task ApplyCutAsync( BroadcastChannel channel, FormatTemplateDefinition template, FormatCutDefinition cut, ElectionDataSnapshot snapshot, BroadcastStationProfile station, string imageRootPath, CancellationToken cancellationToken) { await ExecuteWithTimeoutAsync(async () => { State = TornadoConnectionState.Sending; await Task.Delay(100, cancellationToken).ConfigureAwait(false); var summary = snapshot.BroadcastPhase == BroadcastPhase.PreElection ? $"turnout={snapshot.TurnoutRate:0.0}% voters={snapshot.TurnoutVotes:N0}" : $"leader={snapshot.Candidates.OrderByDescending(candidate => candidate.VoteCount).FirstOrDefault()?.Name ?? "없음"} counted={snapshot.CountedVotes:N0}"; _logService.Info($"[{channel}] Apply {template.Name}/{cut.Name} station={station.Name} {summary} imageRoot={imageRootPath}"); State = TornadoConnectionState.Ready; }, cancellationToken).ConfigureAwait(false); } public Task TryCapturePendingCutPreviewAsync( BroadcastChannel channel, string fileName, int width, int height, int frame, CancellationToken cancellationToken) { return Task.FromResult(false); } public Task TryCaptureCutPreviewAsync( BroadcastChannel channel, FormatTemplateDefinition template, FormatCutDefinition cut, ElectionDataSnapshot snapshot, BroadcastStationProfile station, string imageRootPath, string fileName, int width, int height, int frame, CancellationToken cancellationToken) { return Task.FromResult(false); } public async Task PrepareAsync(BroadcastChannel channel, CancellationToken cancellationToken) { await ExecuteWithTimeoutAsync(async () => { State = TornadoConnectionState.Ready; await Task.Delay(60, cancellationToken).ConfigureAwait(false); _logService.Info($"[{channel}] Prepare"); }, cancellationToken).ConfigureAwait(false); } public async Task ShowPreparedFirstFrameAsync(BroadcastChannel channel, CancellationToken cancellationToken) { await ExecuteWithTimeoutAsync(async () => { State = TornadoConnectionState.Ready; await Task.Delay(40, cancellationToken).ConfigureAwait(false); _logService.Info($"[{channel}] Show prepared first frame on PGM"); }, cancellationToken).ConfigureAwait(false); } public async Task TakeAsync(BroadcastChannel channel, CancellationToken cancellationToken) { await ExecuteWithTimeoutAsync(async () => { State = TornadoConnectionState.OnAir; await Task.Delay(60, cancellationToken).ConfigureAwait(false); _logService.Info($"[{channel}] Take"); }, cancellationToken).ConfigureAwait(false); } public async Task ClearOutputAsync(BroadcastChannel channel, CancellationToken cancellationToken) { await ExecuteWithTimeoutAsync(async () => { State = TornadoConnectionState.Idle; await Task.Delay(30, cancellationToken).ConfigureAwait(false); _logService.Info($"[{channel}] Clear output layer"); }, cancellationToken).ConfigureAwait(false); } public async Task OutAsync(BroadcastChannel channel, CancellationToken cancellationToken) { await ExecuteWithTimeoutAsync(async () => { State = TornadoConnectionState.Idle; await Task.Delay(60, cancellationToken).ConfigureAwait(false); _logService.Info($"[{channel}] Layer Out"); }, cancellationToken).ConfigureAwait(false); } private static async Task ExecuteWithTimeoutAsync(Func action, CancellationToken cancellationToken) { using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); await action().WaitAsync(timeoutCts.Token).ConfigureAwait(false); } }