using Xunit; using ytLive.Models; namespace ytLive.Tests; /// Session-scoped scoreboard tallies. The rules that matter: money is never added across /// currencies, and a gifted membership is counted ONCE (from the purchase), never again from the /// recipient's event — that double-count is the easy mistake, and the creator called it out. public class ScoreboardStateTests { private static ChatMessage Message(ChatEventKind kind, string author = "viewer", double amount = 0, string? currency = "USD", int giftCount = 0, string? display = null) => new() { Kind = kind, AuthorName = author, SuperChatAmount = amount, SuperChatCurrency = currency, GiftNewMembersCount = giftCount, SuperChatDisplayString = display, }; [Fact] public void Tallies_follow_the_six_events_and_never_double_count_a_gift() { var state = new ScoreboardState(); state.Apply(Message(ChatEventKind.NewMember, "Paid")); Assert.Equal(1, state.NewMembers); Assert.Equal("Paid", state.LastMemberName); // A gift purchase of 5 memberships counts 5... state.Apply(Message(ChatEventKind.MemberGift, "Gifter", giftCount: 5)); Assert.Equal(6, state.NewMembers); Assert.Equal(5, state.LastGiftCount); Assert.Equal("Gifter", state.LastGifterName); // ...and the recipients' own events add NOTHING (they are the same memberships). for (var i = 0; i < 5; i++) state.Apply(Message(ChatEventKind.GiftReceived, $"Recipient{i}", giftCount: 1)); Assert.Equal(6, state.NewMembers); Assert.Equal(5, state.LastGiftCount); Assert.Equal("Gifter", state.LastGifterName); state.Apply(Message(ChatEventKind.MemberMilestone, "Longtimer")); Assert.Equal(1, state.MilestoneCount); // Super Chat and Super Sticker both carry money; the last one drives the "latest" tile. state.Apply(Message(ChatEventKind.SuperChat, "Tipper", 10, "USD", display: "$10.00")); state.Apply(Message(ChatEventKind.SuperSticker, "Sticker", 5, "USD")); Assert.Equal(15, state.SessionTotal.Total, 3); Assert.Equal("USD", state.SessionTotal.Currency); Assert.Equal("Sticker", state.LastSuperChatAuthor); Assert.False(state.HasMixedCurrencies); // A second currency is tracked separately, never summed into the first. state.Apply(Message(ChatEventKind.SuperChat, "Euro", 3, "EUR")); Assert.Equal(15, state.MoneyByCurrency["USD"], 3); Assert.Equal(3, state.MoneyByCurrency["EUR"], 3); Assert.True(state.HasMixedCurrencies); Assert.Equal("USD", state.SessionTotal.Currency); // dominant by value, not by recency // Plain chat is ignored entirely. state.Apply(Message(ChatEventKind.None, "Chatter")); Assert.Equal(6, state.NewMembers); Assert.Equal(1, state.MilestoneCount); // A new session starts clean. state.Reset(); Assert.Equal(0, state.NewMembers); Assert.Equal(0, state.SessionTotal.Total); Assert.Null(state.LastMemberName); } }