using System.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;

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

/// <summary>
///     Per-cluster mesh Phase 3 (Task 3) — the coexistence of the ConfigServe artifact h2c listener
///     with the LocalDb-sync h2c listener on a fused admin+driver node.
/// </summary>
/// <remarks>
///     <para>
///         <b>Why this is the load-bearing test.</b> A fused central node can carry BOTH dedicated
///         h2c ports (LocalDb sync + ConfigServe). Any explicit Kestrel <c>Listen*</c> makes Kestrel
///         discard <c>ASPNETCORE_URLS</c> wholesale, so the existing HTTP surface must be re-bound
///         exactly ONCE alongside the two dedicated ports. Re-binding it twice (one block per h2c
///         port) throws "address already in use"; re-binding it zero times silently unbinds the
///         AdminUI behind Traefik. This test pins that all three answer simultaneously.
///     </para>
///     <para>
///         Like <see cref="LocalDbSyncListenerTests"/>, this drives a minimal <c>WebApplication</c>
///         shaped exactly like <c>Program.cs</c>'s merged listener block rather than booting the real
///         host (which needs SQL, an Akka mesh and LDAP). What is under test is the Kestrel binding
///         contract, fully reproduced here; the real end-to-end fetch is the Task 8 boundary test.
///     </para>
/// </remarks>
public sealed class ConfigServeListenerTests
{
    [Fact]
    public async Task BothDedicatedH2cPorts_AndTheHttpSurface_AnswerSimultaneously()
    {
        var httpPort = GetFreePort();
        var syncPort = GetFreePort();
        var configServePort = GetFreePort();

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

        // Exactly the shape Program.cs's merged block applies: compute the existing surface ONCE,
        // apply it ONCE, then add each configured dedicated h2c 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);
        });

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

        try
        {
            // The re-bound HTTP/1.1 surface still answers (the AdminUI/Traefik guarantee).
            using var http1 = new HttpClient();
            (await http1.GetStringAsync(
                $"http://localhost:{httpPort}/healthz", TestContext.Current.CancellationToken))
                .ShouldBe("ok");

            // Both dedicated ports negotiate prior-knowledge h2c. A 404 is a fine result — it proves
            // the HTTP/2 connection was established and routed, which is all that is in question.
            using var http2 = new HttpClient
            {
                DefaultRequestVersion = HttpVersion.Version20,
                DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact,
            };

            using var syncResponse = await http2.GetAsync(
                $"http://localhost:{syncPort}/", TestContext.Current.CancellationToken);
            syncResponse.Version.ShouldBe(HttpVersion.Version20);

            using var configServeResponse = await http2.GetAsync(
                $"http://localhost:{configServePort}/", TestContext.Current.CancellationToken);
            configServeResponse.Version.ShouldBe(HttpVersion.Version20);
        }
        finally
        {
            await app.StopAsync(TestContext.Current.CancellationToken);
        }
    }

    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;
    }
}
