using System.Collections.Concurrent;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;

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

[Trait("Category", "Unit")]
public sealed class TwinCATNativeNotificationTests
{
    private static (TwinCATDriver drv, FakeTwinCATClientFactory factory) NewNativeDriver(params TwinCATTagDefinition[] tags)
    {
        var factory = new FakeTwinCATClientFactory();
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            RawTags = TwinCATRawTags.Entries(tags),
            Probe = new TwinCATProbeOptions { Enabled = false },
            UseNativeNotifications = true,
        }, "drv-1", factory);
        return (drv, factory);
    }

    /// <summary>Verifies that native subscribe registers one notification per tag.</summary>
    [Fact]
    public async Task Native_subscribe_registers_one_notification_per_tag()
    {
        var (drv, factory) = NewNativeDriver(
            new TwinCATTagDefinition("A", "ads://5.23.91.23.1.1:851", "MAIN.A", TwinCATDataType.DInt),
            new TwinCATTagDefinition("B", "ads://5.23.91.23.1.1:851", "MAIN.B", TwinCATDataType.Real));
        await drv.InitializeAsync("{}", CancellationToken.None);

        var handle = await drv.SubscribeAsync(["A", "B"], TimeSpan.FromMilliseconds(100), CancellationToken.None);
        handle.DiagnosticId.ShouldStartWith("twincat-native-sub-");

        factory.Clients[0].Notifications.Count.ShouldBe(2);
        factory.Clients[0].Notifications.Select(n => n.SymbolPath).ShouldBe(["MAIN.A", "MAIN.B"], ignoreOrder: true);
    }

    /// <summary>Verifies that native notification fires OnDataChange with pushed value.</summary>
    [Fact]
    public async Task Native_notification_fires_OnDataChange_with_pushed_value()
    {
        var (drv, factory) = NewNativeDriver(
            new TwinCATTagDefinition("Speed", "ads://5.23.91.23.1.1:851", "MAIN.Speed", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);

        var events = new ConcurrentQueue<DataChangeEventArgs>();
        drv.OnDataChange += (_, e) => events.Enqueue(e);

        _ = await drv.SubscribeAsync(["Speed"], TimeSpan.FromMilliseconds(100), CancellationToken.None);

        factory.Clients[0].FireNotification("MAIN.Speed", 4200);
        factory.Clients[0].FireNotification("MAIN.Speed", 4201);

        events.Count.ShouldBe(2);
        events.Last().Snapshot.Value.ShouldBe(4201);
        events.Last().FullReference.ShouldBe("Speed"); // driver-side reference, not ADS symbol
    }

    /// <summary>Verifies that native unsubscribe disposes all notifications.</summary>
    [Fact]
    public async Task Native_unsubscribe_disposes_all_notifications()
    {
        var (drv, factory) = NewNativeDriver(
            new TwinCATTagDefinition("A", "ads://5.23.91.23.1.1:851", "MAIN.A", TwinCATDataType.DInt),
            new TwinCATTagDefinition("B", "ads://5.23.91.23.1.1:851", "MAIN.B", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);

        var handle = await drv.SubscribeAsync(["A", "B"], TimeSpan.FromMilliseconds(100), CancellationToken.None);
        factory.Clients[0].Notifications.Count.ShouldBe(2);

        await drv.UnsubscribeAsync(handle, CancellationToken.None);
        factory.Clients[0].Notifications.ShouldBeEmpty();
    }

    /// <summary>Verifies that native unsubscribe halts future notifications.</summary>
    [Fact]
    public async Task Native_unsubscribe_halts_future_notifications()
    {
        var (drv, factory) = NewNativeDriver(
            new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);

        var events = new ConcurrentQueue<DataChangeEventArgs>();
        drv.OnDataChange += (_, e) => events.Enqueue(e);

        var handle = await drv.SubscribeAsync(["X"], TimeSpan.FromMilliseconds(100), CancellationToken.None);
        factory.Clients[0].FireNotification("MAIN.X", 1);
        var snapshotFake = factory.Clients[0];
        await drv.UnsubscribeAsync(handle, CancellationToken.None);

        var afterUnsub = events.Count;
        // After unsubscribe the fake's Notifications list is empty so FireNotification finds nothing
        // to invoke. This mirrors the production contract — disposed handles no longer deliver.
        snapshotFake.FireNotification("MAIN.X", 999);
        events.Count.ShouldBe(afterUnsub);
    }

    /// <summary>Verifies that native subscribe failure mid-registration cleans up partial state.</summary>
    [Fact]
    public async Task Native_subscribe_failure_mid_registration_cleans_up_partial_state()
    {
        // Fail-on-second-call fake — first AddNotificationAsync succeeds, second throws.
        // Subscribe's catch block must tear the first one down before rethrowing so no zombie
        // notification lingers.
        var fake = new FailAfterNAddsFake(new AbTagParamsIrrelevant(), succeedBefore: 1);
        var factory = new FakeTwinCATClientFactory { Customise = () => fake };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            RawTags = TwinCATRawTags.Entries(
            [
                new TwinCATTagDefinition("A", "ads://5.23.91.23.1.1:851", "MAIN.A", TwinCATDataType.DInt),
                new TwinCATTagDefinition("B", "ads://5.23.91.23.1.1:851", "MAIN.B", TwinCATDataType.DInt),
            ]),
            Probe = new TwinCATProbeOptions { Enabled = false },
            UseNativeNotifications = true,
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        await Should.ThrowAsync<InvalidOperationException>(() =>
            drv.SubscribeAsync(["A", "B"], TimeSpan.FromMilliseconds(100), CancellationToken.None));

        // First registration succeeded then got torn down by the catch; second threw.
        fake.AddCallCount.ShouldBe(2);
        fake.Notifications.Count.ShouldBe(0); // partial handle cleaned up
    }

    private sealed class AbTagParamsIrrelevant { }

    private sealed class FailAfterNAddsFake : FakeTwinCATClient
    {
        private readonly int _succeedBefore;
        /// <summary>Gets the number of times AddNotificationAsync has been called.</summary>
        public int AddCallCount { get; private set; }

        /// <summary>Initializes a new instance of FailAfterNAddsFake.</summary>
        /// <param name="_">Unused parameter for alignment with parent constructor.</param>
        /// <param name="succeedBefore">The number of calls to succeed before failing.</param>
        public FailAfterNAddsFake(AbTagParamsIrrelevant _, int succeedBefore) : base()
        {
            _succeedBefore = succeedBefore;
        }

        /// <inheritdoc />
        public override Task<ITwinCATNotificationHandle> AddNotificationAsync(
            string symbolPath, TwinCATDataType type, int? bitIndex, TimeSpan cycleTime,
            int maxDelayMs, Action<string, object?> onChange, CancellationToken cancellationToken)
        {
            AddCallCount++;
            if (AddCallCount > _succeedBefore)
                throw new InvalidOperationException($"fake fail on call #{AddCallCount}");
            return base.AddNotificationAsync(symbolPath, type, bitIndex, cycleTime, maxDelayMs, onChange, cancellationToken);
        }
    }

    /// <summary>Verifies that native shutdown disposes subscriptions.</summary>
    [Fact]
    public async Task Native_shutdown_disposes_subscriptions()
    {
        var (drv, factory) = NewNativeDriver(
            new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
        await drv.InitializeAsync("{}", CancellationToken.None);

        _ = await drv.SubscribeAsync(["X"], TimeSpan.FromMilliseconds(100), CancellationToken.None);
        factory.Clients[0].Notifications.Count.ShouldBe(1);

        await drv.ShutdownAsync(CancellationToken.None);
        factory.Clients[0].Notifications.ShouldBeEmpty();
    }

    /// <summary>Verifies that the poll path still works when UseNativeNotifications is false.</summary>
    [Fact]
    public async Task Poll_path_still_works_when_UseNativeNotifications_false()
    {
        var factory = new FakeTwinCATClientFactory
        {
            Customise = () => new FakeTwinCATClient { Values = { ["MAIN.X"] = 7 } },
        };
        var drv = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt)]),
            Probe = new TwinCATProbeOptions { Enabled = false },
            UseNativeNotifications = false,
        }, "drv-1", factory);
        await drv.InitializeAsync("{}", CancellationToken.None);

        var events = new ConcurrentQueue<DataChangeEventArgs>();
        drv.OnDataChange += (_, e) => events.Enqueue(e);

        var handle = await drv.SubscribeAsync(["X"], TimeSpan.FromMilliseconds(150), CancellationToken.None);
        await WaitForAsync(() => events.Count >= 1, TimeSpan.FromSeconds(2));

        events.First().Snapshot.Value.ShouldBe(7);
        factory.Clients[0].Notifications.ShouldBeEmpty(); // no native notifications on poll path
        await drv.UnsubscribeAsync(handle, CancellationToken.None);
    }

    /// <summary>Verifies that subscribe handle DiagnosticId indicates native vs poll.</summary>
    [Fact]
    public async Task Subscribe_handle_DiagnosticId_indicates_native_vs_poll()
    {
        var (drvNative, _) = NewNativeDriver(
            new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt));
        await drvNative.InitializeAsync("{}", CancellationToken.None);
        var nativeHandle = await drvNative.SubscribeAsync(["X"], TimeSpan.FromMilliseconds(100), CancellationToken.None);
        nativeHandle.DiagnosticId.ShouldContain("native");

        var factoryPoll = new FakeTwinCATClientFactory
        {
            Customise = () => new FakeTwinCATClient { Values = { ["MAIN.X"] = 1 } },
        };
        var drvPoll = new TwinCATDriver(new TwinCATDriverOptions
        {
            Devices = [new TwinCATDeviceOptions("ads://5.23.91.23.1.1:851")],
            RawTags = TwinCATRawTags.Entries([new TwinCATTagDefinition("X", "ads://5.23.91.23.1.1:851", "MAIN.X", TwinCATDataType.DInt)]),
            Probe = new TwinCATProbeOptions { Enabled = false },
            UseNativeNotifications = false,
        }, "drv-1", factoryPoll);
        await drvPoll.InitializeAsync("{}", CancellationToken.None);
        var pollHandle = await drvPoll.SubscribeAsync(["X"], TimeSpan.FromMilliseconds(100), CancellationToken.None);
        pollHandle.DiagnosticId.ShouldNotContain("native");
    }

    private static async Task WaitForAsync(Func<bool> condition, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (!condition() && DateTime.UtcNow < deadline)
            await Task.Delay(20);
    }
}
