using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
using LdapTransport = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapTransport;
using LdapAuthFailure = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapAuthFailure;
using LibILdapAuthService = ZB.MOM.WW.Auth.Abstractions.Ldap.ILdapAuthService;
using LibLdapAuthResult = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapAuthResult;

namespace ZB.MOM.WW.OtOpcUa.Security.Tests;

/// <summary>
/// Task 1.2 — proves <see cref="OtOpcUaLdapAuthService"/> (the app's ILdapAuthService wrapper over
/// the shared <c>ZB.MOM.WW.Auth.Ldap</c> service) preserves the two app-only concerns the library
/// does not model: the <c>Enabled</c> master switch and the <c>DevStubMode</c> bypass. Both must
/// short-circuit WITHOUT delegating to the library. On the real path it adapts the library result
/// (groups, never roles) onto the app result shape with roles left for the downstream mapper.
/// </summary>
public sealed class OtOpcUaLdapAuthServiceTests
{
    private static OtOpcUaLdapAuthService Build(LdapOptions options, RecordingLibService inner) =>
        new(options, inner, NullLogger<OtOpcUaLdapAuthService>.Instance);

    /// <summary>DevStubMode on → stub Administrator success WITHOUT hitting the library.</summary>
    [Fact]
    public async Task DevStubMode_grants_Administrator_without_calling_the_library()
    {
        var inner = new RecordingLibService(LibLdapAuthResult.Fail(LdapAuthFailure.BadCredentials));
        var sut = Build(new LdapOptions { Enabled = true, DevStubMode = true }, inner);

        var result = await sut.AuthenticateAsync("anyone", "anything", CancellationToken.None);

        result.Success.ShouldBeTrue();
        result.Username.ShouldBe("anyone");
        result.Groups.ShouldBe(new[] { "dev" });
        result.Roles.ShouldBe(new[] { "Administrator" });
        inner.Called.ShouldBeFalse("DevStubMode must never reach the real directory client");
    }

    /// <summary>Enabled=false → denial, no library call (master switch wins over DevStubMode).</summary>
    [Fact]
    public async Task Disabled_denies_without_calling_the_library_even_with_devstub()
    {
        var inner = new RecordingLibService(LibLdapAuthResult.Success("x", "x", new[] { "g" }));
        var sut = Build(new LdapOptions { Enabled = false, DevStubMode = true }, inner);

        var result = await sut.AuthenticateAsync("user", "pw", CancellationToken.None);

        result.Success.ShouldBeFalse();
        result.Error.ShouldBe("LDAP authentication is disabled.");
        inner.Called.ShouldBeFalse("a disabled provider must never touch the network");
    }

    /// <summary>Real path: a library success surfaces its Groups; Roles are left empty for the
    /// downstream mapper (the library returns groups, not roles).</summary>
    [Fact]
    public async Task Real_path_success_surfaces_groups_and_leaves_roles_for_the_mapper()
    {
        var inner = new RecordingLibService(
            LibLdapAuthResult.Success("alice", "Alice User", new[] { "ReadOnly", "Engineers" }));
        var sut = Build(
            new LdapOptions { Enabled = true, DevStubMode = false, Transport = LdapTransport.Ldaps },
            inner);

        var result = await sut.AuthenticateAsync("alice", "secret", CancellationToken.None);

        inner.Called.ShouldBeTrue();
        result.Success.ShouldBeTrue();
        result.Username.ShouldBe("alice");
        result.DisplayName.ShouldBe("Alice User");
        result.Groups.ShouldBe(new[] { "ReadOnly", "Engineers" });
        result.Roles.ShouldBeEmpty();
    }

    /// <summary>Real path: a library failure folds into a fail-closed error string.</summary>
    [Fact]
    public async Task Real_path_failure_folds_into_error()
    {
        var inner = new RecordingLibService(LibLdapAuthResult.Fail(LdapAuthFailure.BadCredentials));
        var sut = Build(
            new LdapOptions { Enabled = true, DevStubMode = false, Transport = LdapTransport.Ldaps },
            inner);

        var result = await sut.AuthenticateAsync("alice", "wrong", CancellationToken.None);

        inner.Called.ShouldBeTrue();
        result.Success.ShouldBeFalse();
        result.Error.ShouldBe("Invalid username or password");
    }

    /// <summary>Insecure transport without AllowInsecure fails closed at the auth boundary WITHOUT
    /// reaching the library — preserving the bespoke service's login-time guard after UseTls→Transport.</summary>
    [Fact]
    public async Task Insecure_transport_without_AllowInsecure_fails_closed_without_calling_library()
    {
        var inner = new RecordingLibService(LibLdapAuthResult.Success("x", "x", new[] { "g" }));
        var sut = Build(
            new LdapOptions { Enabled = true, DevStubMode = false, Transport = LdapTransport.None, AllowInsecure = false },
            inner);

        var result = await sut.AuthenticateAsync("alice", "secret", CancellationToken.None);

        result.Success.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Error!.ShouldContain("Insecure LDAP is disabled");
        inner.Called.ShouldBeFalse();
    }

    /// <summary>Empty username/password are rejected up front without a library call.</summary>
    [Theory]
    [InlineData("", "pw")]
    [InlineData("user", "")]
    public async Task Empty_credentials_are_rejected_without_calling_library(string user, string pw)
    {
        var inner = new RecordingLibService(LibLdapAuthResult.Success("x", "x", new[] { "g" }));
        var sut = Build(new LdapOptions { Enabled = true, Transport = LdapTransport.Ldaps }, inner);

        var result = await sut.AuthenticateAsync(user, pw, CancellationToken.None);

        result.Success.ShouldBeFalse();
        inner.Called.ShouldBeFalse();
    }

    /// <summary>A system-side library failure (<c>ServiceAccountBindFailed</c> — the directory-unreachable
    /// bucket) adapts to <c>IsSystemFailure == true</c> so the OPC UA authenticator's outage circuit can
    /// distinguish it from a user-credential deny (03/S2).</summary>
    [Fact]
    public async Task ServiceAccountBindFailed_adapts_to_IsSystemFailure_true()
    {
        var inner = new RecordingLibService(LibLdapAuthResult.Fail(LdapAuthFailure.ServiceAccountBindFailed));
        var sut = Build(
            new LdapOptions { Enabled = true, DevStubMode = false, Transport = LdapTransport.Ldaps },
            inner);

        var result = await sut.AuthenticateAsync("alice", "secret", CancellationToken.None);

        result.Success.ShouldBeFalse();
        result.IsSystemFailure.ShouldBeTrue();
    }

    /// <summary>User-side library failures and success adapt to <c>IsSystemFailure == false</c> — they must
    /// NOT trip the outage circuit (03/S2).</summary>
    [Theory]
    [InlineData(LdapAuthFailure.BadCredentials)]
    [InlineData(LdapAuthFailure.UserNotFound)]
    public async Task User_side_failures_adapt_to_IsSystemFailure_false(LdapAuthFailure failure)
    {
        var inner = new RecordingLibService(LibLdapAuthResult.Fail(failure));
        var sut = Build(
            new LdapOptions { Enabled = true, DevStubMode = false, Transport = LdapTransport.Ldaps },
            inner);

        var result = await sut.AuthenticateAsync("alice", "wrong", CancellationToken.None);

        result.Success.ShouldBeFalse();
        result.IsSystemFailure.ShouldBeFalse();
    }

    /// <summary>A successful bind is never a system failure.</summary>
    [Fact]
    public async Task Success_is_not_a_system_failure()
    {
        var inner = new RecordingLibService(LibLdapAuthResult.Success("alice", "Alice", new[] { "ReadOnly" }));
        var sut = Build(
            new LdapOptions { Enabled = true, DevStubMode = false, Transport = LdapTransport.Ldaps },
            inner);

        var result = await sut.AuthenticateAsync("alice", "secret", CancellationToken.None);

        result.Success.ShouldBeTrue();
        result.IsSystemFailure.ShouldBeFalse();
    }

    /// <summary>Records whether the library service was invoked and returns a canned result.</summary>
    private sealed class RecordingLibService(LibLdapAuthResult result) : LibILdapAuthService
    {
        public bool Called { get; private set; }

        public Task<LibLdapAuthResult> AuthenticateAsync(string username, string password, CancellationToken ct)
        {
            Called = true;
            return Task.FromResult(result);
        }
    }
}
