using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Client;
using Shouldly;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;

/// <summary>
///     v3 Batch 4 (B4-WP4) — the HEADLINE over-the-wire proof for the multi-notifier native alarm (Wave C
///     review M1): a SINGLE native transition on a condition wired to MULTIPLE notifier roots (its raw
///     device-folder parent + N referencing equipment folders) delivers <b>exactly one</b> event copy to a
///     Server-object event subscriber — the Part 9 shared-<c>InstanceStateSnapshot</c> dedup
///     (<c>MonitoredItem.QueueEvent</c> → <c>IsEventContainedInQueue</c>), NOT one copy per root. This is the
///     regression guard the design's "duplicate-report fallback rejected" decision rests on, and it is exactly
///     the multi-root topology (2 equipment folders → 2 notifier paths up to Server) where a per-root
///     re-report would surface as double delivery.
///
///     <para>Boots the real <see cref="OtOpcUaSdkServer"/> exactly as <c>SubscriptionSurvivalTests</c> does,
///     materialises the raw condition + two referencing equipment folders + the notifier wiring through the
///     production <see cref="SdkAddressSpaceSink"/>, opens a real client subscription with event monitored
///     items (captured via the subscription <see cref="Subscription.FastEventCallback"/>, keyed by
///     <c>ClientHandle</c>), fires ONE transition via <see cref="SdkAddressSpaceSink.WriteAlarmCondition"/>,
///     and counts the delivered events. Heavy in-process server+client integration — runs in the serial
///     integration pass, not the lightweight unit sweep.</para>
/// </summary>
public sealed class NativeAlarmMultiNotifierEventDeliveryTests
{
    private const string ServerUri = "urn:OtOpcUa.MultiNotifierEventDelivery";

    // The raw device-oriented topology (ns=Raw) + two referencing equipment folders (ns=UNS).
    private const string RawDeviceFolder = "Plant/Modbus/dev1";
    private const string RawAlarmPath = "Plant/Modbus/dev1/temp_hi";
    private const string Equip1 = "EQ-filling-line1-station1";
    private const string Equip2 = "EQ-filling-line2-station2";
    private const string AlarmMessage = "over-temperature (multi-notifier test)";

    // Index of the Message select clause in the event filter (see BuildEventFilter): [0]=EventId, [1]=Message.
    private const int MessageFieldIndex = 1;

    /// <summary>One native transition on a condition wired to two equipment folders delivers EXACTLY ONE event
    /// to a Server-object subscriber (not one per notifier root) — the shared-snapshot queue dedup.</summary>
    [Fact]
    public async Task Single_transition_delivers_exactly_one_event_at_the_Server_object()
    {
        var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multinotif-evt-{Guid.NewGuid():N}");
        var port = AllocateFreePort();
        var ct = TestContext.Current.CancellationToken;
        try
        {
            var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri, ct);
            await using var _ = host;

            var sink = new SdkAddressSpaceSink(server.NodeManager!);
            WireSingleConditionToTwoEquipment(sink);

            using var session = await OpenSessionAsync($"opc.tcp://127.0.0.1:{port}/OtOpcUa", ct);
            var collector = new EventCollector();
            var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = 100 };
            subscription.FastEventCallback = collector.OnEvents;
            session.AddSubscription(subscription);
            await subscription.CreateAsync(ct);

            var serverItem = AddEventItem(subscription, ObjectIds.Server);
            await subscription.ApplyChangesAsync(ct);

            // Fire ONE genuine transition (inactive+acked → active+unacked). No ConditionRefresh is issued, so
            // the ONLY event is this transition — a per-root re-report would show up as a second copy.
            sink.WriteAlarmCondition(RawAlarmPath, ActiveSnapshot(), DateTime.UtcNow, AddressSpaceRealm.Raw);

            await WaitUntilAsync(() => collector.CountFor(serverItem.ClientHandle) >= 1, TimeSpan.FromSeconds(5));
            // Settle past the publishing interval so any (erroneous) duplicate would have arrived.
            await Task.Delay(TimeSpan.FromMilliseconds(700), ct);

            collector.CountFor(serverItem.ClientHandle).ShouldBe(1,
                "the single condition must deliver exactly ONE event to the Server object despite two notifier " +
                "roots (2 equipment folders) — a per-root re-report would deliver 2.");

            await subscription.DeleteAsync(true, ct);
        }
        finally
        {
            SafeDelete(pkiRoot + "-srv");
        }
    }

    /// <summary>Overlapping subscriptions: event monitored items at the Server object AND an equipment folder
    /// each receive EXACTLY ONE copy of the one transition — the multi-root topology does not multiply delivery
    /// to any single subscriber, and the equipment-folder subscriber (ns=UNS) sees the native alarm without
    /// touching ns=Raw.</summary>
    [Fact]
    public async Task Overlapping_folder_and_server_subscriptions_each_receive_exactly_one_copy()
    {
        var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-multinotif-ovl-{Guid.NewGuid():N}");
        var port = AllocateFreePort();
        var ct = TestContext.Current.CancellationToken;
        try
        {
            var (server, host) = await BootServerAsync(port, pkiRoot + "-srv", ServerUri + ".Overlap", ct);
            await using var _ = host;

            var sink = new SdkAddressSpaceSink(server.NodeManager!);
            WireSingleConditionToTwoEquipment(sink);

            using var session = await OpenSessionAsync($"opc.tcp://127.0.0.1:{port}/OtOpcUa", ct);
            var unsNs = (ushort)session.NamespaceUris.GetIndex(V3NodeIds.UnsNamespaceUri);
            unsNs.ShouldBeGreaterThan((ushort)0);

            var collector = new EventCollector();
            var subscription = new Subscription(session.DefaultSubscription) { PublishingInterval = 100 };
            subscription.FastEventCallback = collector.OnEvents;
            session.AddSubscription(subscription);
            await subscription.CreateAsync(ct);

            var serverItem = AddEventItem(subscription, ObjectIds.Server);
            var equipItem = AddEventItem(subscription, new NodeId(Equip1, unsNs));
            await subscription.ApplyChangesAsync(ct);

            sink.WriteAlarmCondition(RawAlarmPath, ActiveSnapshot(), DateTime.UtcNow, AddressSpaceRealm.Raw);

            await WaitUntilAsync(() =>
                    collector.CountFor(serverItem.ClientHandle) >= 1 &&
                    collector.CountFor(equipItem.ClientHandle) >= 1,
                TimeSpan.FromSeconds(5));
            await Task.Delay(TimeSpan.FromMilliseconds(700), ct);

            // Each subscriber gets exactly one copy — the shared snapshot is deduped per monitored item; the
            // multi-root topology never multiplies delivery.
            collector.CountFor(serverItem.ClientHandle).ShouldBe(1, "Server-object subscriber");
            collector.CountFor(equipItem.ClientHandle).ShouldBe(1, "equipment-folder subscriber (ns=UNS) sees the native alarm");

            await subscription.DeleteAsync(true, ct);
        }
        finally
        {
            SafeDelete(pkiRoot + "-srv");
        }
    }

    /// <summary>Materialise the raw device folder + the single native condition at its RawPath + two referencing
    /// equipment folders (UNS), then wire the condition as an event notifier of both equipment folders.</summary>
    private static void WireSingleConditionToTwoEquipment(SdkAddressSpaceSink sink)
    {
        sink.EnsureFolder(RawDeviceFolder, parentNodeId: null, displayName: "dev1", realm: AddressSpaceRealm.Raw);
        sink.MaterialiseAlarmCondition(RawAlarmPath, RawDeviceFolder, "temp_hi", "OffNormalAlarm", 700, AddressSpaceRealm.Raw, isNative: true);
        sink.EnsureFolder(Equip1, parentNodeId: null, displayName: "station1", realm: AddressSpaceRealm.Uns);
        sink.EnsureFolder(Equip2, parentNodeId: null, displayName: "station2", realm: AddressSpaceRealm.Uns);
        sink.WireAlarmNotifiers(RawAlarmPath, AddressSpaceRealm.Raw, new[] { Equip1, Equip2 }, AddressSpaceRealm.Uns);
    }

    private static AlarmConditionSnapshot ActiveSnapshot() =>
        new(Active: true, Acknowledged: false, Confirmed: true, Enabled: true,
            Shelving: AlarmShelvingKind.Unshelved, Severity: 700, Message: AlarmMessage);

    private static MonitoredItem AddEventItem(Subscription subscription, NodeId source)
    {
        var item = new MonitoredItem(subscription.DefaultItem)
        {
            StartNodeId = source,
            AttributeId = Attributes.EventNotifier,
            Filter = BuildEventFilter(),
            QueueSize = 100,
            SamplingInterval = 0,
        };
        subscription.AddItem(item);
        return item;
    }

    /// <summary>An EventFilter selecting [0]=EventId (dedup identity) and [1]=Message (our discriminator).</summary>
    private static EventFilter BuildEventFilter()
    {
        var filter = new EventFilter();
        filter.AddSelectClause(ObjectTypes.BaseEventType, BrowseNames.EventId);
        filter.AddSelectClause(ObjectTypes.BaseEventType, BrowseNames.Message);
        return filter;
    }

    /// <summary>Collects events delivered to a subscription's <see cref="Subscription.FastEventCallback"/>,
    /// tallying per monitored-item ClientHandle and filtering to our unique alarm Message (so unrelated server
    /// events / refresh brackets never count).</summary>
    private sealed class EventCollector
    {
        private readonly object _gate = new();
        private readonly Dictionary<uint, int> _byHandle = new();

        public void OnEvents(Subscription subscription, EventNotificationList notification, IList<string> stringTable)
        {
            lock (_gate)
            {
                foreach (var e in notification.Events)
                {
                    if (e.EventFields.Count > MessageFieldIndex &&
                        e.EventFields[MessageFieldIndex].Value is LocalizedText lt &&
                        lt.Text == AlarmMessage)
                    {
                        _byHandle[e.ClientHandle] = _byHandle.GetValueOrDefault(e.ClientHandle) + 1;
                    }
                }
            }
        }

        public int CountFor(uint clientHandle)
        {
            lock (_gate) return _byHandle.GetValueOrDefault(clientHandle);
        }
    }

    private static async Task<(OtOpcUaSdkServer Server, OpcUaApplicationHost Host)> BootServerAsync(
        int port, string pkiRoot, string appUri, CancellationToken ct)
    {
        var options = new OpcUaApplicationHostOptions
        {
            ApplicationName = appUri,
            ApplicationUri = appUri,
            OpcUaPort = port,
            PublicHostname = "127.0.0.1",
            PkiStoreRoot = pkiRoot,
            EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.None },
            AutoAcceptUntrustedClientCertificates = true,
        };
        var server = new OtOpcUaSdkServer();
        var host = new OpcUaApplicationHost(options, NullLogger<OpcUaApplicationHost>.Instance);
        await host.StartAsync(server, ct);
        return (server, host);
    }

    private static async Task<ISession> OpenSessionAsync(string endpointUrl, CancellationToken ct)
    {
        var appConfig = new ApplicationConfiguration
        {
            ApplicationName = "OtOpcUa.MultiNotifierEventDeliveryClient",
            ApplicationUri = $"urn:OtOpcUa.MultiNotifierEventDeliveryClient.{Guid.NewGuid():N}",
            ApplicationType = ApplicationType.Client,
            SecurityConfiguration = TestClientSecurity.Build(TestClientSecurity.AllocatePkiRoot()),
            ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 },
        };
        await appConfig.ValidateAsync(ApplicationType.Client, ct);
        appConfig.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;

        var endpoint = await CoreClientUtils.SelectEndpointAsync(
            appConfig, endpointUrl, false, DefaultTelemetry.Create(_ => { }), ct);
        var endpointConfiguration = EndpointConfiguration.Create(appConfig);
        var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfiguration);

        return await new DefaultSessionFactory(DefaultTelemetry.Create(_ => { })).CreateAsync(
            appConfig, configuredEndpoint, updateBeforeConnect: false, checkDomain: false,
            sessionName: "MultiNotifierEventDeliveryTests", sessionTimeout: 60_000,
            identity: new UserIdentity(new AnonymousIdentityToken()), preferredLocales: null, ct: ct);
    }

    private static async Task WaitUntilAsync(Func<bool> condition, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            if (condition()) return;
            await Task.Delay(50);
        }
        condition().ShouldBeTrue("condition not met within timeout");
    }

    private static int AllocateFreePort()
    {
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();
        return port;
    }

    private static void SafeDelete(string dir)
    {
        if (Directory.Exists(dir))
        {
            try { Directory.Delete(dir, recursive: true); }
            catch { /* best-effort */ }
        }
    }
}
