using System.Reflection;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;

namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests;

/// <summary>
///     Guards the deploy-time TagConfig gate against the failure mode that produced deferment-register G-7:
///     a driver ships an <c>Inspect</c> on its <c>*TagDefinitionFactory</c>, nobody adds it to
///     <see cref="EquipmentTagConfigInspector"/>, and the gate is silently blind for that driver at deploy
///     while the AdminUI still validates it at authoring time. MQTT sat in exactly that state.
///     <para><b>Why reflection rather than a hand-written list.</b> The sibling
///     <c>EquipmentTagConfigInspectorTests</c> enumerates driver-type strings by hand, so it guards
///     <i>renames</i> but can never notice a <i>gap</i> — a newly-inspectable driver simply never appears in
///     it. This is the same hollow-guard shape found in <c>TagConfigDriverTypeNameGuardTests</c> and
///     <c>CsvColumnMapReflectionTests</c>. Deriving the expected set from the assemblies means the test
///     fails the moment a new inspector exists, without anyone remembering to update it.</para>
/// </summary>
public sealed class EquipmentTagConfigInspectorCoverageTests
{
    /// <summary>
    ///     Every <c>*TagDefinitionFactory</c> reachable from ControlPlane that exposes a public static
    ///     <c>Inspect(string)</c> must be wired into the dispatch map.
    ///     <para>Discovery walks the referenced assemblies rather than <c>AppDomain</c> alone, because a
    ///     Contracts assembly with no other use is not loaded until first touched — an
    ///     <c>AppDomain.GetAssemblies()</c> sweep would quietly find nothing and pass vacuously.</para>
    /// </summary>
    [Fact]
    public void Every_driver_shipping_an_Inspect_is_wired_into_the_dispatch_map()
    {
        var inspectable = DiscoverInspectableFactories();

        // Positive control: the discovery itself must find something, or the assertion below is vacuous —
        // an empty expected-set is satisfied by an empty map, which is the exact bug this guards.
        inspectable.Count.ShouldBeGreaterThan(
            1,
            "the reflection sweep found no (or one) driver factory with an Inspect — discovery is broken, "
            + "and the coverage assertion below would pass no matter what the map contained");

        var mapped = EquipmentTagConfigInspector.MappedDriverTypes;

        var missing = inspectable
            .Where(t => !mapped.Any(m => LooksLike(m, t.Name)))
            .Select(t => t.Name)
            .OrderBy(n => n, StringComparer.Ordinal)
            .ToArray();

        missing.ShouldBeEmpty(
            "these drivers ship an Inspect() but the deploy-time gate does not call it, so their TagConfig "
            + "is checked at authoring time and NOT at deploy — config arriving by CSV import or API is "
            + "unchecked: " + string.Join(", ", missing));
    }

    /// <summary>The map must not name a driver type with no inspector behind it — a dead key would read as
    /// coverage while dispatching nowhere.</summary>
    [Fact]
    public void Every_mapped_driver_type_resolves_to_a_real_inspector()
    {
        foreach (var driverType in EquipmentTagConfigInspector.MappedDriverTypes)
        {
            EquipmentTagConfigInspector.IsMapped(driverType).ShouldBeTrue(driverType);
            // A structurally invalid blob must come back as warnings, not an exception — the gate is on the
            // deploy path and must never throw.
            Should.NotThrow(() => EquipmentTagConfigInspector.Inspect(driverType, "{not json"));
        }
    }

    /// <summary>
    ///     Public static <c>IReadOnlyList&lt;string&gt; Inspect(string)</c> is the contract every driver's
    ///     deploy-time checker implements; anything else is not one of these factories.
    ///     <para><b>Discovery scans the output DIRECTORY, not <c>Assembly.GetReferencedAssemblies()</c>.</b>
    ///     The obvious implementation walks the reference graph — and it is circular, because the C# compiler
    ///     elides an assembly reference nothing in the IL uses. Drop a driver from the dispatch map and its
    ///     Contracts assembly drops out of ControlPlane's manifest too, so the sweep stops looking for
    ///     exactly the driver that just went missing and the test passes. <b>That version was written, and
    ///     caught only by deleting the MQTT entry and watching it stay green.</b> MSBuild copies every
    ///     project-referenced assembly to the output regardless of IL usage, so the directory is the honest
    ///     source.</para>
    ///     <para><b>Boundary, stated so it is not mistaken for more:</b> this sees the Contracts assemblies
    ///     ControlPlane project-references. A driver whose Contracts project is not referenced at all (Sql,
    ///     MTConnect, Calculation, OpcUaClient) is invisible here — adding the reference is itself the act
    ///     that makes this guard start demanding a map entry.</para>
    /// </summary>
    private static IReadOnlyList<Type> DiscoverInspectableFactories() =>
        Directory.GetFiles(AppContext.BaseDirectory, "ZB.MOM.WW.OtOpcUa.Driver.*.Contracts.dll")
            .Select(TryLoad)
            .Where(a => a is not null)
            .SelectMany(a => a!.GetTypes())
            .Where(t => t is { IsClass: true, IsPublic: true }
                        && t.Name.EndsWith("TagDefinitionFactory", StringComparison.Ordinal)
                        && HasInspect(t))
            .DistinctBy(t => t.FullName)
            .ToArray();

    private static Assembly? TryLoad(string path)
    {
        try { return Assembly.LoadFrom(path); }
        catch (BadImageFormatException) { return null; }
    }

    private static bool HasInspect(Type t)
    {
        var m = t.GetMethod("Inspect", BindingFlags.Public | BindingFlags.Static, [typeof(string)]);
        return m is not null && typeof(IReadOnlyList<string>).IsAssignableFrom(m.ReturnType);
    }

    /// <summary>Matches a mapped driver-type string to a factory type name (<c>ModbusTagDefinitionFactory</c>
    /// ↔ <c>Modbus</c>), tolerating the casing differences between the two vocabularies
    /// (<c>FOCAS</c>/<c>Focas</c>, <c>TwinCAT</c>/<c>TwinCAT</c>).</summary>
    private static bool LooksLike(string driverType, string factoryTypeName) =>
        factoryTypeName.StartsWith(driverType, StringComparison.OrdinalIgnoreCase);
}
