using System.Reflection;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw;

/// <summary>
///     Guards the <c>/raw</c> "New driver" picker against driver-type drift.
/// </summary>
/// <remarks>
///     <para>
///         <see cref="RawDriverTypeDialog"/> is the <b>only</b> surface an operator can author a
///         <c>/raw</c> driver from, and its option list is a hand-maintained array — nothing tied it to
///         <see cref="DriverTypeNames"/>. The MQTT P1 live gate found the consequence: the driver shipped
///         complete (contracts, driver, browser, factory + host registration, typed tag editor, 266 green
///         unit tests) and was still <b>unauthorable in production</b>, because nobody added the row to
///         this array. Every offline test passed the whole way — the array is not reachable from any of
///         them, and AdminUI has no bUnit.
///     </para>
///     <para>
///         So this asserts parity by reflection rather than re-listing the drivers: a new
///         <see cref="DriverTypeNames"/> constant that nobody surfaces in the picker fails here instead of
///         at someone's live gate. It reads the private static <c>Types</c> field because that array is
///         the component's own source of truth for the rendered <c>&lt;option&gt;</c> set.
///     </para>
/// </remarks>
public sealed class RawDriverTypeDialogParityTests
{
    private static IReadOnlyList<(string Label, string Value)> DialogTypes()
    {
        var field = typeof(RawDriverTypeDialog).GetField(
            "Types",
            BindingFlags.NonPublic | BindingFlags.Static);

        field.ShouldNotBeNull(
            "RawDriverTypeDialog.Types was renamed or removed — this guard reads it by name and " +
            "silently guards nothing once it cannot find it.");

        var raw = (Array)field!.GetValue(null)!;

        return raw.Cast<object>()
            .Select(t =>
            {
                var type = t.GetType();
                return ((string)type.GetField("Item1")!.GetValue(t)!,
                        (string)type.GetField("Item2")!.GetValue(t)!);
            })
            .ToList();
    }

    [Fact]
    public void Every_declared_driver_type_is_offered_by_the_new_driver_picker()
    {
        var offered = DialogTypes().Select(t => t.Value).ToHashSet(StringComparer.Ordinal);

        var missing = DriverTypeNames.All.Where(n => !offered.Contains(n)).ToList();

        missing.ShouldBeEmpty(
            $"These driver types are declared in DriverTypeNames but cannot be authored from the " +
            $"/raw New-driver picker, so they are unreachable in production: {string.Join(", ", missing)}. " +
            "Add a row to RawDriverTypeDialog.Types.");
    }

    [Fact]
    public void The_picker_offers_no_driver_type_that_is_not_declared()
    {
        var declared = DriverTypeNames.All.ToHashSet(StringComparer.Ordinal);

        var unknown = DialogTypes().Select(t => t.Value).Where(v => !declared.Contains(v)).ToList();

        unknown.ShouldBeEmpty(
            $"The /raw New-driver picker offers driver types with no DriverTypeNames constant, so " +
            $"authoring one produces a driver no factory can build: {string.Join(", ", unknown)}.");
    }

    [Fact]
    public void Mqtt_is_authorable_from_the_new_driver_picker()
    {
        // The specific regression the P1 live gate caught, pinned by name so a future refactor of the
        // parity facts above cannot quietly drop MQTT again.
        DialogTypes().Select(t => t.Value).ShouldContain(DriverTypeNames.Mqtt);
    }
}
