using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Xunit;
namespace ytLive.Tests;
/// A missing {StaticResource} key does not fail the build — WPF substitutes
/// DependencyProperty.UnsetValue and throws at the next layout pass. That is exactly how
/// the app crashed on 2026-10-30: the token conversion deleted YtSliderKnobHoverBrush
/// while two slider triggers still asked for it, so the first slider the creator touched
/// (the new socials opacity slider) killed the app inside Ellipse.OnRender.
///
/// This guard scans every XAML file in the app and proves each referenced key exists —
/// either in the theme/App resources or in the same file — so the class cannot come back.
public class ThemeResourceTests
{
[Fact]
public void Every_StaticResource_reference_resolves_to_a_defined_key()
{
var root = System.IO.Path.GetDirectoryName(SourceAssert.RepoPath("App.xaml"))!;
var globalKeys = new HashSet(StringComparer.Ordinal);
foreach (var file in new[] { System.IO.Path.Combine(root, "App.xaml") }
.Concat(Directory.GetFiles(System.IO.Path.Combine(root, "Themes"), "*.xaml")))
globalKeys.UnionWith(KeysIn(File.ReadAllText(file)));
var missing = new List();
foreach (var path in XamlFiles())
{
var text = File.ReadAllText(path);
var localKeys = KeysIn(text);
foreach (Match match in Regex.Matches(text, @"\{StaticResource ([^}]+)\}"))
{
var key = match.Groups[1].Value.Trim();
if (key.StartsWith("{", StringComparison.Ordinal)) continue; // nested markup extension
if (globalKeys.Contains(key) || localKeys.Contains(key)) continue;
missing.Add($"{System.IO.Path.GetFileName(path)}: {key}");
}
}
Assert.True(missing.Count == 0,
"StaticResource keys with no definition (WPF throws UnsetValue at layout):\n "
+ string.Join("\n ", missing.Distinct()));
}
[Fact]
public void A_dictionary_never_references_a_key_defined_later_in_itself()
{
// StaticResource resolves while the dictionary is being built, so a forward reference
// fails at load with "Cannot find resource named …" — exactly how the app refused to
// launch on 2026-10-30 (a BasedOn pointing at a style defined further down the file).
var root = System.IO.Path.GetDirectoryName(SourceAssert.RepoPath("App.xaml"))!;
var files = new[] { System.IO.Path.Combine(root, "App.xaml") }
.Concat(Directory.GetFiles(System.IO.Path.Combine(root, "Themes"), "*.xaml"));
var forward = new List();
foreach (var file in files)
{
var text = File.ReadAllText(file);
var definitions = Regex.Matches(text, @"x:Key=""([^""]+)""")
.ToDictionary(m => m.Groups[1].Value, m => m.Index, StringComparer.Ordinal);
foreach (Match use in Regex.Matches(text, @"\{StaticResource ([^}]+)\}"))
{
var key = use.Groups[1].Value.Trim();
if (!definitions.TryGetValue(key, out var defined)) continue;
if (defined > use.Index)
forward.Add($"{System.IO.Path.GetFileName(file)}: {key} used before it is defined");
}
}
Assert.True(forward.Count == 0,
"Forward StaticResource references fail at load:\n " + string.Join("\n ", forward.Distinct()));
}
private static HashSet KeysIn(string text)
=> Regex.Matches(text, @"x:Key=""([^""]+)""").Select(m => m.Groups[1].Value)
.ToHashSet(StringComparer.Ordinal);
private static IEnumerable XamlFiles()
{
var root = System.IO.Path.GetDirectoryName(SourceAssert.RepoPath("App.xaml"))!;
return Directory.EnumerateFiles(root, "*.xaml", SearchOption.AllDirectories)
.Where(p => !p.Contains($"{System.IO.Path.DirectorySeparatorChar}obj{System.IO.Path.DirectorySeparatorChar}", StringComparison.Ordinal)
&& !p.Contains($"{System.IO.Path.DirectorySeparatorChar}bin{System.IO.Path.DirectorySeparatorChar}", StringComparison.Ordinal)
&& !p.Contains("ytLive.Tests", StringComparison.Ordinal));
}
}