using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

public sealed class DriverTypeRegistryTests
{
    private static DriverTypeMetadata SampleMetadata(
        string typeName = "Modbus",
        DriverTier tier = DriverTier.B) =>
        new(typeName,
            DriverConfigJsonSchema: "{\"type\": \"object\"}",
            DeviceConfigJsonSchema: "{\"type\": \"object\"}",
            TagConfigJsonSchema: "{\"type\": \"object\"}",
            Tier: tier);

    /// <summary>
    /// Verifies that Register followed by Get round-trips metadata correctly.
    /// </summary>
    [Fact]
    public void Register_ThenGet_RoundTrips()
    {
        var registry = new DriverTypeRegistry();
        var metadata = SampleMetadata();

        registry.Register(metadata);

        registry.Get("Modbus").ShouldBe(metadata);
    }

    /// <summary>
    /// Verifies that Register accepts and preserves non-null tier values.
    /// </summary>
    /// <param name="tier">The driver tier to test.</param>
    [Theory]
    [InlineData(DriverTier.A)]
    [InlineData(DriverTier.B)]
    [InlineData(DriverTier.C)]
    public void Register_Requires_NonNullTier(DriverTier tier)
    {
        var registry = new DriverTypeRegistry();
        var metadata = SampleMetadata(typeName: $"Driver-{tier}", tier: tier);

        registry.Register(metadata);

        registry.Get(metadata.TypeName).Tier.ShouldBe(tier);
    }

    /// <summary>
    /// Verifies that Get is case-insensitive when looking up driver types.
    /// </summary>
    [Fact]
    public void Get_IsCaseInsensitive()
    {
        var registry = new DriverTypeRegistry();
        registry.Register(SampleMetadata("Galaxy"));

        registry.Get("galaxy").ShouldNotBeNull();
        registry.Get("GALAXY").ShouldNotBeNull();
    }

    /// <summary>
    /// Verifies that Get throws when looking up an unknown type.
    /// </summary>
    [Fact]
    public void Get_UnknownType_Throws()
    {
        var registry = new DriverTypeRegistry();
        registry.Register(SampleMetadata("Modbus"));

        Should.Throw<KeyNotFoundException>(() => registry.Get("UnregisteredType"));
    }

    /// <summary>
    /// Verifies that TryGet returns null when looking up an unknown type.
    /// </summary>
    [Fact]
    public void TryGet_UnknownType_ReturnsNull()
    {
        var registry = new DriverTypeRegistry();
        registry.Register(SampleMetadata("Modbus"));

        registry.TryGet("UnregisteredType").ShouldBeNull();
    }

    /// <summary>
    /// Verifies that Register throws when attempting to register a duplicate type.
    /// </summary>
    [Fact]
    public void Register_DuplicateType_Throws()
    {
        var registry = new DriverTypeRegistry();
        registry.Register(SampleMetadata("Modbus"));

        Should.Throw<InvalidOperationException>(() => registry.Register(SampleMetadata("Modbus")));
    }

    /// <summary>
    /// Verifies that duplicate type detection is case-insensitive.
    /// </summary>
    [Fact]
    public void Register_DuplicateTypeIsCaseInsensitive()
    {
        var registry = new DriverTypeRegistry();
        registry.Register(SampleMetadata("Modbus"));

        Should.Throw<InvalidOperationException>(() => registry.Register(SampleMetadata("modbus")));
    }

    /// <summary>
    /// Verifies that All returns all registered driver types.
    /// </summary>
    [Fact]
    public void All_ReturnsRegisteredTypes()
    {
        var registry = new DriverTypeRegistry();
        registry.Register(SampleMetadata("Modbus"));
        registry.Register(SampleMetadata("S7"));
        registry.Register(SampleMetadata("Galaxy"));

        var all = registry.All();

        all.Count.ShouldBe(3);
        all.Select(m => m.TypeName).ShouldBe(new[] { "Modbus", "S7", "Galaxy" }, ignoreOrder: true);
    }

    /// <summary>
    /// Verifies that Get rejects empty or null type names.
    /// </summary>
    /// <param name="typeName">The type name to test (null, empty, or whitespace).</param>
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void Get_RejectsEmptyTypeName(string? typeName)
    {
        var registry = new DriverTypeRegistry();
        Should.Throw<ArgumentException>(() => registry.Get(typeName!));
    }

    /// <summary>
    ///     Core.Abstractions-004: concurrent <see cref="DriverTypeRegistry.Register"/> calls for
    ///     two different driver types must both succeed and both end up visible to readers. A
    ///     non-atomic check-then-act would let the second swap silently discard the first
    ///     registration; this test asserts the "registered only once per process" guarantee
    ///     holds under concurrent writers too.
    /// </summary>
    [Fact]
    public void Register_ConcurrentDistinctTypes_AllSucceed()
    {
        var registry = new DriverTypeRegistry();
        const int parallelism = 32;
        var ready = new ManualResetEventSlim(false);

        var threads = Enumerable.Range(0, parallelism)
            .Select(i => new Thread(() =>
            {
                ready.Wait();
                registry.Register(SampleMetadata($"Driver-{i}"));
            }))
            .ToArray();

        foreach (var t in threads) t.Start();
        ready.Set(); // release all threads simultaneously
        foreach (var t in threads) t.Join();

        var all = registry.All();
        all.Count.ShouldBe(parallelism);
        for (var i = 0; i < parallelism; i++)
            registry.TryGet($"Driver-{i}").ShouldNotBeNull(
                $"Driver-{i} was lost to a race in concurrent Register");
    }

    /// <summary>
    ///     Core.Abstractions-004: concurrent <see cref="DriverTypeRegistry.Register"/> calls for
    ///     the SAME driver type must result in exactly one successful registration and exactly
    ///     (parallelism - 1) <see cref="InvalidOperationException"/> throws — not a silent
    ///     last-writer-wins replacement.
    /// </summary>
    [Fact]
    public void Register_ConcurrentDuplicateType_ExactlyOneWins()
    {
        var registry = new DriverTypeRegistry();
        const int parallelism = 16;
        var ready = new ManualResetEventSlim(false);
        var successes = 0;
        var failures = 0;

        var threads = Enumerable.Range(0, parallelism)
            .Select(_ => new Thread(() =>
            {
                ready.Wait();
                try
                {
                    registry.Register(SampleMetadata("Contended"));
                    Interlocked.Increment(ref successes);
                }
                catch (InvalidOperationException)
                {
                    Interlocked.Increment(ref failures);
                }
            }))
            .ToArray();

        foreach (var t in threads) t.Start();
        ready.Set();
        foreach (var t in threads) t.Join();

        successes.ShouldBe(1);
        failures.ShouldBe(parallelism - 1);
        registry.All().Count.ShouldBe(1);
    }
}
