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>
///     LocalDb Phase 1 (Task 5) — the dedicated h2c sync listener and, more importantly, the
///     re-binding of the endpoints the host was already serving.
/// </summary>
/// <remarks>
///     <para>
///         <b>Why this file exists.</b> The host binds exclusively via <c>ASPNETCORE_URLS</c> and has
///         no <c>ConfigureKestrel</c> call. Any explicit <c>Listen*</c> makes Kestrel discard that
///         configuration wholesale — it logs "Overriding address(es)" and serves only what was
///         listed explicitly. Getting this wrong unbinds the AdminUI and the deploy API behind
///         Traefik, and it fails silently: the process starts, logs look normal, and nothing answers.
///     </para>
///     <para>
///         These tests drive a minimal <c>WebApplication</c> shaped exactly like <c>Program.cs</c>'s
///         block rather than booting the real host, which needs SQL Server, an Akka mesh and LDAP.
///         What is under test is the Kestrel binding contract, and that is fully reproduced here.
///     </para>
/// </remarks>
public sealed class LocalDbSyncListenerTests
{
    // ---------------------------------------------------------------------------------------
    // KestrelHttpBinding.Parse
    // ---------------------------------------------------------------------------------------

    [Fact]
    public void Parse_NullOrEmpty_YieldsNoBindings()
    {
        // "Nothing configured" is a real supported state: Install-Services.ps1 sets ASPNETCORE_URLS
        // only for admin nodes, so a driver-only service genuinely has none.
        KestrelHttpBinding.Parse(null).ShouldBeEmpty();
        KestrelHttpBinding.Parse("").ShouldBeEmpty();
        KestrelHttpBinding.Parse("   ").ShouldBeEmpty();
    }

    [Theory]
    [InlineData("http://+:9000", "+", 9000)]
    [InlineData("http://*:9000", "*", 9000)]
    [InlineData("http://localhost:9000", "localhost", 9000)]
    [InlineData("http://0.0.0.0:8080", "0.0.0.0", 8080)]
    [InlineData("http://127.0.0.1:5001", "127.0.0.1", 5001)]
    public void Parse_SingleUrl_ExtractsHostAndPort(string url, string expectedHost, int expectedPort)
    {
        // The "+" and "*" wildcards are Kestrel-isms that System.Uri cannot parse — the docker-dev
        // rig uses "http://+:9000", so mishandling them would break every rig node.
        var bindings = KestrelHttpBinding.Parse(url);

        bindings.Count.ShouldBe(1);
        bindings[0].Host.ShouldBe(expectedHost);
        bindings[0].Port.ShouldBe(expectedPort);
        bindings[0].IsSecure.ShouldBeFalse();
    }

    [Fact]
    public void Parse_MultipleUrls_PreservesAllOfThem()
    {
        // Dropping one of several configured endpoints is the exact silent-unbind failure this
        // whole mechanism exists to avoid.
        var bindings = KestrelHttpBinding.Parse("http://+:9000;http://localhost:9100");

        bindings.Count.ShouldBe(2);
        bindings[0].Port.ShouldBe(9000);
        bindings[1].Port.ShouldBe(9100);
    }

    [Fact]
    public void Parse_HttpsUrl_IsFlaggedSecure()
    {
        // Program.cs refuses to take over Kestrel when any endpoint is HTTPS, because replaying
        // certificate configuration is not modelled here. This flag is what drives that refusal.
        KestrelHttpBinding.Parse("https://+:443")[0].IsSecure.ShouldBeTrue();
    }

    [Fact]
    public void Parse_MalformedEntry_IsSkipped_NotThrown()
    {
        // A typo'd URL must not take the process down at startup.
        KestrelHttpBinding.Parse("not a url;http://+:9000").ShouldHaveSingleItem().Port.ShouldBe(9000);
    }

    // ---- ParsePorts (ASPNETCORE_HTTP_PORTS / HTTPS_PORTS) ----------------------------------

    [Fact]
    public void ParsePorts_NullOrEmpty_YieldsNoBindings()
    {
        KestrelHttpBinding.ParsePorts(null, isSecure: false).ShouldBeEmpty();
        KestrelHttpBinding.ParsePorts("", isSecure: false).ShouldBeEmpty();
        KestrelHttpBinding.ParsePorts("  ", isSecure: false).ShouldBeEmpty();
    }

    [Fact]
    public void ParsePorts_BarePort_BindsAllInterfaces()
    {
        // The aspnet:8.0+ container default is ASPNETCORE_HTTP_PORTS=8080, on all interfaces — not
        // loopback. Re-binding it as a wildcard is what keeps a driver node's health/metrics surface
        // reachable from other containers once the sync listener takes over Kestrel.
        var binding = KestrelHttpBinding.ParsePorts("8080", isSecure: false).ShouldHaveSingleItem();
        binding.Host.ShouldBe("+");
        binding.Port.ShouldBe(8080);
        binding.IsSecure.ShouldBeFalse();
    }

    [Fact]
    public void ParsePorts_SeparatedList_AndSecureFlag()
    {
        KestrelHttpBinding.ParsePorts("8080;8081", isSecure: false).Select(b => b.Port).ShouldBe([8080, 8081]);
        KestrelHttpBinding.ParsePorts("8080,8081", isSecure: false).Select(b => b.Port).ShouldBe([8080, 8081]);
        KestrelHttpBinding.ParsePorts("443", isSecure: true).ShouldHaveSingleItem().IsSecure.ShouldBeTrue();
    }

    [Fact]
    public void ParsePorts_MalformedPort_IsSkipped_NotThrown()
    {
        KestrelHttpBinding.ParsePorts("notaport;8080;0;70000", isSecure: false)
            .ShouldHaveSingleItem().Port.ShouldBe(8080);
    }

    // ---------------------------------------------------------------------------------------
    // The real Kestrel contract
    // ---------------------------------------------------------------------------------------

    [Fact]
    public async Task ExplicitListen_ReBindsTheExistingSurface_AndAddsAnH2cListener()
    {
        // THE test. Both ports must answer simultaneously: the re-bound HTTP/1.1 surface (proving
        // the ASPNETCORE_URLS override was compensated for) and the HTTP/2-only sync port (proving
        // prior-knowledge h2c works, which a cleartext Http1AndHttp2 endpoint cannot do).
        var httpPort = GetFreePort();
        var syncPort = GetFreePort();

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

        // Exactly the shape Program.cs applies.
        foreach (var binding in KestrelHttpBinding.Parse($"http://localhost:{httpPort}"))
        {
            var captured = binding;
            builder.WebHost.ConfigureKestrel(k => captured.Apply(k));
        }

        builder.WebHost.ConfigureKestrel(k =>
            k.ListenAnyIP(syncPort, o => o.Protocols = HttpProtocols.Http2));

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

        try
        {
            using var http1 = new HttpClient();
            var body = await http1.GetStringAsync(
                $"http://localhost:{httpPort}/healthz", TestContext.Current.CancellationToken);
            body.ShouldBe("ok");

            // Prior-knowledge h2c against the sync port. A 404 is a perfectly good result — it
            // proves the HTTP/2 connection was established and the request was routed, which is
            // the only thing in question here.
            using var http2 = new HttpClient
            {
                DefaultRequestVersion = HttpVersion.Version20,
                DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact,
            };
            using var response = await http2.GetAsync(
                $"http://localhost:{syncPort}/", TestContext.Current.CancellationToken);

            response.Version.ShouldBe(HttpVersion.Version20);
        }
        finally
        {
            await app.StopAsync(TestContext.Current.CancellationToken);
        }
    }

    [Fact]
    public async Task ExplicitListen_WithoutReBinding_SilentlyDiscardsTheConfiguredUrls()
    {
        // POSITIVE CONTROL for the whole task. If Kestrel did NOT override configured URLs, the
        // re-binding above would be pointless ceremony. This pins the actual behaviour: a single
        // explicit Listen* call makes the ASPNETCORE_URLS port stop answering entirely — no
        // exception, no failed startup, just silence. That is the regression being guarded against.
        var configuredPort = GetFreePort();
        var explicitPort = GetFreePort();

        var builder = WebApplication.CreateBuilder();
        builder.Environment.ApplicationName = typeof(LocalDbSyncListenerTests).Assembly.GetName().Name!;
        builder.WebHost.UseUrls($"http://localhost:{configuredPort}");

        // Deliberately NOT re-binding configuredPort — this is the mistake being demonstrated.
        builder.WebHost.ConfigureKestrel(k => k.ListenAnyIP(explicitPort));

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

        try
        {
            using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };

            // The explicitly listed port serves.
            (await client.GetStringAsync(
                $"http://localhost:{explicitPort}/healthz",
                TestContext.Current.CancellationToken)).ShouldBe("ok");

            // The configured one does not — nothing is listening there at all.
            await Should.ThrowAsync<HttpRequestException>(() => client.GetStringAsync(
                $"http://localhost:{configuredPort}/healthz",
                TestContext.Current.CancellationToken));
        }
        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;
    }
}
