using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Forms;
using ZB.MOM.WW.OtOpcUa.Driver.Mqtt;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
///
/// Regression guard for the systemic driver-config enum-serialization bug found 2026-06-19.
/// Every driver-config editor serialised enum config fields (e.g. S7 CpuType, Modbus
/// Family, AbCip PlcFamily) as JSON numbers when its private static
/// _jsonOpts had no . The driver factories, however,
/// deserialise into string-typed DTOs (+ lenient ParseEnum) and throw when binding a
/// JSON number to a string? — so an AdminUI-authored config that contained any enum field
/// produced a blob the driver could not parse, faulting the driver on deploy. The fix makes every
/// editor serialise enums as strings (matching the factory + the long-correct OpcUaClient template).
/// v3 Batch-2 (WP3): the config serializer moved from the routed *DriverPage into the
/// embeddable *DriverForm body (shared by the page + the raw-tree DriverConfigModal), so this
/// guard now reflects over the *DriverForm fleet. It fails if any driver form loses its
/// string-enum converter again.
///
public sealed class DriverPageJsonConverterTests
{
///
/// Driver forms whose config serializer does NOT live in a private static _jsonOpts field
/// on the form, mapped to the options instance they actually use. Each entry is an explicit,
/// reviewed exception — the guard below still asserts the converter on whatever instance is
/// named here, so this narrows where the options live, never whether they are
/// checked.
/// MqttDriverForm: the MQTT driver deliberately keeps one
/// for all five of its config seams (runtime factory,
/// Test-connect probe, driver re-parse, address-picker browser, this form) in
/// MqttJson.Options — a per-form copy would be a fifth divergent instance, i.e. exactly
/// the bug this whole file guards. The form itself is a shell over the pure
/// MqttDriverFormModel, which serialises through that shared instance.
///
private static IReadOnlyDictionary ExternalConfigSerializers { get; } =
new Dictionary
{
[typeof(MqttDriverForm)] = MqttJson.Options,
};
/// Every concrete *DriverForm in the AdminUI assembly.
private static IReadOnlyList DriverFormTypes { get; } =
typeof(ModbusDriverForm).Assembly.GetTypes()
.Where(t => t.Name.EndsWith("DriverForm", StringComparison.Ordinal) && !t.IsAbstract)
.OrderBy(t => t.Name)
.ToList();
/// Resolves a form's config serializer — its own _jsonOpts field, else the
/// explicitly-registered external instance — or null when it has neither.
/// A driver form component type.
/// The options the form serialises config through, or null.
private static JsonSerializerOptions? ResolveConfigSerializer(Type formType)
{
if (formType.GetField("_jsonOpts", BindingFlags.NonPublic | BindingFlags.Static) is { } field)
{
return (JsonSerializerOptions?)field.GetValue(null);
}
return ExternalConfigSerializers.GetValueOrDefault(formType);
}
/// xUnit theory source over the driver-form types discovered by reflection.
public static TheoryData DriverFormsWithJsonOpts()
{
var data = new TheoryData();
foreach (var t in DriverFormTypes)
data.Add(t);
return data;
}
/// Verifies every driver form's config serializer registers a string-enum converter so
/// enum config fields round-trip as strings (and the driver factory can parse the result).
/// A driver form component type discovered by reflection.
[Theory]
[MemberData(nameof(DriverFormsWithJsonOpts))]
public void Driver_form_json_options_register_string_enum_converter(Type formType)
{
var opts = ResolveConfigSerializer(formType);
opts.ShouldNotBeNull(
$"{formType.Name} must serialise config through a private static _jsonOpts field, or be listed in " +
"ExternalConfigSerializers with the shared options instance it uses — otherwise its enum handling " +
"is unguarded.");
opts.Converters.OfType().ShouldNotBeEmpty(
$"{formType.Name}'s config serializer must register a JsonStringEnumConverter; otherwise " +
"AdminUI-authored enum config fields serialise as numbers and the string-typed driver factory " +
"throws on parse.");
}
/// Enforces that EVERY concrete *DriverForm routes config serialization through a
/// serializer the guard above can reach — otherwise a new form that serialised config a different way
/// would slip past it. Also a floor check so a rename can't silently shrink the set.
[Fact]
public void Every_driver_form_uses_a_guarded_jsonOpts_serializer()
{
DriverFormTypes.Count.ShouldBeGreaterThanOrEqualTo(9, "reflection should discover the full driver-form fleet");
var unguarded = DriverFormTypes.Where(t => ResolveConfigSerializer(t) is null).Select(t => t.Name).ToList();
unguarded.ShouldBeEmpty(
"every *DriverForm must expose its config serializer (a _jsonOpts field, or an ExternalConfigSerializers " +
"entry) so the string-enum converter guard covers it");
}
}