using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Xunit;
namespace ytLive.Tests;
/// DESIGN.md became the visual standard on 2026-10-30. This is its guard: the document
/// is the contract, `Themes/Tokens.xaml` is the implementation, and a token documented in one but
/// missing (or different) in the other fails here — so the palette cannot drift into two truths
/// the way the 2026-10-26 and 2026-10-29 Stitch exports did.
///
/// Deliberately parses the markdown rather than duplicating a list: adding a colour to the
/// standard requires implementing it, and changing a value in the theme without the document is a
/// lie the gate catches.
public class DesignTokensTests
{
private static readonly XNamespace Xaml = "http://schemas.microsoft.com/winfx/2006/xaml";
[Fact]
public void Every_colour_in_the_standard_exists_as_a_token_brush_with_the_documented_value()
{
var documented = DocumentedColours();
Assert.True(documented.Count >= 20,
$"parsed only {documented.Count} colour tokens out of DESIGN.md — the table format changed");
var brushes = Brushes();
foreach (var (token, hex) in documented)
{
var key = "Brush." + Pascal(token);
Assert.True(brushes.ContainsKey(key),
$"DESIGN.md documents `{token}` ({hex}) but Themes/Tokens.xaml has no `{key}` brush");
Assert.Equal(hex, brushes[key], ignoreCase: true);
}
}
[Fact]
public void The_meter_bands_are_the_standard_three()
{
var brushes = Brushes();
// DESIGN.md -> "Meter thresholds": nominal to -12 dB, caution to -3 dB, red above.
Assert.Equal("#38bdf8", brushes["Brush.Meter.Nominal"], ignoreCase: true);
Assert.Equal("#ffb95f", brushes["Brush.Meter.Caution"], ignoreCase: true);
Assert.Equal("#ff5451", brushes["Brush.Meter.Peak"], ignoreCase: true);
}
[Fact]
public void Tokens_merge_before_the_theme_so_styles_can_resolve_them()
{
var app = XDocument.Load(SourceAssert.RepoPath("App.xaml"));
var sources = app.Descendants()
.Where(e => e.Name.LocalName == "ResourceDictionary")
.Select(e => (string?)e.Attribute("Source"))
.Where(s => s != null)
.ToList();
var tokens = sources.FindIndex(s => s!.EndsWith("Themes/Tokens.xaml", StringComparison.Ordinal));
var controls = sources.FindIndex(s => s!.EndsWith("Themes/Controls.xaml", StringComparison.Ordinal));
Assert.True(tokens >= 0, "App.xaml does not merge Themes/Tokens.xaml");
Assert.True(controls >= 0, "App.xaml no longer merges Themes/Controls.xaml");
Assert.True(tokens < controls,
"Themes/Tokens.xaml must merge BEFORE Themes/Controls.xaml, or styles cannot resolve a token");
}
[Fact]
public void Converted_surfaces_carry_no_raw_colour()
{
// The sweep converts one surface per commit; each adds its file here, so every step is
// guarded on its own instead of waiting for a global grep at the end. Pure white and black
// are allowed (gloss highlights, knockout masks) — everything else must be a token.
string[] converted = { "Themes/Controls.xaml" };
foreach (var file in converted)
{
var text = SourceAssert.Text(file.Split('/'));
var raw = Regex.Matches(text, @"#[0-9a-fA-F]{6,8}")
.Select(m => m.Value)
.Where(v => v is not "#ffffff" and not "#000000" and not "#FFFFFF" and not "#000000")
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
Assert.True(raw.Count == 0,
$"{file} still hardcodes {string.Join(", ", raw)} — resolve a Brush.* token instead");
}
}
[Fact]
public void The_retired_brand_red_is_not_a_token()
{
// #e94560 was the general accent in 15 files. It is retired (DESIGN.md, 2026-10-30):
// blue is interactive, red means live/record. Re-introducing it as a token would undo
// the rule, so it must not appear anywhere in the token sheet.
Assert.DoesNotContain("#e94560", File.ReadAllText(SourceAssert.RepoPath("Themes", "Tokens.xaml")),
StringComparison.OrdinalIgnoreCase);
}
/// The `| Token | Hex |` tables under "## Colour", stopping at "## Typography".
/// The "Rejected from the export" table lives further down and must not count.
private static Dictionary DocumentedColours()
{
var result = new Dictionary(StringComparer.Ordinal);
var inColour = false;
foreach (var raw in File.ReadAllLines(SourceAssert.RepoPath("DESIGN.md")))
{
var line = raw.Trim();
if (line.StartsWith("## ", StringComparison.Ordinal))
{
inColour = line.StartsWith("## Colour", StringComparison.Ordinal);
continue;
}
if (!inColour || !line.StartsWith("| `", StringComparison.Ordinal)) continue;
var cells = line.Split('|').Select(c => c.Trim()).Where(c => c.Length > 0).ToArray();
if (cells.Length < 2) continue;
// The token cell may carry a note after the backticked name("`tertiary` (electric blue)").
var token = Backticked(cells[0]);
var hex = Backticked(cells[1]);
// 6-digit RGB or 8-digit ARGB (the scrim washes carry alpha).
if (!hex.StartsWith("#", StringComparison.Ordinal) || hex.Length is not (7 or 9)) continue;
result[token] = hex;
}
return result;
}
private static Dictionary Brushes()
{
var dict = XDocument.Load(SourceAssert.RepoPath("Themes", "Tokens.xaml"));
return dict.Descendants()
.Where(e => e.Name.LocalName == "SolidColorBrush")
.Select(e => ((string?)e.Attribute(Xaml + "Key"), (string?)e.Attribute("Color")))
.Where(p => p.Item1 != null && p.Item2 != null)
.ToDictionary(p => p.Item1!, p => p.Item2!, StringComparer.Ordinal);
}
/// The first `backticked` run in a markdown cell, or the trimmed cell itself.
private static string Backticked(string cell)
{
var start = cell.IndexOf('`');
if (start < 0) return cell.Trim();
var end = cell.IndexOf('`', start + 1);
return end < 0 ? cell.Trim() : cell[(start + 1)..end].Trim();
}
/// `surface-container-lowest` → `SurfaceContainerLowest`.
private static string Pascal(string token) =>
string.Concat(token.Split('-', StringSplitOptions.RemoveEmptyEntries)
.Select(part => char.ToUpperInvariant(part[0]) + part[1..]));
}