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) { }
}
}
}