using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NSubstitute;
using ZB.MOM.WW.Auth.Abstractions.Ldap;
using ZB.MOM.WW.ScadaBridge.ManagementService;
using ZB.MOM.WW.ScadaBridge.Security;
using ZB.MOM.WW.ScadaBridge.Security.Auth;

namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;

/// <summary>
/// Tests for the dev/test <c>DisableLogin</c> bypass on the management/CLI auth surface
/// (<see cref="ManagementAuthenticator"/>). The bypass lets the HTTP-Basic CLI surfaces
/// authenticate without LDAP when login is disabled — mirroring the interactive cookie
/// handler — so a login-disabled deployment is not locked out of the CLI.
/// </summary>
public class ManagementAuthenticatorTests
{
    private static HttpContext ContextWith(AuthDisableLoginOptions options)
    {
        var services = new ServiceCollection();
        services.AddSingleton<IOptions<AuthDisableLoginOptions>>(Options.Create(options));
        return new DefaultHttpContext { RequestServices = services.BuildServiceProvider() };
    }

    [Fact]
    public void TryDisableLoginUser_WhenDisabled_ReturnsNull()
    {
        var context = ContextWith(new AuthDisableLoginOptions { DisableLogin = false });

        Assert.Null(ManagementAuthenticator.TryDisableLoginUser(context));
    }

    [Fact]
    public void TryDisableLoginUser_WhenNoOptionsRegistered_ReturnsNull()
    {
        var context = new DefaultHttpContext
        {
            RequestServices = new ServiceCollection().BuildServiceProvider(),
        };

        Assert.Null(ManagementAuthenticator.TryDisableLoginUser(context));
    }

    [Fact]
    public void TryDisableLoginUser_WhenEnabled_ReturnsDevUserWithAllRolesSystemWide()
    {
        var context = ContextWith(new AuthDisableLoginOptions { DisableLogin = true, User = "ci-bot" });

        var user = ManagementAuthenticator.TryDisableLoginUser(context);

        Assert.NotNull(user);
        Assert.Equal("ci-bot", user!.Username);
        Assert.Equal("ci-bot", user.DisplayName);
        Assert.Equal(Roles.All, user.Roles);
        Assert.Empty(user.PermittedSiteIds); // empty == system-wide, mirrors AutoLoginAuthenticationHandler
    }

    [Fact]
    public void TryDisableLoginUser_WhenEnabledWithBlankUser_FallsBackToMultiRole()
    {
        var context = ContextWith(new AuthDisableLoginOptions { DisableLogin = true, User = "   " });

        var user = ManagementAuthenticator.TryDisableLoginUser(context);

        Assert.NotNull(user);
        Assert.Equal("multi-role", user!.Username);
    }

    [Fact]
    public async Task AuthenticateAsync_WhenDisabled_BypassesBasicAuthAndReturnsDevUser()
    {
        // No Authorization header and no ILdapAuthService registered: the bypass must short-circuit
        // before either is consulted.
        var context = ContextWith(new AuthDisableLoginOptions { DisableLogin = true });

        var outcome = await ManagementAuthenticator.AuthenticateAsync(context);

        Assert.Null(outcome.Failure);
        Assert.NotNull(outcome.User);
        Assert.Equal("multi-role", outcome.User!.Username);
        Assert.Equal(Roles.All, outcome.User.Roles);
    }

    [Fact]
    public async Task AuthenticateAsync_WhenEnabledWithNoHeader_ReturnsUnauthorized()
    {
        // Login NOT disabled and no Basic header: the real auth path rejects with a 401 result
        // before any LDAP lookup.
        var context = ContextWith(new AuthDisableLoginOptions { DisableLogin = false });

        var outcome = await ManagementAuthenticator.AuthenticateAsync(context);

        Assert.Null(outcome.User);
        Assert.NotNull(outcome.Failure);
    }

    // --- Login-throttle wiring (arch-review P1/UA5) -------------------------------------------

    /// <summary>
    /// Builds a real Basic-Auth request context wired with a <see cref="LoginThrottle"/>
    /// (over the injected <paramref name="clock"/>) and the supplied LDAP mock, with the given
    /// remote IP. Returns the context plus the resolvable throttle so tests can seed failures.
    /// </summary>
    private static (HttpContext Context, LoginThrottle Throttle) BasicAuthContext(
        string username,
        string password,
        ILdapAuthService ldap,
        TimeProvider clock,
        string remoteIp = "127.0.0.1")
    {
        var services = new ServiceCollection();
        services.AddSingleton<IOptions<AuthDisableLoginOptions>>(
            Options.Create(new AuthDisableLoginOptions { DisableLogin = false }));
        services.AddSingleton<IOptions<SecurityOptions>>(Options.Create(new SecurityOptions()));
        services.AddSingleton(clock);
        services.AddSingleton<LoginThrottle>();
        services.AddSingleton(ldap);
        services.AddLogging(); // JsonHttpResult.ExecuteAsync resolves ILoggerFactory
        var provider = services.BuildServiceProvider();

        var context = new DefaultHttpContext { RequestServices = provider };
        var creds = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
        context.Request.Headers.Authorization = $"Basic {creds}";
        context.Connection.RemoteIpAddress = System.Net.IPAddress.Parse(remoteIp);
        context.Response.Body = new MemoryStream();

        return (context, provider.GetRequiredService<LoginThrottle>());
    }

    private static async Task<(int Status, string Body)> ExecuteAsync(HttpContext context, IResult result)
    {
        await result.ExecuteAsync(context);
        context.Response.Body.Position = 0;
        using var reader = new StreamReader(context.Response.Body);
        var body = await reader.ReadToEndAsync();
        return (context.Response.StatusCode, body);
    }

    [Fact]
    public async Task AuthenticateAsync_LockedOutKey_Returns429WithoutLdapBind()
    {
        var ldap = Substitute.For<ILdapAuthService>();
        var (context, throttle) = BasicAuthContext("alice", "wrong", ldap, TimeProvider.System);
        for (var i = 0; i < 5; i++) throttle.RecordFailure("alice", "127.0.0.1");

        var outcome = await ManagementAuthenticator.AuthenticateAsync(context);

        Assert.Null(outcome.User);
        Assert.NotNull(outcome.Failure);
        var (status, body) = await ExecuteAsync(context, outcome.Failure!);
        Assert.Equal(429, status);
        Assert.Contains("AUTH_THROTTLED", body);
        await ldap.DidNotReceiveWithAnyArgs().AuthenticateAsync(default!, default!, default);
    }

    [Fact]
    public async Task AuthenticateAsync_FailedBind_RecordsFailure()
    {
        var ldap = Substitute.For<ILdapAuthService>();
        ldap.AuthenticateAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(LdapAuthResult.Fail(LdapAuthFailure.BadCredentials));
        var (context, throttle) = BasicAuthContext("alice", "wrong", ldap, TimeProvider.System);

        var outcome = await ManagementAuthenticator.AuthenticateAsync(context);

        // The bind failed → a 401 is returned AND the failure is recorded on the throttle.
        Assert.Null(outcome.User);
        Assert.NotNull(outcome.Failure);
        Assert.False(throttle.IsLockedOut("alice", "127.0.0.1"));
        for (var i = 0; i < 4; i++) throttle.RecordFailure("alice", "127.0.0.1");
        Assert.True(throttle.IsLockedOut("alice", "127.0.0.1"));
    }
}
