using System.Net;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Grpc;
using ZB.MOM.WW.OtOpcUa.Cluster;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
using ZB.MOM.WW.OtOpcUa.Runtime.DeploymentCache;

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

/// <summary>
///     Per-cluster mesh Phase 3 (Task 8) — the REAL gRPC artifact-fetch boundary. An in-memory fake
///     client (Task 4) proves the fetcher's logic; this proves a real stream crosses a real Kestrel
///     h2c listener through the real <see cref="ConfigServeAuthInterceptor"/> into the real
///     <see cref="DeploymentArtifactGrpcService"/> and back through the real
///     <see cref="GrpcDeploymentArtifactFetcher"/>.
/// </summary>
/// <remarks>
///     The Phase-2 lesson applied: a passing fake client cannot distinguish "the boundary works" from
///     "something else supplied the bytes". The wrong-key control (assertion 2) is the falsifier — it
///     must return null on the SAME server where the right key succeeds.
/// </remarks>
[Trait("Category", "ArtifactBoundary")]
public sealed class DeploymentArtifactFetchBoundaryTests
{
    private const string ApiKey = "the-shared-node-key";

    [Fact]
    public async Task RightKey_FetchesAndReassembles_ByteEqual_AndVerifiesAgainstTheRevisionHash()
    {
        // >256 KB so the stream really chunks (3 × 128 KiB).
        var blob = RandomBlob(300 * 1024);
        var revisionHash = Sha256Hex(blob);

        var (server, deploymentId, port) = await StartServerAsync(revisionHash, blob);
        await using var _ = server;

        var fetcher = Fetcher([$"http://localhost:{port}"], ApiKey);
        var result = await fetcher.FetchAsync(deploymentId, revisionHash, TestContext.Current.CancellationToken);

        result.ShouldBe(blob);
    }

    [Fact]
    public async Task WrongKey_ReturnsNull_WhileTheRightKeySucceedsOnTheSameServer()
    {
        var blob = RandomBlob(4096);
        var revisionHash = Sha256Hex(blob);

        var (server, deploymentId, port) = await StartServerAsync(revisionHash, blob);
        await using var _ = server;

        // Falsifiability control: the interceptor rejects a wrong key -> the fetcher gets an RpcException
        // -> null. Without this, a green right-key fetch cannot prove the boundary is what carries bytes.
        var wrong = await Fetcher([$"http://localhost:{port}"], "not-the-key")
            .FetchAsync(deploymentId, revisionHash, TestContext.Current.CancellationToken);
        wrong.ShouldBeNull();

        // Same server, right key: succeeds — proving the null above was the auth gate, not a dead server.
        var right = await Fetcher([$"http://localhost:{port}"], ApiKey)
            .FetchAsync(deploymentId, revisionHash, TestContext.Current.CancellationToken);
        right.ShouldBe(blob);
    }

    [Fact]
    public async Task AnUnknownDeploymentId_ReturnsNull()
    {
        var blob = RandomBlob(4096);
        var (server, _, port) = await StartServerAsync(Sha256Hex(blob), blob);
        await using var __ = server;

        var result = await Fetcher([$"http://localhost:{port}"], ApiKey)
            .FetchAsync(Guid.NewGuid().ToString(), Sha256Hex(blob), TestContext.Current.CancellationToken);

        result.ShouldBeNull();
    }

    [Fact]
    public async Task Failover_FromADeadEndpoint_ToTheRealServer_StillReturnsTheBytes()
    {
        var blob = RandomBlob(4096);
        var revisionHash = Sha256Hex(blob);

        var (server, deploymentId, port) = await StartServerAsync(revisionHash, blob);
        await using var _ = server;
        var deadPort = GetFreePort(); // nothing listening

        var fetcher = Fetcher([$"http://localhost:{deadPort}", $"http://localhost:{port}"], ApiKey);
        var result = await fetcher.FetchAsync(deploymentId, revisionHash, TestContext.Current.CancellationToken);

        result.ShouldBe(blob);
    }

    // ---- harness --------------------------------------------------------------------------------

    /// <summary>
    ///     Stands a minimal real Host: an h2c-only Kestrel listener serving the real
    ///     <see cref="DeploymentArtifactGrpcService"/> behind the real <see cref="ConfigServeAuthInterceptor"/>,
    ///     backed by an in-memory config DB seeded with one sealed deployment.
    /// </summary>
    private static async Task<(WebApplication Server, string DeploymentId, int Port)> StartServerAsync(
        string revisionHash, byte[] blob)
    {
        var port = GetFreePort();
        var id = Guid.NewGuid();
        var dbName = $"artboundary-{id:N}";

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

        builder.Services.Configure<ConfigServeOptions>(o => o.ApiKey = ApiKey);
        builder.Services.AddGrpc(o => o.Interceptors.Add<ConfigServeAuthInterceptor>());
        builder.Services.AddDbContextFactory<OtOpcUaConfigDbContext>(o => o.UseInMemoryDatabase(dbName));

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

        var app = builder.Build();
        app.MapGrpcService<DeploymentArtifactGrpcService>();

        // Seed the sealed deployment the service will serve.
        await using (var scope = app.Services.CreateAsyncScope())
        {
            var factory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<OtOpcUaConfigDbContext>>();
            await using var db = await factory.CreateDbContextAsync(TestContext.Current.CancellationToken);
            db.Deployments.Add(new Deployment
            {
                DeploymentId = id,
                RevisionHash = revisionHash,
                Status = DeploymentStatus.Sealed,
                CreatedBy = "test",
                ArtifactBlob = blob,
            });
            await db.SaveChangesAsync(TestContext.Current.CancellationToken);
        }

        await app.StartAsync(TestContext.Current.CancellationToken);
        return (app, id.ToString(), port);
    }

    private static GrpcDeploymentArtifactFetcher Fetcher(string[] endpoints, string apiKey)
        => new(
            Options.Create(new ConfigSourceOptions
            {
                Mode = ConfigSourceOptions.ModeFetchAndCache,
                CentralFetchEndpoints = endpoints,
                ApiKey = apiKey,
                FetchTimeoutSeconds = 15,
            }),
            NullLogger<GrpcDeploymentArtifactFetcher>.Instance);

    private static byte[] RandomBlob(int size)
    {
        var blob = new byte[size];
        for (var i = 0; i < size; i++) blob[i] = (byte)(i * 31 + 7);
        return blob;
    }

    private static string Sha256Hex(byte[] bytes) => Convert.ToHexStringLower(SHA256.HashData(bytes));

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