using System.Net;
using System.Threading.Channels;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Cluster;
using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
using ZB.MOM.WW.OtOpcUa.Host.Grpc;
using ZB.MOM.WW.OtOpcUa.Runtime.Telemetry;

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

/// <summary>
///     Per-cluster mesh Phase 5 (host-wiring task) — the telemetry-stream h2c listener coexisting with
///     the two earlier dedicated ports (LocalDb sync + ConfigServe) and the main HTTP surface, and the
///     <see cref="TelemetryStreamGrpcService"/> actually being <b>mapped and interceptor-gated</b> on
///     that port.
/// </summary>
/// <remarks>
///     <para>
///         <b>Why this exists.</b> Phase 5 adds a THIRD dedicated h2c port to <c>Program.cs</c>'s merged
///         listener block. That block must still re-bind the existing HTTP surface EXACTLY ONCE while
///         adding all three dedicated ports (re-binding it per-port throws "address already in use";
///         re-binding it zero times silently unbinds the AdminUI behind Traefik). The sibling
///         <see cref="ConfigServeListenerTests"/> pins the two-port case; this pins the three-port case
///         AND — the part a port-open check cannot prove — that the telemetry gRPC service is genuinely
///         wired: an un-keyed dial gets <see cref="StatusCode.PermissionDenied"/> (mapped + interceptor
///         reached), not <see cref="StatusCode.Unimplemented"/> (never mapped), and a correctly-keyed
///         dial opens a real stream. This is the executable regression for the "ships clean but never
///         actually wired" gap.
///     </para>
///     <para>
///         Like <see cref="ConfigServeListenerTests"/>, this drives a minimal <c>WebApplication</c>
///         shaped exactly like <c>Program.cs</c>'s driver-role wiring (AddGrpc +
///         <see cref="TelemetryStreamAuthInterceptor"/> + <c>Configure&lt;TelemetryOptions&gt;</c> +
///         <c>MapGrpcService&lt;TelemetryStreamGrpcService&gt;</c>) rather than booting the real host
///         (which needs SQL, an Akka mesh and LDAP). What is under test is the Kestrel binding contract
///         and the gRPC map/intercept wiring, both fully reproduced here. The node's own telemetry hub
///         is a stub — this test proves reachability, not fan-out content.
///     </para>
/// </remarks>
public sealed class TelemetryListenerTests
{
    private const string ApiKey = "telemetry-test-key";

    [Fact]
    public async Task AllThreeDedicatedH2cPorts_AndTheHttpSurface_AnswerSimultaneously_AndTelemetryServiceIsWired()
    {
        var httpPort = GetFreePort();
        var syncPort = GetFreePort();
        var configServePort = GetFreePort();
        var telemetryPort = GetFreePort();

        var builder = WebApplication.CreateBuilder();
        builder.Environment.ApplicationName = typeof(TelemetryListenerTests).Assembly.GetName().Name!;

        // Exactly the driver-role wiring Program.cs applies for the telemetry endpoint: the fail-closed
        // interceptor on the shared AddGrpc pipeline, the TelemetryOptions the interceptor reads its
        // expected key from, and a stub hub so the mapped service can be constructed on the keyed path.
        builder.Services.Configure<TelemetryOptions>(o => o.ApiKey = ApiKey);
        builder.Services.AddSingleton(StubHubYieldingAnEmptyStream());
        builder.Services.AddGrpc(o => o.Interceptors.Add<TelemetryStreamAuthInterceptor>());

        // The shape Program.cs's merged block applies: compute the existing surface ONCE, apply it ONCE,
        // then add each configured dedicated h2c port. Here all THREE dedicated ports are set at once —
        // the fused-node case that would double-bind the HTTP surface if the block re-applied it per-port.
        var existingBindings = KestrelHttpBinding.Parse($"http://localhost:{httpPort}");
        builder.WebHost.ConfigureKestrel(kestrel =>
        {
            foreach (var binding in existingBindings)
                binding.Apply(kestrel);

            kestrel.ListenAnyIP(syncPort, o => o.Protocols = HttpProtocols.Http2);
            kestrel.ListenAnyIP(configServePort, o => o.Protocols = HttpProtocols.Http2);
            kestrel.ListenAnyIP(telemetryPort, o => o.Protocols = HttpProtocols.Http2);
        });

        await using var app = builder.Build();
        app.MapGet("/healthz", () => "ok");
        app.MapGrpcService<TelemetryStreamGrpcService>();
        await app.StartAsync(TestContext.Current.CancellationToken);

        try
        {
            var ct = TestContext.Current.CancellationToken;

            // 1) The re-bound HTTP/1.1 surface still answers (the AdminUI/Traefik guarantee) even with
            //    all three dedicated ports added — i.e. the existing surface was re-bound exactly once.
            using var http1 = new HttpClient();
            (await http1.GetStringAsync($"http://localhost:{httpPort}/healthz", ct)).ShouldBe("ok");

            // 2) The sync + config-serve dedicated ports negotiate prior-knowledge h2c. A 404 is fine —
            //    it proves the HTTP/2 connection established and routed, which is all that is in question
            //    for those two (their services are mapped elsewhere; here we only pin the port binding).
            using var http2 = new HttpClient
            {
                DefaultRequestVersion = HttpVersion.Version20,
                DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact,
            };
            (await http2.GetAsync($"http://localhost:{syncPort}/", ct)).Version.ShouldBe(HttpVersion.Version20);
            (await http2.GetAsync($"http://localhost:{configServePort}/", ct)).Version.ShouldBe(HttpVersion.Version20);

            // 3) The telemetry port serves the MAPPED, interceptor-gated gRPC service. Dial it for real.
            using var channel = GrpcChannel.ForAddress(
                $"http://localhost:{telemetryPort}", new GrpcChannelOptions { HttpHandler = new SocketsHttpHandler() });
            var client = new TelemetryStreamService.TelemetryStreamServiceClient(channel);

            // 3a) No bearer -> PermissionDenied. This is the load-bearing assertion: PermissionDenied can
            //     only come from the interceptor, and the interceptor only runs because the method was
            //     routed to the mapped service. A service that was never mapped would answer Unimplemented.
            var noKey = await Should.ThrowAsync<RpcException>(async () =>
            {
                using var call = client.Subscribe(new TelemetryStreamRequest { CorrelationId = "no-key" }, cancellationToken: ct);
                await foreach (var _ in call.ResponseStream.ReadAllAsync(ct)) { }
            });
            noKey.StatusCode.ShouldBe(StatusCode.PermissionDenied);
            noKey.StatusCode.ShouldNotBe(StatusCode.Unimplemented);

            // 3b) Correct bearer -> the mapped handler actually executes end-to-end (interceptor passes,
            //     the service resolves the stub hub and streams). The stub yields an empty, completed
            //     stream, so the call completes cleanly with zero envelopes.
            var headers = new Metadata { { "authorization", $"Bearer {ApiKey}" } };
            using var keyed = client.Subscribe(
                new TelemetryStreamRequest { CorrelationId = "keyed" }, headers, cancellationToken: ct);
            var received = 0;
            await foreach (var _ in keyed.ResponseStream.ReadAllAsync(ct))
                received++;
            received.ShouldBe(0);
        }
        finally
        {
            await app.StopAsync(TestContext.Current.CancellationToken);
        }
    }

    /// <summary>
    ///     A hub whose subscription reader is already completed and empty, so the keyed
    ///     <c>Subscribe</c> handler runs to completion and closes the stream with zero envelopes —
    ///     enough to prove the mapped service executed without depending on any live telemetry producer.
    /// </summary>
    private static ITelemetryLocalHub StubHubYieldingAnEmptyStream()
    {
        var hub = new Mock<ITelemetryLocalHub>();
        hub.Setup(h => h.Subscribe(It.IsAny<int>())).Returns(() => new EmptySubscription());
        return hub.Object;
    }

    private sealed class EmptySubscription : ITelemetrySubscription
    {
        private readonly Channel<TelemetryItem> _channel = Channel.CreateBounded<TelemetryItem>(1);

        public EmptySubscription() => _channel.Writer.Complete();

        public ChannelReader<TelemetryItem> Reader => _channel.Reader;

        public void Dispose()
        {
        }
    }

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