using System.Collections.Concurrent;
using Akka.Actor;
using Akka.Cluster.Tools.PublishSubscribe;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;

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

/// <summary>
///     E2E integration coverage for the <c>ScriptLogSignalRBridge</c> actor → in-process
///     broadcaster → Blazor <c>/script-log</c> page pipeline (Layer 0 of the script-log /
///     scripted-alarm runtime work).
///
///     <para><b>Scope note:</b> mirrors <see cref="DriverStatusHubE2eTests"/>. The Blazor circuit
///     itself can't be exercised from an integration test (it needs an HTTP listener, JWT auth, and
///     a real WebSocket upgrade — covered by the manual runbook). This suite instead exercises the
///     exact server-side delivery the page depends on: it spawns a
///     <see cref="ScriptLogSignalRBridge"/> in the harness actor system, publishes a
///     <see cref="ScriptLogEntry"/> to the <c>script-logs</c> DPS topic, and asserts that the entry
///     reaches the <see cref="IInProcessBroadcaster{T}"/> singleton resolved from the SAME DI
///     container the <c>/script-log</c> page injects from (plus the SignalR hub push). This is the
///     link the existing <see cref="DpsScriptLogPublisherTests"/> (publisher → topic) and
///     <see cref="InProcessBroadcasterTests"/> (broadcaster → subscriber) tests do not cover
///     together: bridge subscription → broadcaster fan-out off the live topic.</para>
/// </summary>
[Trait("Category", "Integration")]
public sealed class ScriptLogHubE2eTests
{
    private static CancellationToken Ct => TestContext.Current.CancellationToken;

    /// <summary>
    ///     Verifies that a <see cref="ScriptLogEntry"/> published to the <c>script-logs</c> DPS
    ///     topic is forwarded by <see cref="ScriptLogSignalRBridge"/> to (a) the
    ///     <see cref="IInProcessBroadcaster{T}"/> singleton the Blazor page reads and (b) the mock
    ///     <see cref="IHubContext{ScriptLogHub}"/> via a <c>SendAsync</c> on the expected method.
    /// </summary>
    [Fact]
    public async Task ScriptLogBridge_ForwardsEntry_ToInProcessBroadcaster_FromDpsTopic()
    {
        await using var harness = await TwoNodeClusterHarness.StartAsync();

        // The SAME singleton the Blazor /script-log page injects: AddAdminUI() →
        // AddOtOpcUaDriverStatusServices() registers IInProcessBroadcaster<> as an open-generic
        // singleton, so resolving the closed type here yields the page's instance.
        var broadcaster = harness.NodeA.Services.GetRequiredService<IInProcessBroadcaster<ScriptLogEntry>>();
        var received = new ConcurrentQueue<ScriptLogEntry>();
        void Handler(ScriptLogEntry e) => received.Enqueue(e);
        broadcaster.Received += Handler;

        // Mock IHubContext<ScriptLogHub>: the bridge pushes to Clients.All in addition to the
        // broadcaster (for any out-of-process SignalR client).
        var hubCalls = new List<(string Method, object? Arg)>();
        var mockClients = new Mock<IHubClients>();
        var mockClientProxy = new Mock<IClientProxy>();
        mockClients.Setup(c => c.All).Returns(mockClientProxy.Object);
        mockClientProxy
            .Setup(p => p.SendCoreAsync(It.IsAny<string>(), It.IsAny<object?[]>(), It.IsAny<CancellationToken>()))
            .Callback<string, object?[], CancellationToken>((method, args, _) =>
                hubCalls.Add((method, args.FirstOrDefault())))
            .Returns(Task.CompletedTask);
        var mockHub = new Mock<IHubContext<ScriptLogHub>>();
        mockHub.Setup(h => h.Clients).Returns(mockClients.Object);

        var bridge = harness.NodeASystem.ActorOf(
            ScriptLogSignalRBridge.Props(mockHub.Object, broadcaster),
            $"test-script-log-bridge-{Guid.NewGuid():N}");

        // Let the DPS subscription register + gossip before publishing.
        await Task.Delay(TimeSpan.FromSeconds(2), Ct);

        var entry = new ScriptLogEntry(
            ScriptId: "script-log-e2e",
            Level: "Information",
            Message: "hello from e2e",
            TimestampUtc: DateTime.UtcNow,
            VirtualTagId: "vtag-e2e",
            AlarmId: null,
            EquipmentId: "EQ-e2e");

        // DPS topic membership is eventually consistent — publish in a retry loop until delivered
        // (mirrors DpsScriptLogPublisherTests / DriverStatusHubE2eTests round-trip pattern).
        await WaitForAsync(() =>
        {
            DistributedPubSub.Get(harness.NodeASystem).Mediator.Tell(
                new Publish(ScriptLogSignalRBridge.TopicName, entry));
            return Task.FromResult(!received.IsEmpty);
        }, TimeSpan.FromSeconds(5));

        received.ShouldNotBeEmpty(
            "The /script-log page's in-process broadcaster must receive the entry the bridge forwards off the DPS topic.");
        received.TryDequeue(out var got).ShouldBeTrue();
        got.ShouldBe(entry);

        // The hub push also fired on the expected method name.
        hubCalls.ShouldNotBeEmpty("Hub mock should have received a SendAsync call.");
        hubCalls[0].Method.ShouldBe(ScriptLogHub.MethodName);

        broadcaster.Received -= Handler;
        harness.NodeASystem.Stop(bridge);
    }

    private static async Task WaitForAsync(Func<Task<bool>> condition, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            if (await condition()) return;
            await Task.Delay(100);
        }
        throw new TimeoutException($"Condition not met within {timeout}");
    }
}
