using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.PlcFamilies;

namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;

/// <summary>
///     Driver.AbLegacy-011 — synchronous <see cref="IDisposable.Dispose"/> must perform real
///     synchronous teardown rather than blocking via <c>DisposeAsync().AsTask().GetAwaiter().GetResult()</c>.
///     Driver.AbLegacy-013 — <see cref="AbLegacyDriver.ResolveHost"/> fallback when the reference
///     is unknown and no devices are configured is the documented single-host fallback per the
///     <c>IPerCallHostResolver</c> contract.
/// </summary>
[Trait("Category", "Unit")]
public sealed class AbLegacyDisposeAndResolveHostTests
{
    // ---- Driver.AbLegacy-011 ----

    /// <summary>Verifies that Dispose performs teardown without blocking on async operations.</summary>
    [Fact]
    public async Task Dispose_runs_teardown_without_blocking_on_async_wait()
    {
        // Build a driver with a real device + tag so InitializeAsync registers state, then Dispose.
        // The teardown must clear the device dictionary just like ShutdownAsync would, but without
        // round-tripping through AsTask().GetAwaiter().GetResult() (which would deadlock under a
        // single-threaded synchronization context).
        var factory = new FakeAbLegacyTagFactory();
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0", AbLegacyPlcFamily.Slc500)],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-dispose", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        // Materialise one runtime so DisposeRuntimes has work to do.
        await drv.ReadAsync(["X"], CancellationToken.None);
        factory.Tags["N7:0"].Disposed.ShouldBeFalse();

        drv.Dispose();

        // The cached libplctag tag must be disposed and the device map cleared.
        factory.Tags["N7:0"].Disposed.ShouldBeTrue();
        drv.DeviceCount.ShouldBe(0);
        drv.GetHealth().State.ShouldBe(DriverState.Unknown);
    }

    /// <summary>Verifies that Dispose can be called multiple times without throwing.</summary>
    [Fact]
    public async Task Dispose_is_idempotent()
    {
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1");
        await drv.InitializeAsync("{}", CancellationToken.None);
        drv.Dispose();
        Should.NotThrow(() => drv.Dispose());
    }

    /// <summary>Verifies that Dispose does not deadlock under a single-threaded synchronization context.</summary>
    [Fact]
    public async Task Dispose_under_single_threaded_sync_context_does_not_deadlock()
    {
        // The legacy sync-over-async pattern (DisposeAsync().AsTask().GetAwaiter().GetResult())
        // can deadlock if any awaited continuation marshals back to a captured single-threaded
        // context. Drive a single-threaded SynchronizationContext + Dispose() and ensure it
        // returns within a short timeout.
        var factory = new FakeAbLegacyTagFactory();
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
            Probe = new AbLegacyProbeOptions { Enabled = false },
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);
        await drv.ReadAsync(["X"], CancellationToken.None);

        using var ctx = new SingleThreadSynchronizationContext();
        var prior = SynchronizationContext.Current;
        SynchronizationContext.SetSynchronizationContext(ctx);
        try
        {
            var disposed = new ManualResetEventSlim(false);
            ctx.Post(_ => { drv.Dispose(); disposed.Set(); }, null);
            ctx.RunUntil(disposed);
            disposed.IsSet.ShouldBeTrue("Dispose must return without blocking on the single-threaded context");
        }
        finally
        {
            SynchronizationContext.SetSynchronizationContext(prior);
        }
    }

    /// <summary>
    ///     Minimal cooperative single-threaded SynchronizationContext for the deadlock-regression
    ///     test. The thread that calls <see cref="RunUntil"/> pumps queued callbacks until the
    ///     stop event is set.
    /// </summary>
    private sealed class SingleThreadSynchronizationContext : SynchronizationContext, IDisposable
    {
        private readonly System.Collections.Concurrent.BlockingCollection<(SendOrPostCallback, object?)> _queue = new();

        /// <inheritdoc />
        public override void Post(SendOrPostCallback d, object? state) => _queue.Add((d, state));

        /// <inheritdoc />
        public override void Send(SendOrPostCallback d, object? state) => d(state);

        /// <summary>Runs the event loop until the stop signal is set.</summary>
        /// <param name="stop">The event to signal loop completion.</param>
        public void RunUntil(ManualResetEventSlim stop)
        {
            while (!stop.IsSet)
            {
                if (_queue.TryTake(out var item, TimeSpan.FromSeconds(2)))
                    item.Item1(item.Item2);
                else
                    throw new TimeoutException("Dispose did not complete — likely sync-over-async deadlock");
            }
        }

        /// <summary>Disposes the internal queue.</summary>
        public void Dispose() => _queue.Dispose();
    }

    // ---- Driver.AbLegacy-013 ----

    /// <summary>Verifies that ResolveHost returns the configured device for a known tag reference.</summary>
    [Fact]
    public void ResolveHost_known_reference_returns_tag_device()
    {
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
            RawTags = AbLegacyRawTags.Entries([new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)]),
        }, "drv-1");
        drv.ResolveHost("X").ShouldBe("ab://10.0.0.5/1,0");
    }

    /// <summary>Verifies that ResolveHost returns the first configured device when reference is unknown.</summary>
    [Fact]
    public void ResolveHost_unknown_reference_with_devices_returns_first_device()
    {
        // Multi-device fallback: an unknown reference returns the first configured device so the
        // resilience pipeline keys on a real ab:// host rather than the instance id.
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions
        {
            Devices =
            [
                new AbLegacyDeviceOptions("ab://10.0.0.5/1,0"),
                new AbLegacyDeviceOptions("ab://10.0.0.6/"),
            ],
        }, "drv-1");
        drv.ResolveHost("unknown").ShouldBe("ab://10.0.0.5/1,0");
    }

    /// <summary>Verifies that ResolveHost returns the driver instance ID when no devices are configured.</summary>
    [Fact]
    public void ResolveHost_unknown_reference_no_devices_returns_driver_instance_id()
    {
        // Per IPerCallHostResolver: implementations MUST NOT throw on an unknown reference; they
        // must return the driver's default-host string. With no devices configured the driver
        // instance id is the documented single-host fallback.
        var drv = new AbLegacyDriver(new AbLegacyDriverOptions(), "drv-singleton");
        drv.ResolveHost("anything").ShouldBe("drv-singleton");
    }
}
