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;

/// <summary>
///     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 <c>CpuType</c>, Modbus
///     <c>Family</c>, AbCip <c>PlcFamily</c>) as JSON <em>numbers</em> when its private static
///     <c>_jsonOpts</c> had no <see cref="JsonStringEnumConverter"/>. The driver factories, however,
///     deserialise into string-typed DTOs (+ lenient <c>ParseEnum</c>) and <em>throw</em> when binding a
///     JSON number to a <c>string?</c> — 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).
///     <para>v3 Batch-2 (WP3): the config serializer moved from the routed <c>*DriverPage</c> into the
///     embeddable <c>*DriverForm</c> body (shared by the page + the raw-tree DriverConfigModal), so this
///     guard now reflects over the <c>*DriverForm</c> fleet. It fails if any driver form loses its
///     string-enum converter again.</para>
/// </summary>
public sealed class DriverPageJsonConverterTests
{
    /// <summary>
    ///     Driver forms whose config serializer does NOT live in a private static <c>_jsonOpts</c> 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 <em>where</em> the options live, never <em>whether</em> they are
    ///     checked.
    ///     <para><c>MqttDriverForm</c>: the MQTT driver deliberately keeps <b>one</b>
    ///     <see cref="JsonSerializerOptions"/> for all five of its config seams (runtime factory,
    ///     Test-connect probe, driver re-parse, address-picker browser, this form) in
    ///     <c>MqttJson.Options</c> — 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
    ///     <c>MqttDriverFormModel</c>, which serialises through that shared instance.</para>
    /// </summary>
    private static IReadOnlyDictionary<Type, JsonSerializerOptions> ExternalConfigSerializers { get; } =
        new Dictionary<Type, JsonSerializerOptions>
        {
            [typeof(MqttDriverForm)] = MqttJson.Options,
        };

    /// <summary>Every concrete <c>*DriverForm</c> in the AdminUI assembly.</summary>
    private static IReadOnlyList<Type> DriverFormTypes { get; } =
        typeof(ModbusDriverForm).Assembly.GetTypes()
            .Where(t => t.Name.EndsWith("DriverForm", StringComparison.Ordinal) && !t.IsAbstract)
            .OrderBy(t => t.Name)
            .ToList();

    /// <summary>Resolves a form's config serializer — its own <c>_jsonOpts</c> field, else the
    /// explicitly-registered external instance — or <c>null</c> when it has neither.</summary>
    /// <param name="formType">A driver form component type.</param>
    /// <returns>The options the form serialises config through, or <c>null</c>.</returns>
    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);
    }

    /// <summary>xUnit theory source over the driver-form types discovered by reflection.</summary>
    public static TheoryData<Type> DriverFormsWithJsonOpts()
    {
        var data = new TheoryData<Type>();
        foreach (var t in DriverFormTypes)
            data.Add(t);
        return data;
    }

    /// <summary>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).</summary>
    /// <param name="formType">A driver form component type discovered by reflection.</param>
    [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<JsonStringEnumConverter>().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.");
    }

    /// <summary>Enforces that EVERY concrete <c>*DriverForm</c> 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.</summary>
    [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");
    }
}
