using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
using ZB.MOM.WW.ScadaBridge.InboundAPI.Middleware;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;

/// <summary>
/// Audit Log #23 — M4 Bundle E (Task E3) end-to-end audit trail for the
/// inbound API surface. Wires the production
/// <see cref="AuditWriteMiddleware"/> into a Microsoft.AspNetCore.TestHost
/// pipeline that mirrors the production
/// <c>UseAuthentication → UseAuditWriteMiddleware → POST /api/{methodName}</c>
/// order, with the real <see cref="CentralAuditWriter"/> backed by the per-class
/// <see cref="MsSqlMigrationFixture"/> MSSQL <c>AuditLog</c> table.
/// </summary>
/// <remarks>
/// <para>
/// Three response shapes are covered: a happy-path 200 (with the actor
/// resolved from <see cref="HttpContext.Items"/>), a 401 unauthenticated
/// (Actor stays null, kind flips to
/// <see cref="AuditKind.InboundAuthFailure"/>), and a 500 internal-error
/// response. Each test uses a unique method name so concurrent tests sharing
/// the fixture don't interfere.
/// </para>
/// <para>
/// The middleware-level unit tests already cover the recording-writer shape
/// (<c>AuditWriteMiddlewareTests</c>) and the pipeline ordering
/// (<c>MiddlewareOrderTests</c>); these tests verify the END-TO-END
/// materialisation in the central <c>AuditLog</c> table — the production
/// glue from request → writer → repository → MSSQL row.
/// </para>
/// </remarks>
public class InboundApiAuditTests : IClassFixture<MsSqlMigrationFixture>
{
    private readonly MsSqlMigrationFixture _fixture;

    public InboundApiAuditTests(MsSqlMigrationFixture fixture)
    {
        _fixture = fixture;
    }

    /// <summary>
    /// Per-test unique method name suffix — the audit row's <c>Target</c>
    /// captures it so each test can query by <see cref="AuditLogQueryFilter.Target"/>
    /// without disturbing other tests using the same MSSQL fixture.
    /// </summary>
    private static string NewMethodName(string prefix) =>
        prefix + "-" + Guid.NewGuid().ToString("N").Substring(0, 8);

    private ScadaBridgeDbContext CreateContext()
    {
        var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
            .UseSqlServer(_fixture.ConnectionString)
            .ConfigureWarnings(w => w.Ignore(
                Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning))
            .Options;
        return new ScadaBridgeDbContext(options);
    }

    /// <summary>
    /// Spins up a minimal in-memory ASP.NET host whose pipeline mirrors the
    /// production arrangement. The endpoint handler delegate is supplied by
    /// each test so it can shape the response (200 with an actor, 401
    /// auth-fail, 500 server error) the way the production handler would.
    /// </summary>
    private async Task<IHost> BuildHostAsync(RequestDelegate endpointHandler)
    {
        var hostBuilder = new HostBuilder()
            .ConfigureWebHost(webBuilder =>
            {
                webBuilder
                    .UseTestServer()
                    .ConfigureServices(services =>
                    {
                        // Real EF DbContext + AuditLogRepository wired against
                        // the per-class MSSQL fixture. CentralAuditWriter is a
                        // singleton — same pattern the production Host uses —
                        // opening a fresh scope per call to resolve the scoped
                        // repository.
                        services.AddDbContext<ScadaBridgeDbContext>(opts =>
                            opts.UseSqlServer(_fixture.ConnectionString)
                                .ConfigureWarnings(w => w.Ignore(
                                    Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)));
                        services.AddScoped<IAuditLogRepository>(sp =>
                            new AuditLogRepository(sp.GetRequiredService<ScadaBridgeDbContext>()));
                        services.AddSingleton<ICentralAuditWriter>(sp =>
                            new CentralAuditWriter(sp, NullLogger<CentralAuditWriter>.Instance));

                        services.AddRouting();
                        services.AddAuthorization();
                        services.AddAuthentication("TestScheme")
                            .AddScheme<AuthenticationSchemeOptions, AlwaysAuthenticatedHandler>(
                                "TestScheme", _ => { });
                    })
                    .Configure(app =>
                    {
                        // Mirror production order: routing → auth → audit
                        // middleware → endpoint. The auth scheme always
                        // succeeds; per-request auth-failure semantics are
                        // produced INSIDE the endpoint handler (mirroring the
                        // shared ZB.MOM.WW.Auth.ApiKeys verifier's in-handler
                        // short-circuit).
                        app.UseRouting();
                        app.UseAuthentication();
                        app.UseAuthorization();
                        app.UseAuditWriteMiddleware();
                        app.UseEndpoints(endpoints =>
                        {
                            endpoints.MapPost("/api/{methodName}", endpointHandler);
                        });
                    });
            });

        return await hostBuilder.StartAsync();
    }

    /// <summary>
    /// Minimal authentication handler that always succeeds — keeps
    /// <see cref="HttpContext.User"/> populated so the middleware's
    /// Items-then-User fallback path has a real principal to ignore. The
    /// middleware's primary actor resolution path uses
    /// <see cref="AuditWriteMiddleware.AuditActorItemKey"/> so this handler's
    /// claim never appears on the emitted Actor unless the endpoint stashes
    /// it explicitly.
    /// </summary>
    private sealed class AlwaysAuthenticatedHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public AlwaysAuthenticatedHandler(
            Microsoft.Extensions.Options.IOptionsMonitor<AuthenticationSchemeOptions> options,
            Microsoft.Extensions.Logging.ILoggerFactory logger,
            UrlEncoder encoder)
            : base(options, logger, encoder) { }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            var identity = new ClaimsIdentity(
                new[] { new Claim(ClaimTypes.Name, "framework-user") }, "TestScheme");
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, "TestScheme");
            return Task.FromResult(AuthenticateResult.Success(ticket));
        }
    }

    /// <summary>
    /// Queries the central <c>AuditLog</c> table for the row produced by the
    /// test's unique method name. Wrapped in <see cref="Assert"/> calls so the
    /// query can be used inside a polling helper.
    /// </summary>
    private async Task<IReadOnlyList<AuditEvent>> QueryByTargetAsync(string methodName)
    {
        await using var ctx = CreateContext();
        var repo = new AuditLogRepository(ctx);
        return await repo.QueryAsync(
            new AuditLogQueryFilter(Target: methodName),
            new AuditLogPaging(PageSize: 10));
    }

    /// <summary>
    /// Awaits the central <c>AuditLog</c> row materialising for
    /// <paramref name="methodName"/>. The writer is fire-and-forget so we
    /// poll briefly after the HTTP response returns to absorb scheduling
    /// jitter between the middleware's <c>finally</c> block and the row hitting
    /// MSSQL.
    /// </summary>
    private async Task<AuditEvent> AwaitOneAsync(string methodName)
    {
        var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(10);
        while (DateTime.UtcNow < deadline)
        {
            var rows = await QueryByTargetAsync(methodName);
            if (rows.Count > 0)
            {
                return Assert.Single(rows);
            }
            await Task.Delay(100);
        }

        // Fall through to a final query so the failure message carries the
        // actual row count from the last attempt.
        var finalRows = await QueryByTargetAsync(methodName);
        return Assert.Single(finalRows);
    }

    [SkippableFact]
    public async Task PostToApi_WithValidActor_Emits_InboundRequest_StatusDelivered_HttpStatus200_ActorPopulated()
    {
        Skip.IfNot(_fixture.Available, _fixture.SkipReason);
        var methodName = NewMethodName("echo");

        using var host = await BuildHostAsync(async ctx =>
        {
            // Simulate the production endpoint stashing the resolved API key
            // name on HttpContext.Items AFTER successful auth — the middleware
            // reads it in its finally block to populate Actor.
            ctx.Items[AuditWriteMiddleware.AuditActorItemKey] = "integration-svc";
            ctx.Response.StatusCode = 200;
            await ctx.Response.WriteAsync("ok");
        });

        var client = host.GetTestClient();
        var resp = await client.PostAsync(
            $"/api/{methodName}",
            new StringContent("{\"x\":1}", Encoding.UTF8, "application/json"));
        Assert.Equal(System.Net.HttpStatusCode.OK, resp.StatusCode);

        var evt = await AwaitOneAsync(methodName);
        Assert.Equal(AuditChannel.ApiInbound, evt.AsRow().Channel);
        Assert.Equal(AuditKind.InboundRequest, evt.AsRow().Kind);
        Assert.Equal(AuditStatus.Delivered, evt.AsRow().Status);
        Assert.Equal(200, evt.AsRow().HttpStatus);
        Assert.Equal("integration-svc", evt.Actor);
        // IngestedAtUtc stamped by the central writer.
        Assert.NotNull(evt.AsRow().IngestedAtUtc);
    }

    [SkippableFact]
    public async Task PostToApi_Without_Auth_Emits_InboundAuthFailure_StatusFailed_HttpStatus401_ActorNull()
    {
        Skip.IfNot(_fixture.Available, _fixture.SkipReason);
        var methodName = NewMethodName("auth-fail");

        using var host = await BuildHostAsync(async ctx =>
        {
            // The production inbound endpoint returns 401 from inside the
            // handler when the Bearer token is missing or fails verification;
            // the handler must NOT stash an actor name in that case so the
            // middleware emits Actor=null on the resulting audit row.
            ctx.Response.StatusCode = 401;
            await ctx.Response.WriteAsync("unauthorized");
        });

        var client = host.GetTestClient();
        var resp = await client.PostAsync(
            $"/api/{methodName}",
            new StringContent("{}", Encoding.UTF8, "application/json"));
        Assert.Equal(System.Net.HttpStatusCode.Unauthorized, resp.StatusCode);

        var evt = await AwaitOneAsync(methodName);
        Assert.Equal(AuditChannel.ApiInbound, evt.AsRow().Channel);
        Assert.Equal(AuditKind.InboundAuthFailure, evt.AsRow().Kind);
        Assert.Equal(AuditStatus.Failed, evt.AsRow().Status);
        Assert.Equal(401, evt.AsRow().HttpStatus);
        // Never echo back an unauthenticated principal — middleware suppresses
        // the framework user resolution on 401/403 paths. C3 (Task 2.5): the
        // canonical Actor is a non-null string (empty when absent); the row view
        // maps empty → null, preserving the "no principal" assertion.
        Assert.Null(evt.AsRow().Actor);
    }

    [SkippableFact]
    public async Task PostToApi_Returning500_Emits_InboundRequest_StatusFailed_HttpStatus500()
    {
        Skip.IfNot(_fixture.Available, _fixture.SkipReason);
        var methodName = NewMethodName("server-error");

        using var host = await BuildHostAsync(async ctx =>
        {
            // A handler-returned 500 (not a throw) — auth succeeded so Actor
            // resolution is still allowed; the audit row's Kind stays
            // InboundRequest (not InboundAuthFailure) and Status flips to
            // Failed because the response is not a 2xx.
            ctx.Items[AuditWriteMiddleware.AuditActorItemKey] = "integration-svc";
            ctx.Response.StatusCode = 500;
            await ctx.Response.WriteAsync("kaboom");
        });

        var client = host.GetTestClient();
        var resp = await client.PostAsync(
            $"/api/{methodName}",
            new StringContent("{}", Encoding.UTF8, "application/json"));
        Assert.Equal(System.Net.HttpStatusCode.InternalServerError, resp.StatusCode);

        var evt = await AwaitOneAsync(methodName);
        Assert.Equal(AuditChannel.ApiInbound, evt.AsRow().Channel);
        Assert.Equal(AuditKind.InboundRequest, evt.AsRow().Kind);
        Assert.Equal(AuditStatus.Failed, evt.AsRow().Status);
        Assert.Equal(500, evt.AsRow().HttpStatus);
        Assert.Equal("integration-svc", evt.Actor);
    }
}
