using System.Globalization;
using MeterVault.App.Localization;
using MeterVault.Core.Domain;
namespace MeterVault.Integration.Tests.Localization;
///
/// Every domain enum value the UI renders has wording in every language (SDD §12, M7).
///
///
/// ends each mapping with a fallback arm that returns the bare identifier,
/// so adding an enum value can never throw mid-render on a dashboard. The cost of that safety is that
/// a forgotten value degrades silently to English — these tests are what make it loud instead, by
/// demanding a matching Enum_<Type>_<Value> resource for every declared value.
///
public sealed class EnumDisplayNameTests
{
[Fact]
public void Every_localized_enum_value_has_a_resource_in_every_language()
{
var missing = new List();
foreach (var type in DisplayNames.LocalizedEnums)
{
foreach (var name in Enum.GetNames(type))
{
// None is the empty bitmask of a flags enum (ReadingFlags, Provenance), deliberately rendered as
// nothing at all.
if (type.IsDefined(typeof(FlagsAttribute), inherit: false) && name == "None")
{
continue;
}
var key = $"Enum_{type.Name}_{name}";
foreach (var culture in Loc.SupportedCultures)
{
var value = Strings.ResourceManager.GetString(key, CultureInfo.GetCultureInfo(culture));
if (string.IsNullOrWhiteSpace(value))
{
missing.Add($"{key} [{culture}]");
}
}
}
}
Assert.True(missing.Count == 0, $"Missing enum display names: {string.Join(", ", missing)}");
}
[Fact]
public void Display_names_are_translated_rather_than_echoing_the_identifier()
{
// Not every value can differ — "Tasmota", "Bonus" and "Global" are the same word in German —
// but if a whole enum came back as its own identifiers, the mapping was never written.
var untranslated = new List();
foreach (var type in DisplayNames.LocalizedEnums)
{
var names = Enum.GetNames(type);
var translated = 0;
foreach (var name in names)
{
var german = Strings.ResourceManager.GetString($"Enum_{type.Name}_{name}", CultureInfo.GetCultureInfo("de"));
if (german is not null && !string.Equals(german, name, StringComparison.Ordinal))
{
translated++;
}
}
if (translated == 0)
{
untranslated.Add(type.Name);
}
}
Assert.True(untranslated.Count == 0, $"Enums with no German wording at all: {string.Join(", ", untranslated)}");
}
[Theory]
[InlineData("en", "Consumption")]
[InlineData("de", "Verbrauch")]
public void The_reader_s_language_decides_the_wording(string culture, string expected) =>
Assert.Equal(expected, WithUiCulture(culture, () => ConsumptionKind.Consumption.Display()));
[Fact]
public void A_bitmask_lists_the_flags_it_actually_carries()
{
// Blank rather than "None": the readings table has a flag column that is empty on almost
// every row, and printing a word down the whole page is noise, not information.
Assert.Equal(string.Empty, ReadingFlags.None.Display());
var both = WithUiCulture("de", () => (ReadingFlags.CounterReset | ReadingFlags.MeterSwap).Display());
Assert.Equal("Zählerreset, Zählerwechsel", both);
Assert.Equal("Meter swap", WithUiCulture("en", () => ReadingFlags.MeterSwap.Display()));
}
[Fact]
public void The_analysis_vocabulary_is_worded_in_both_languages()
{
Assert.Equal("Keine Daten", WithUiCulture("de", () => MeterVault.Core.Analysis.BucketStatus.Missing.Display()));
Assert.Equal("Last 12 months", WithUiCulture("en", () => MeterVault.Core.Analysis.PeriodPreset.Last12Months.Display()));
Assert.Equal("Nicht bewertet (kein Tarif)", WithUiCulture("de", () => MeterVault.Core.Analysis.Costing.CostStatus.NotPriced.Display()));
Assert.Equal("Tanks & Vorräte", WithUiCulture("de", () => Strings.Nav_Consumables));
Assert.Equal("Same period last year", WithUiCulture("en", () => new MeterVault.Core.Analysis.ComparisonRequest(MeterVault.Core.Analysis.ComparisonKind.PreviousYear).Display()));
Assert.Equal("2024", new MeterVault.Core.Analysis.ComparisonRequest(MeterVault.Core.Analysis.ComparisonKind.Year, 2024).Display());
// Provenance is a set of flags: each worded, none shown as nothing.
var provenance = MeterVault.Core.Analysis.Provenance.Measured | MeterVault.Core.Analysis.Provenance.Estimated;
Assert.Equal("Measured, Estimated", WithUiCulture("en", () => provenance.Display()));
Assert.Equal("Gemessen, Geschätzt", WithUiCulture("de", () => provenance.Display()));
Assert.Equal(string.Empty, MeterVault.Core.Analysis.Provenance.None.Display());
}
[Fact]
public void Every_meter_role_has_a_one_line_meaning_in_every_language()
{
// D-21: the editor shows a role's name and what it means, never the raw token.
foreach (var role in Enum.GetValues())
{
foreach (var culture in Loc.SupportedCultures)
{
var name = WithUiCulture(culture, () => role.Display());
var meaning = WithUiCulture(culture, () => role.Meaning());
Assert.False(string.IsNullOrWhiteSpace(meaning), $"{role} [{culture}]");
Assert.NotEqual(name, meaning);
Assert.DoesNotContain("_", name, StringComparison.Ordinal);
}
}
}
[Fact]
public void An_undeclared_enum_value_degrades_to_its_identifier_instead_of_throwing()
{
// Guards the fallback arm itself: a value cast in from the database (or a future migration)
// must not take a dashboard down.
var unknown = (MeterMode)999;
Assert.Equal("999", unknown.Display());
}
private static T WithUiCulture(string culture, Func body)
{
var previous = CultureInfo.CurrentUICulture;
try
{
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(culture);
return body();
}
finally
{
CultureInfo.CurrentUICulture = previous;
}
}
}