using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Server;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;

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

/// <summary>
/// F13c — verifies the impersonation handler routes UserName tokens through
/// <see cref="IOpcUaUserAuthenticator"/> and translates its result into the SDK's
/// <see cref="ImpersonateEventArgs"/> shape (granted identity vs. rejection status).
/// </summary>
public sealed class OpcUaApplicationHostImpersonationTests
{
    private static readonly UserTokenPolicy UserNamePolicy = new(UserTokenType.UserName) { PolicyId = "username_basic256sha256" };
    private static readonly UserTokenPolicy AnonPolicy = new(UserTokenType.Anonymous) { PolicyId = "anonymous" };

    /// <summary>Verifies successful UserName token impersonation sets identity and clears validation error.</summary>
    [Fact]
    public void HandleImpersonation_username_success_sets_identity_and_no_validation_error()
    {
        var token = new UserNameIdentityToken { UserName = "alice", DecryptedPassword = Encoding.UTF8.GetBytes("secret") };
        var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription());
        var authenticator = new RecordingAuthenticator(
            OpcUaUserAuthResult.Allow("Alice", new[] { "ReadOnly", "WriteOperate" }));

        OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger<object>.Instance);

        args.Identity.ShouldNotBeNull();
        args.IdentityValidationError.ShouldBeNull();
        authenticator.LastUsername.ShouldBe("alice");
        authenticator.LastPassword.ShouldBe("secret");
    }

    /// <summary>T17 — verifies the granted identity carries the authenticator's resolved roles
    /// (so a downstream method handler can read them off <c>context.UserIdentity</c> for the
    /// AlarmAck gate). The identity must be a <see cref="RoleCarryingUserIdentity"/> whose
    /// <see cref="RoleCarryingUserIdentity.Roles"/> equals <c>result.Roles</c>.</summary>
    [Fact]
    public void HandleImpersonation_username_success_carries_roles_on_identity()
    {
        var roles = new[] { "ReadOnly", "WriteOperate", "AlarmAck" };
        var token = new UserNameIdentityToken { UserName = "alice", DecryptedPassword = Encoding.UTF8.GetBytes("secret") };
        var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription());
        var authenticator = new RecordingAuthenticator(OpcUaUserAuthResult.Allow("Alice", roles));

        OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger<object>.Instance);

        var identity = args.Identity.ShouldBeOfType<RoleCarryingUserIdentity>();
        identity.Roles.ShouldBe(roles);
    }

    /// <summary>Verifies failed UserName token impersonation sets validation error and clears identity.</summary>
    [Fact]
    public void HandleImpersonation_username_denial_sets_validation_error_and_no_identity()
    {
        var token = new UserNameIdentityToken { UserName = "mallory", DecryptedPassword = Encoding.UTF8.GetBytes("wrong") };
        var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription());
        var authenticator = new RecordingAuthenticator(OpcUaUserAuthResult.Deny("Invalid credentials"));

        OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger<object>.Instance);

        args.Identity.ShouldBeNull();
        args.IdentityValidationError.Code.ShouldBe(StatusCodes.BadIdentityTokenRejected);
        args.IdentityValidationError.LocalizedText.Text.ShouldContain("Invalid credentials");
    }

    /// <summary>Verifies anonymous identity tokens are passed through to the SDK default handler.</summary>
    [Fact]
    public void HandleImpersonation_anonymous_token_falls_through_to_sdk_default()
    {
        var args = new ImpersonateEventArgs(new AnonymousIdentityToken(), AnonPolicy, new EndpointDescription());
        var authenticator = new RecordingAuthenticator(OpcUaUserAuthResult.Allow("x", Array.Empty<string>()));

        OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger<object>.Instance);

        // Handler leaves anonymous tokens untouched — no identity, no validation error.
        args.Identity.ShouldBeNull();
        args.IdentityValidationError.ShouldBeNull();
        authenticator.LastUsername.ShouldBeNull("anonymous tokens must not hit the authenticator");
    }

    /// <summary>Verifies authenticator exceptions result in rejection with validation error.</summary>
    [Fact]
    public void HandleImpersonation_authenticator_throwing_results_in_rejection()
    {
        var token = new UserNameIdentityToken { UserName = "bob", DecryptedPassword = Encoding.UTF8.GetBytes("x") };
        var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription());
        var authenticator = new ThrowingAuthenticator(new InvalidOperationException("LDAP unreachable"));

        OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger<object>.Instance);

        args.Identity.ShouldBeNull();
        args.IdentityValidationError.Code.ShouldBe(StatusCodes.BadIdentityTokenRejected);
    }

    /// <summary>Verifies null username is normalized to empty string before authenticator call.</summary>
    [Fact]
    public void HandleImpersonation_null_username_treated_as_empty_string()
    {
        var token = new UserNameIdentityToken { UserName = null, DecryptedPassword = Encoding.UTF8.GetBytes("abc") };
        var args = new ImpersonateEventArgs(token, UserNamePolicy, new EndpointDescription());
        var authenticator = new RecordingAuthenticator(OpcUaUserAuthResult.Deny("no user"));

        OpcUaApplicationHost.HandleImpersonation(authenticator, args, NullLogger<object>.Instance);

        authenticator.LastUsername.ShouldBe(string.Empty);
    }

    /// <summary>Verifies NullOpcUaUserAuthenticator always returns denial result.</summary>
    [Fact]
    public async Task NullOpcUaUserAuthenticator_always_denies()
    {
        var result = await NullOpcUaUserAuthenticator.Instance
            .AuthenticateUserNameAsync("anyone", "anything", CancellationToken.None);

        result.Success.ShouldBeFalse();
        result.Error.ShouldNotBeNull();
        result.Roles.ShouldBeEmpty();
    }

    private sealed class RecordingAuthenticator(OpcUaUserAuthResult outcome) : IOpcUaUserAuthenticator
    {
        /// <summary>Gets the username passed to the last authentication call.</summary>
        public string? LastUsername { get; private set; }
        /// <summary>Gets the password passed to the last authentication call.</summary>
        public string? LastPassword { get; private set; }

        /// <summary>Authenticates a username and password, recording them for inspection by tests.</summary>
        /// <param name="username">The username to authenticate.</param>
        /// <param name="password">The password to authenticate.</param>
        /// <param name="ct">The cancellation token.</param>
        /// <returns>The predefined authentication result.</returns>
        public Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string username, string password, CancellationToken ct)
        {
            LastUsername = username;
            LastPassword = password;
            return Task.FromResult(outcome);
        }
    }

    /// <summary>Test authenticator that throws an exception during authentication.</summary>
    private sealed class ThrowingAuthenticator(Exception ex) : IOpcUaUserAuthenticator
    {
        /// <summary>Authenticates by throwing the configured exception.</summary>
        /// <param name="username">The username to authenticate.</param>
        /// <param name="password">The password to authenticate.</param>
        /// <param name="ct">The cancellation token.</param>
        /// <returns>Never returns; always throws the configured exception.</returns>
        public Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string username, string password, CancellationToken ct)
            => Task.FromException<OpcUaUserAuthResult>(ex);
    }
}
