using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Xunit;
using ytLive.Models;
namespace ytLive.Tests;
///
/// The output-profile rule (2026-11-01): a build without Pro may emit at most 921,600 pixels
/// at 30 fps, and this is the only place that decision is made.
///
/// The one thing that must never break: a cracked or unlicensed build silently gets a
/// *smaller frame*, never a smaller feature set. If a future tier is added above the budget,
/// the last test here fails instead of shipping it to everyone.
///
public class OutputProfileTests
{
/// Mirror of MainViewModel.QualityOptions — the shipped tiers. Kept in
/// step deliberately: this is the table the budget has to cover.
private static readonly QualityOption[] Shipped =
{
new("1080p60", 60, 8.0, 1920, 1080),
new("1080p30", 30, 8.0, 1920, 1080),
new("720p60", 60, 6.0, 1280, 720),
new("720p30", 30, 6.0, 1280, 720),
new("Vertical 1080p30", 30, 8.0, 1080, 1920),
new("Vertical 1080p60", 60, 8.0, 1080, 1920),
};
private static QualityOption Tier(string name) => Shipped.Single(t => t.Display == name);
[Fact]
public void A_free_build_resolves_1080p60_down_to_720p30_with_its_own_bitrate()
{
var resolved = OutputProfile.Resolve(Tier("1080p60"), Shipped, pro: false);
Assert.Equal(1280, resolved.Width);
Assert.Equal(720, resolved.Height);
Assert.Equal(30, resolved.Fps);
// The bitrate follows the profile: a capped user is not offered an 8 Mbps budget.
Assert.Equal(6.0, resolved.Bitrate);
}
[Fact]
public void A_free_build_leaves_its_own_tier_untouched()
{
var requested = Tier("720p30");
Assert.Same(requested, OutputProfile.Resolve(requested, Shipped, pro: false));
}
[Fact]
public void Pro_passes_every_shipped_tier_through()
{
foreach (var tier in Shipped)
Assert.Same(tier, OutputProfile.Resolve(tier, Shipped, pro: true));
}
[Theory]
[InlineData("1080p60")]
[InlineData("1080p30")]
[InlineData("720p60")]
[InlineData("720p30")]
[InlineData("Vertical 1080p30")]
[InlineData("Vertical 1080p60")]
public void No_free_selection_can_emit_more_than_the_budget(string name)
{
var resolved = OutputProfile.Resolve(Tier(name), Shipped, pro: false);
Assert.True(resolved.Width * resolved.Height <= OutputProfile.FreeMaxPixels,
$"{resolved.Display} resolves to {resolved.Width * resolved.Height} px on a free build");
Assert.True(resolved.Fps <= OutputProfile.FreeMaxFps,
$"{resolved.Display} resolves to {resolved.Fps} fps on a free build");
}
[Fact]
public void An_empty_tier_table_still_yields_a_legal_frame()
{
var resolved = OutputProfile.Resolve(Tier("1080p60"), Array.Empty(), pro: false);
Assert.Equal(1280, resolved.Width);
Assert.Equal(720, resolved.Height);
Assert.Equal(30, resolved.Fps);
}
///
/// The developer override must not exist in a shipped binary. A runtime test cannot prove
/// that (the test host is a Debug build), so this asserts the build rule itself: the
/// panel's XAML and code-behind are removed whenever the configuration is not Debug.
///
[Fact]
public void The_developer_panel_is_excluded_from_the_release_compile()
{
var project = XDocument.Load(SourceAssert.RepoPath("ytLive.csproj"));
var guards = project.Descendants()
.Where(e => e.Name.LocalName is "Page" or "Compile")
.Where(e => (string?)e.Attribute("Remove") is string remove
&& remove.Contains("DeveloperPanel", StringComparison.OrdinalIgnoreCase))
.ToArray();
Assert.Equal(2, guards.Length);
foreach (var guard in guards)
{
// The condition may sit on the item or on its ItemGroup — either excludes it.
var condition = (string?)guard.Attribute("Condition")
?? (string?)guard.Parent?.Attribute("Condition");
Assert.NotNull(condition);
Assert.Contains("'$(Configuration)' != 'Debug'", condition, StringComparison.Ordinal);
}
}
}