using System; using System.IO; using System.Linq; using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.ViewModels; namespace ytLive.Tests; /// /// The creator's own video rate: per tier, clamped at the tier's YouTube minimum, persisted across /// runs, and substituted into every consumer that builds encoder options. /// /// The rule that must never break: whatever the creator typed is what the encoder is asked to /// hold, and no tier's minimum is quietly undercut. /// [Collection("MainViewModelLayoutPath")] public class RatedBitrateTests { [Fact] public void The_creator_rate_is_per_tier_clamped_to_the_minimum_and_restored() { var tempDb = Path.Combine(Path.GetTempPath(), $"llamacasty-rate-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; try { var vm = new MainViewModel(); var tier60 = vm.QualityOptions.Single(tier => tier.Display == "1080p60"); var tier30 = vm.QualityOptions.Single(tier => tier.Display == "1080p30"); vm.SelectedQuality = tier60; // Unset means the shipped default, not zero. Assert.Equal(tier60.Bitrate, vm.RatedMbps); // A creator who knows their line sets their own number, and the encoder sees it. vm.RatedMbps = 15; Assert.Equal(15, vm.RatedMbps); Assert.Equal(15, vm.EffectiveQuality(tier60).Bitrate); Assert.Equal(60, vm.EffectiveQuality(tier60).Fps); // geometry stays the tier's // Below YouTube's minimum for the tier is not a legal target. vm.RatedMbps = 2; Assert.Equal(RateLadder.For(tier60).Minimum, vm.RatedMbps); // The other tier keeps its own value — one setting does not leak into the ladder. vm.SelectedQuality = tier30; Assert.Equal(tier30.Bitrate, vm.RatedMbps); vm.SelectedQuality = tier60; // And it survives a restart: the same profile, a fresh view model. var reloaded = new MainViewModel(); reloaded.SelectedQuality = reloaded.QualityOptions.Single(tier => tier.Display == "1080p60"); Assert.Equal(RateLadder.For(tier60).Minimum, reloaded.RatedMbps); } finally { MainViewModel.LayoutPathOverride = null; try { File.Delete(tempDb); } catch (IOException) { } } } /// /// The suggestion the creator is asked about before a stream, and the refusal that stops it coming /// back: raising is proposed only from five clean sessions above the 10% gap, applied only on a /// click, and "keep mine" suppresses that exact number for the tier. /// [Fact] public void A_suggested_rate_is_offered_between_sessions_and_only_a_click_applies_it() { var tempDb = Path.Combine(Path.GetTempPath(), $"llamacasty-rate-proposal-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; try { using (var seed = new LayoutStore(tempDb)) { for (var i = 0; i < 5; i++) seed.AppendRateHistory(new SessionRateResult("1080p60", "h264_nvenc", 15.0, 15.0, 0)); Assert.Equal(5, seed.LoadRateHistory().Count); } var vm = new MainViewModel(); vm.SelectedQuality = vm.QualityOptions.Single(tier => tier.Display == "1080p60"); var historyCount = new LayoutStore(tempDb).LoadRateHistory().Count; // Offered, not applied: the rate is still the shipped default until the creator agrees. Assert.True(vm.HasRateProposal, $"history={historyCount}"); Assert.Contains("15", vm.RateProposalText); Assert.Equal(12.0, vm.RatedMbps); vm.AcceptRateProposalCommand.Execute(null); Assert.Equal(15.0, vm.RatedMbps); Assert.False(vm.HasRateProposal); // Honest evidence now says 15 is what this line holds, so the same question stops being // asked: 15 is no longer 10% above the rate in use. var second = new MainViewModel(); second.SelectedQuality = second.QualityOptions.Single(tier => tier.Display == "1080p60"); Assert.False(second.HasRateProposal); // Back to 12 by hand: the evidence qualifies again, and this time the creator says no. second.RatedMbps = 12; second.SelectedQuality = second.QualityOptions.Single(tier => tier.Display == "1080p30"); second.SelectedQuality = second.QualityOptions.Single(tier => tier.Display == "1080p60"); Assert.True(second.HasRateProposal); second.DismissRateProposalCommand.Execute(null); Assert.False(second.HasRateProposal); Assert.Equal(12.0, second.RatedMbps); // Refusal is remembered against that number, so a fresh run stays quiet. var third = new MainViewModel(); third.SelectedQuality = third.QualityOptions.Single(tier => tier.Display == "1080p60"); Assert.False(third.HasRateProposal); } finally { MainViewModel.LayoutPathOverride = null; try { File.Delete(tempDb); } catch (IOException) { } } } }