using System.Net;
using System.Net.Sockets;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.OtOpcUa.Admin.Hubs;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;

namespace ZB.MOM.WW.OtOpcUa.Admin.E2ETests;

/// <summary>
///     Stands up the Admin Blazor Server host on a free TCP port with the live SQL Server
///     context swapped for an EF Core InMemory DbContext + the LDAP cookie auth swapped for
///     <see cref="TestAuthHandler"/>. Playwright connects to <see cref="BaseUrl"/>.
///     InMemory is sufficient because UnsService's drag-drop path exercises EF operations,
///     not raw SQL.
/// </summary>
/// <remarks>
///     We deliberately build a <see cref="WebApplication"/> directly rather than going through
///     <c>WebApplicationFactory&lt;Program&gt;</c> — the factory's TestServer transport doesn't
///     coexist cleanly with Kestrel-on-a-real-port, and Playwright needs a real loopback HTTP
///     endpoint to hit. This mirrors the Program.cs entry-points for everything else.
/// </remarks>
public sealed class AdminWebAppFactory : IAsyncDisposable
{
    private WebApplication? _app;

    public string BaseUrl { get; private set; } = "";
    public long SeededGenerationId { get; private set; }
    public string SeededClusterId { get; } = "e2e-cluster";

    /// <summary>
    ///     Root service provider of the running host. Tests use this to create scopes that
    ///     share the InMemory DB with the Blazor-rendered page — e.g. to assert post-commit
    ///     state, or to simulate a concurrent peer edit that bumps the DraftRevisionToken
    ///     between preview-open and Confirm-click.
    /// </summary>
    public IServiceProvider Services => _app?.Services
        ?? throw new InvalidOperationException("AdminWebAppFactory: StartAsync has not been called");

    public async Task StartAsync()
    {
        var port = GetFreeTcpPort();
        BaseUrl = $"http://127.0.0.1:{port}";

        // Point the content root at the Admin project's build output so the Admin
        // assembly + its sibling staticwebassets manifest are discoverable. The manifest
        // maps /_framework/* to the framework NuGet cache + /app.css to the Admin source
        // wwwroot; StaticWebAssetsLoader.UseStaticWebAssets reads it and wires a composite
        // file provider automatically.
        var adminAssemblyDir = System.IO.Path.GetDirectoryName(
            typeof(Admin.Components.App).Assembly.Location)!;
        var builder = WebApplication.CreateBuilder(new WebApplicationOptions
        {
            ContentRootPath = adminAssemblyDir,
            ApplicationName = typeof(Admin.Components.App).Assembly.GetName().Name,
        });
        builder.WebHost.UseUrls(BaseUrl);
        // UseStaticWebAssets reads {ApplicationName}.staticwebassets.runtime.json (or the
        // development variant via the ASPNETCORE_HOSTINGSTARTUPASSEMBLIES convention) and
        // composes a PhysicalFileProvider per declared ContentRoot. This is what
        // `dotnet run` does automatically via the MSBuild targets — we replicate it
        // explicitly for the test-owned pipeline.
        builder.WebHost.UseStaticWebAssets();
        // E2E host runs in Development so unhandled exceptions during Blazor render surface
        // as visible 500s with stacks the test can capture — prod-style generic errors make
        // diagnosis of circuit / DI misconfig effectively impossible.
        builder.Environment.EnvironmentName = Microsoft.Extensions.Hosting.Environments.Development;

        // --- Mirror the Admin composition in Program.cs, but with the InMemory DB + test
        // auth swaps instead of SQL Server + LDAP cookie auth.
        builder.Services.AddRazorComponents().AddInteractiveServerComponents();
        builder.Services.AddHttpContextAccessor();
        builder.Services.AddSignalR();
        builder.Services.AddAntiforgery();

        builder.Services.AddAuthentication(TestAuthHandler.SchemeName)
            .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.SchemeName, _ => { });
        builder.Services.AddAuthorizationBuilder()
            .AddPolicy("CanEdit", p => p.RequireRole(Admin.Services.AdminRoles.ConfigEditor, Admin.Services.AdminRoles.FleetAdmin))
            .AddPolicy("CanPublish", p => p.RequireRole(Admin.Services.AdminRoles.FleetAdmin));
        builder.Services.AddCascadingAuthenticationState();

        // One InMemory database name per fixture — the lambda below runs on every DbContext
        // construction, so capturing a stable string (not calling Guid.NewGuid() inline) is
        // critical: every scope (seed, Blazor circuit, test assertions) must share the same
        // backing store or rows written in one scope disappear in the next.
        var dbName = $"e2e-{Guid.NewGuid():N}";
        builder.Services.AddDbContext<OtOpcUaConfigDbContext>(opt =>
            opt.UseInMemoryDatabase(dbName));

        builder.Services.AddScoped<Admin.Services.ClusterService>();
        builder.Services.AddScoped<Admin.Services.GenerationService>();
        builder.Services.AddScoped<Admin.Services.UnsService>();
        builder.Services.AddScoped<Admin.Services.EquipmentService>();
        builder.Services.AddScoped<Admin.Services.NamespaceService>();
        builder.Services.AddScoped<Admin.Services.DriverInstanceService>();
        builder.Services.AddScoped<Admin.Services.DraftValidationService>();

        _app = builder.Build();
        _app.UseStaticFiles();
        _app.UseRouting();
        _app.UseAuthentication();
        _app.UseAuthorization();
        _app.UseAntiforgery();
        _app.MapRazorComponents<Admin.Components.App>().AddInteractiveServerRenderMode();
        // The ClusterDetail + other pages connect SignalR hubs at render time — the
        // endpoints must exist or the Blazor circuit surfaces a 500 on first interactive
        // step. No background pollers (FleetStatusPoller etc.) are registered so the hubs
        // stay quiet until something pushes through IHubContext, which the E2E tests don't.
        _app.MapHub<FleetStatusHub>("/hubs/fleet");
        _app.MapHub<AlertHub>("/hubs/alerts");

        // Seed the draft BEFORE starting the host so Playwright sees a ready page on first nav.
        using (var scope = _app.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<OtOpcUaConfigDbContext>();
            SeededGenerationId = Seed(db, SeededClusterId);
        }

        await _app.StartAsync();
    }

    public async ValueTask DisposeAsync()
    {
        if (_app is not null)
        {
            await _app.StopAsync();
            await _app.DisposeAsync();
        }
    }

    private static long Seed(OtOpcUaConfigDbContext db, string clusterId)
    {
        var cluster = new ServerCluster
        {
            ClusterId = clusterId, Name = "e2e", Enterprise = "zb", Site = "lab",
            RedundancyMode = RedundancyMode.None, NodeCount = 1, CreatedBy = "e2e",
        };
        var gen = new ConfigGeneration
        {
            ClusterId = clusterId, Status = GenerationStatus.Draft, CreatedBy = "e2e",
        };

        db.ServerClusters.Add(cluster);
        db.ConfigGenerations.Add(gen);
        db.SaveChanges();

        db.UnsAreas.AddRange(
            new UnsArea { UnsAreaId = "area-a", ClusterId = clusterId, Name = "warsaw", GenerationId = gen.GenerationId },
            new UnsArea { UnsAreaId = "area-b", ClusterId = clusterId, Name = "berlin", GenerationId = gen.GenerationId });
        db.UnsLines.Add(new UnsLine
        {
            UnsLineId = "line-a1", UnsAreaId = "area-a", Name = "oven-line", GenerationId = gen.GenerationId,
        });
        db.SaveChanges();
        return gen.GenerationId;
    }

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