using System; using System.Threading.Tasks; using System.Windows.Input; using ytLive.Helpers; using ytLive.Models; namespace ytLive.Services; /// /// TASK 41 — the Test Stream pull-out (the "TEST" tab at the bottom of the Live /// screen's right rail). Drives a private test broadcast's chat tooling: /// Mock Chat Input sends a REAL liveChat/messages.insert (round-tripping through /// the normal ~2s poll and the live overlay path), and the simulated-event buttons /// inject synthetic member / SuperChat / subscriber rows through the exact /// MessageReceived seam the real poll uses — local-only, never sent to YouTube /// (the insert API only creates text). The broadcast itself runs through the real /// private go-live pipeline; this VM only owns the dock. /// public sealed class TestSessionViewModel : ViewModelBase { private readonly YouTubeStreamService _stream; private readonly YouTubeChatService _chat; private readonly Action? _onEndStream; private bool _isDrawerOpen; private bool _isTestSession; private string? _broadcastId; private string? _liveChatId; private string _sendMessageText = string.Empty; private bool _isSending; private string? _lastEventText; private int _eventCounter; /// Fake viewer names for simulated events — several backs make the /// rows look like a real muted room. private static readonly string[] SimulatedViewers = { "PixelPasta", "NightskyNick", "LunaGlow", "CactusFox" }; public TestSessionViewModel( YouTubeStreamService stream, YouTubeChatService chat, Action? onEndStream = null) { _stream = stream; _chat = chat; _onEndStream = onEndStream; SendTestMessageCommand = new RelayCommand(_ => _ = SendTestMessageAsync(), _ => CanSendTestMessage); SimulateSubscriberCommand = new RelayCommand(_ => Simulate(SimulatedSubscriber())); SimulateMemberCommand = new RelayCommand(_ => Simulate(SimulatedMember())); SimulateSuperChatCommand = new RelayCommand(_ => Simulate(SimulatedSuperChat())); EndTestCommand = new RelayCommand(_ => _onEndStream?.Invoke()); ToggleDrawerCommand = new RelayCommand(_ => IsDrawerOpen = !IsDrawerOpen); CloseDrawerCommand = new RelayCommand(_ => IsDrawerOpen = false); } public ICommand SendTestMessageCommand { get; } public RelayCommand SimulateSubscriberCommand { get; } public RelayCommand SimulateMemberCommand { get; } public RelayCommand SimulateSuperChatCommand { get; } public RelayCommand EndTestCommand { get; } public RelayCommand ToggleDrawerCommand { get; } public RelayCommand CloseDrawerCommand { get; } public bool IsDrawerOpen { get => _isDrawerOpen; set => SetProperty(ref _isDrawerOpen, value); } /// Whether a test broadcast is currently live (ended by BeginTest / /// EndTestSilently; EndTestCommand asks the app to stop, which routes back here). public bool IsTestSession { get => _isTestSession; private set { if (SetProperty(ref _isTestSession, value)) { OnPropertyChanged(nameof(SessionCaption)); OnPropertyChanged(nameof(ChatIdLine)); OnPropertyChanged(nameof(CanSendTestMessage)); } } } public string? BroadcastId => _broadcastId; public string ChatIdLine => _liveChatId == null ? "liveChatId: —" : $"liveChatId: {_liveChatId}"; public string SessionCaption => IsTestSession ? "Test stream is live — private to you, zero notifications, real chat." : "No test in progress — hit the Test button in the top bar to start a private broadcast and check your chat overlay."; public string SendMessageText { get => _sendMessageText; set { if (SetProperty(ref _sendMessageText, value)) OnPropertyChanged(nameof(CanSendTestMessage)); } } public bool IsSending { get => _isSending; private set { if (SetProperty(ref _isSending, value)) OnPropertyChanged(nameof(CanSendTestMessage)); } } public bool CanSendTestMessage => IsTestSession && !IsSending && _liveChatId != null && !string.IsNullOrWhiteSpace(SendMessageText); /// Feedback line for the last send / simulated event (null = nothing yet). public string? LastEventText { get => _lastEventText; private set => SetProperty(ref _lastEventText, value); } /// Called once by MainViewModel when the test broadcast is live and its /// liveChatId resolved — opens the dock so the chat tools are immediately usable. public void BeginTest(string broadcastId, string? liveChatId) { _broadcastId = broadcastId; _liveChatId = string.IsNullOrWhiteSpace(liveChatId) ? null : liveChatId; SendMessageText = string.Empty; LastEventText = _liveChatId == null ? "Chat polling couldn't start — Mock Chat Input is disabled." : null; IsTestSession = true; IsDrawerOpen = true; } /// Called by MainViewModel's stop path when the broadcast ends — the /// dock forgets the ended session (idempotent). public void EndTestSilently() { _broadcastId = null; _liveChatId = null; IsTestSession = false; IsSending = false; } /// Mock Chat Input: real liveChat/messages.insert. The inserted text /// arrives through the normal poll and renders via the live overlay path — the /// whole point is that the round-trip is the real one. public async Task SendTestMessageAsync() { if (!CanSendTestMessage || _liveChatId == null) return; var text = SendMessageText.Trim(); IsSending = true; SendMessageText = string.Empty; LastEventText = null; try { var error = await _stream.InsertChatMessageAsync(_liveChatId, text); LastEventText = error == null ? $"Sent — “{text}” should appear on the chat overlay within a couple of seconds." : $"Couldn't send: {error}"; } catch (Exception ex) { AppLog.Write($"Test chat message send failed: {ex.Message}"); LastEventText = "Couldn't send that message — try again."; } finally { IsSending = false; } } private void Simulate(ChatMessage message) { if (!IsTestSession) return; _chat.InjectSimulatedMessage(message); _eventCounter++; LastEventText = $"Fired #{_eventCounter}: {Describe(message)} — renders on any visible chat overlay."; AppLog.Write($"Test chat simulation {_eventCounter} injected"); } /// Subscriptions aren't chat events on YouTube, so a "subscriber" row /// is a plain text-shaped message carrying the count — honest: the chat overlay /// shows it as a regular row, which is exactly what a sub looks like in chat. /// It carries on purpose: no alert animation /// fires for free subs — the chat mention is the announcement (creator ruling). private ChatMessage SimulatedSubscriber() { var author = NextViewer(); return NewSimulated(author, "New subscriber — welcome to the herd! 🦙 (simulated subscribe)"); } private ChatMessage SimulatedMember() { var author = NextViewer(); var message = NewSimulated(author, $"🎉 {author} became a member!"); message.IsMember = true; message.MembershipLevel = "New member"; message.Kind = ChatEventKind.NewMember; return message; } private ChatMessage SimulatedSuperChat() { var author = NextViewer(); var message = NewSimulated(author, "Keep the streams flowing, llama power 🦙"); message.IsSuperChat = true; message.SuperChatAmount = 5.00; message.SuperChatCurrency = "$"; message.SuperChatDisplayString = "$5.00"; message.Kind = ChatEventKind.SuperChat; return message; } private ChatMessage NewSimulated(string author, string text) { return new ChatMessage { AuthorName = author, Message = text, Timestamp = DateTime.Now, IsSimulated = true, }; } private string NextViewer() => SimulatedViewers[_eventCounter % SimulatedViewers.Length]; private static string Describe(ChatMessage m) { if (m.IsSuperChat) return $"{m.AuthorName} pledged {m.SuperChatCurrency}{m.SuperChatAmount:0.00} in a Super Chat"; if (m.IsMember) return $"{m.AuthorName} just became a member ({m.MembershipLevel})"; return $"{m.AuthorName} subscribed"; } }