using System.Reflection;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.Secrets.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;

/// <summary>
///     Guards <see cref="DriverTypeNames"/> against drift from the driver factories that are the
///     authority for these strings. Factory discovery is <b>reflection-driven, not a hand-copied list</b>:
///     the test scans the driver assemblies deployed to its output directory for the
///     <c>*DriverFactoryExtensions.Register(DriverFactoryRegistry, …)</c> convention and invokes each into
///     a live <see cref="DriverFactoryRegistry"/> — the same registration path production takes — then
///     asserts bidirectional parity with the constants. Because discovery is by convention, a brand-new
///     driver assembly referenced by this project (and thus copied to bin) is picked up automatically: if
///     it registers a <c>DriverType</c> string with no matching <see cref="DriverTypeNames"/> constant (or
///     a constant is added/removed without a matching factory), this fails, forcing the two back in sync.
/// </summary>
public sealed class DriverTypeNamesGuardTests
{
    // Driver-adjacent assemblies that ship no runtime factory (contracts/addressing/CLI helpers).
    private static readonly string[] NonFactoryDriverAssemblySuffixes =
        { ".Contracts", ".Addressing", ".Cli" };

    /// <summary>
    ///     A stand-in <see cref="ISecretResolver"/> for factories that require one.
    /// </summary>
    /// <remarks>
    ///     Every secret resolves to absent. That is deliberate and safe here: the test only needs the
    ///     type-name key each <c>Register</c> call installs, and never invokes the registered factory
    ///     func, so no secret is ever requested. Resolving to absent rather than to a fake value means
    ///     that if this assumption is ever broken, the driver fails closed.
    /// </remarks>
    private sealed class AbsentSecretResolver : ISecretResolver
    {
        public static readonly AbsentSecretResolver Instance = new();

        public Task<string?> GetAsync(SecretName name, CancellationToken ct) =>
            Task.FromResult<string?>(null);
    }

    /// <summary>
    ///     Build the authoritative registered-factory set by discovering every
    ///     <c>*DriverFactoryExtensions.Register</c> in the deployed driver assemblies (by convention) and
    ///     invoking it against a fresh registry. The factory func is never invoked — only the type-name
    ///     key each <c>Register</c> call installs is needed.
    /// </summary>
    /// <remarks>
    ///     Argument construction is by parameter <i>type</i>, not by position. It used to pass
    ///     <see langword="null"/> for everything after the registry on the assumption that the
    ///     remaining parameters were all optional. They are not: <c>GalaxyDriverFactoryExtensions.Register</c>
    ///     takes a <b>required</b> <see cref="ISecretResolver"/> and guards it with
    ///     <c>ArgumentNullException.ThrowIfNull</c>, so the reflective call threw and all three parity
    ///     facts failed — for a reason that had nothing to do with the drift they exist to catch.
    ///     (<c>OpcUaClient</c>'s equivalent parameter is optional, which is why only Galaxy tripped it.)
    ///     A parameter this method cannot satisfy now fails with a message naming the factory and the
    ///     parameter, rather than surfacing as an opaque <see cref="ArgumentNullException"/>.
    /// </remarks>
    private static IReadOnlyCollection<string> RegisteredDriverTypeNames()
    {
        var registry = new DriverFactoryRegistry();
        var invoked = 0;

        var binDir = Path.GetDirectoryName(typeof(DriverTypeNamesGuardTests).Assembly.Location)!;
        foreach (var dll in Directory.GetFiles(binDir, "ZB.MOM.WW.OtOpcUa.Driver.*.dll"))
        {
            var name = Path.GetFileNameWithoutExtension(dll);
            if (NonFactoryDriverAssemblySuffixes.Any(s => name.EndsWith(s, StringComparison.Ordinal)))
                continue;

            Assembly asm;
            try { asm = Assembly.LoadFrom(dll); }
            catch { continue; }

            Type[] types;
            try { types = asm.GetTypes(); }
            catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t is not null).ToArray()!; }

            foreach (var type in types)
            {
                if (type is null || !type.IsClass || type.Name is not { } n
                    || !n.EndsWith("DriverFactoryExtensions", StringComparison.Ordinal))
                    continue;

                var register = type.GetMethod("Register", BindingFlags.Public | BindingFlags.Static);
                var ps = register?.GetParameters();
                if (register is null || ps is null || ps.Length == 0
                    || ps[0].ParameterType != typeof(DriverFactoryRegistry))
                    continue;

                var args = ps.Select(p => ArgumentFor(p, registry, type.Name)).ToArray();
                register.Invoke(null, args);
                invoked++;
            }
        }

        // Discovery must find *something*; a zero here means the bin scan/convention broke, not that the
        // fleet has no drivers — fail loudly rather than pass a vacuously-empty parity check.
        invoked.ShouldBeGreaterThan(0,
            "no *DriverFactoryExtensions.Register was discovered in the output directory — the driver " +
            "assemblies did not deploy to bin, or the naming convention changed");

        return registry.RegisteredTypes;
    }

    /// <summary>
    ///     Supplies one argument for a discovered <c>Register</c> parameter, by type.
    /// </summary>
    /// <param name="parameter">The parameter to supply.</param>
    /// <param name="registry">The registry the factories register into.</param>
    /// <param name="factoryTypeName">The declaring factory type, for the failure message.</param>
    /// <returns>The argument value to pass.</returns>
    private static object? ArgumentFor(ParameterInfo parameter, DriverFactoryRegistry registry, string factoryTypeName)
    {
        if (parameter.ParameterType == typeof(DriverFactoryRegistry)) return registry;
        if (parameter.ParameterType == typeof(ISecretResolver)) return AbsentSecretResolver.Instance;

        // Optional or nullable-reference parameters are safe to omit; the factory is documented to
        // cope. Anything else is a required dependency this method does not know how to build, and
        // guessing null would resurface as an opaque ArgumentNullException from inside the factory.
        if (parameter.IsOptional || !parameter.ParameterType.IsValueType) return null;

        throw new InvalidOperationException(
            $"{factoryTypeName}.Register requires parameter '{parameter.Name}' of type "
            + $"{parameter.ParameterType.Name}, which the DriverTypeNames guard does not know how to "
            + "supply. Add a case to ArgumentFor, or give the parameter a default on the factory.");
    }

    /// <summary>Reflect every <c>public const string</c> declared on <see cref="DriverTypeNames"/>.</summary>
    private static IReadOnlyCollection<string> DeclaredConstantValues() =>
        typeof(DriverTypeNames)
            .GetFields(BindingFlags.Public | BindingFlags.Static)
            .Where(f => f.IsLiteral && !f.IsInitOnly && f.FieldType == typeof(string))
            .Select(f => (string)f.GetRawConstantValue()!)
            .ToArray();

    [Fact]
    public void Every_registered_factory_has_a_matching_constant()
    {
        var constants = DeclaredConstantValues();

        foreach (var registered in RegisteredDriverTypeNames())
        {
            constants.ShouldContain(registered,
                $"driver factory registers '{registered}' but no DriverTypeNames constant has that value — " +
                "add/rename the constant so dispatch maps can reference it instead of a drifting literal");
        }
    }

    [Fact]
    public void Every_constant_matches_a_registered_factory()
    {
        var registered = RegisteredDriverTypeNames();

        foreach (var constant in DeclaredConstantValues())
        {
            registered.ShouldContain(constant,
                $"DriverTypeNames has '{constant}' but no registered driver factory uses it — " +
                "remove the constant or wire the factory (do not add constants for unregistered drivers)");
        }
    }

    [Fact]
    public void Constants_and_registered_factories_are_exactly_the_same_set()
    {
        DeclaredConstantValues()
            .OrderBy(s => s, StringComparer.Ordinal)
            .ShouldBe(
                RegisteredDriverTypeNames().OrderBy(s => s, StringComparer.Ordinal),
                "DriverTypeNames must be exactly the set of registered driver-type strings — no drift, no extras, no gaps");
    }

    [Fact]
    public void All_collection_equals_the_declared_constants()
    {
        // Keeps the convenience `All` collection honest against the individual consts.
        DriverTypeNames.All
            .OrderBy(s => s, StringComparer.Ordinal)
            .ShouldBe(
                DeclaredConstantValues().OrderBy(s => s, StringComparer.Ordinal),
                "DriverTypeNames.All must list every declared constant exactly once");
    }
}
