using System.Runtime.CompilerServices;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Grpc;
using ZB.MOM.WW.MxGateway.Server.Metrics;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
using ZB.MOM.WW.MxGateway.Server.Sessions;
using ZB.MOM.WW.MxGateway.Tests.TestSupport;

namespace ZB.MOM.WW.MxGateway.Tests.Security.Authorization;

public sealed class GatewayGrpcAuthorizationInterceptorTests
{
    /// <summary>Verifies that missing API key returns unauthenticated status.</summary>
    [Fact]
    public async Task UnaryServerHandler_MissingApiKey_ReturnsUnauthenticated()
    {
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(ApiKeyVerificationResult.Fail(
                ApiKeyVerificationFailure.MissingOrMalformedCredentials)),
            new GatewayRequestIdentityAccessor());

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            () => interceptor.UnaryServerHandler(
                new OpenSessionRequest(),
                new TestServerCallContext([]),
                (_, _) => Task.FromResult(new OpenSessionReply())));

        Assert.Equal(StatusCode.Unauthenticated, exception.StatusCode);
        Assert.DoesNotContain("secret", exception.Status.Detail, StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>Verifies that invalid API key error does not expose raw credentials.</summary>
    [Fact]
    public async Task UnaryServerHandler_InvalidApiKey_DoesNotExposeRawCredentialInStatus()
    {
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(ApiKeyVerificationResult.Fail(ApiKeyVerificationFailure.SecretMismatch)),
            new GatewayRequestIdentityAccessor());

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            () => interceptor.UnaryServerHandler(
                new OpenSessionRequest(),
                ContextWithAuthorization("Bearer mxgw_operator01_super-secret"),
                (_, _) => Task.FromResult(new OpenSessionReply())));

        Assert.Equal(StatusCode.Unauthenticated, exception.StatusCode);
        Assert.DoesNotContain("super-secret", exception.Status.Detail, StringComparison.Ordinal);
    }

    /// <summary>Verifies that valid key without required scope returns permission denied.</summary>
    [Fact]
    public async Task UnaryServerHandler_ValidApiKeyMissingScope_ReturnsPermissionDenied()
    {
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.EventsRead)),
            new GatewayRequestIdentityAccessor());

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            () => interceptor.UnaryServerHandler(
                new OpenSessionRequest(),
                ContextWithAuthorization("Bearer mxgw_operator01_secret"),
                (_, _) => Task.FromResult(new OpenSessionReply())));

        Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
        Assert.Contains(GatewayScopes.SessionOpen, exception.Status.Detail, StringComparison.Ordinal);
    }

    /// <summary>Verifies that valid key with scope sets request identity for the handler.</summary>
    [Fact]
    public async Task UnaryServerHandler_ValidApiKeyWithScope_SetsRequestIdentity()
    {
        GatewayRequestIdentityAccessor identityAccessor = new();
        ApiKeyIdentity? identitySeenByHandler = null;
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.SessionOpen)),
            identityAccessor);

        OpenSessionReply reply = await interceptor.UnaryServerHandler(
            new OpenSessionRequest(),
            ContextWithAuthorization("Bearer mxgw_operator01_secret"),
            (_, _) =>
            {
                identitySeenByHandler = identityAccessor.Current;

                return Task.FromResult(new OpenSessionReply { SessionId = "session-1" });
            });

        Assert.Equal("session-1", reply.SessionId);
        Assert.NotNull(identitySeenByHandler);
        Assert.Equal("operator01", identitySeenByHandler.KeyId);
        Assert.Null(identityAccessor.Current);
    }

    /// <summary>Verifies that server stream handler requires proper scope.</summary>
    [Fact]
    public async Task ServerStreamingServerHandler_ValidApiKeyMissingScope_ReturnsPermissionDenied()
    {
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.SessionOpen)),
            new GatewayRequestIdentityAccessor());

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            () => interceptor.ServerStreamingServerHandler(
                new StreamEventsRequest(),
                new RecordingServerStreamWriter<MxEvent>(),
                ContextWithAuthorization("Bearer mxgw_operator01_secret"),
                (_, _, _) => Task.CompletedTask));

        Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
        Assert.Contains(GatewayScopes.EventsRead, exception.Status.Detail, StringComparison.Ordinal);
    }

    /// <summary>Verifies that server stream handler allows streams with proper scope.</summary>
    [Fact]
    public async Task ServerStreamingServerHandler_ValidApiKeyWithScope_AllowsStream()
    {
        GatewayRequestIdentityAccessor identityAccessor = new();
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.EventsRead)),
            identityAccessor);
        RecordingServerStreamWriter<MxEvent> streamWriter = new();

        await interceptor.ServerStreamingServerHandler(
            new StreamEventsRequest(),
            streamWriter,
            ContextWithAuthorization("Bearer mxgw_operator01_secret"),
            async (_, writer, _) =>
            {
                Assert.Equal("operator01", identityAccessor.Current?.KeyId);
                await writer.WriteAsync(new MxEvent { SessionId = "session-1" });
            });

        MxEvent eventMessage = Assert.Single(streamWriter.Messages);
        Assert.Equal("session-1", eventMessage.SessionId);
        Assert.Null(identityAccessor.Current);
    }

    /// <summary>Verifies that disabled authentication skips API key verification.</summary>
    [Fact]
    public async Task UnaryServerHandler_AuthenticationDisabled_SkipsApiKeyVerification()
    {
        GatewayRequestIdentityAccessor identityAccessor = new();
        FakeApiKeyVerifier verifier = new(ApiKeyVerificationResult.Fail(
            ApiKeyVerificationFailure.MissingOrMalformedCredentials));
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            verifier,
            identityAccessor,
            AuthenticationMode.Disabled);

        OpenSessionReply reply = await interceptor.UnaryServerHandler(
            new OpenSessionRequest(),
            new TestServerCallContext([]),
            (_, _) => Task.FromResult(new OpenSessionReply { SessionId = "session-1" }));

        Assert.Equal("session-1", reply.SessionId);
        Assert.False(verifier.WasCalled);
        Assert.Null(identityAccessor.Current);
    }

    /// <summary>
    /// End-to-end composition test: runs an <c>OpenSession</c> call through the real
    /// interceptor in front of the real <see cref="MxAccessGatewayService"/> with a key
    /// that lacks the <c>session:open</c> scope, and asserts the interceptor denies the
    /// call with <see cref="StatusCode.PermissionDenied"/> before the service runs.
    /// </summary>
    [Fact]
    public async Task InterceptorComposedWithService_OpenSessionMissingScope_DeniesBeforeServiceRuns()
    {
        GatewayRequestIdentityAccessor identityAccessor = new();
        RecordingSessionManager sessionManager = new();
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.EventsRead)),
            identityAccessor);
        MxAccessGatewayService service = CreateService(sessionManager, identityAccessor);

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            () => interceptor.UnaryServerHandler(
                new OpenSessionRequest { ClientSessionName = "operator-session" },
                ContextWithAuthorization("Bearer mxgw_operator01_secret"),
                (request, context) => service.OpenSession(request, context)));

        Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
        Assert.Contains(GatewayScopes.SessionOpen, exception.Status.Detail, StringComparison.Ordinal);
        Assert.Equal(0, sessionManager.OpenSessionCount);
    }

    /// <summary>
    /// End-to-end composition test: runs an <c>OpenSession</c> call through the real
    /// interceptor in front of the real <see cref="MxAccessGatewayService"/> with a key
    /// that holds <c>session:open</c>, and asserts the service runs and observes the
    /// interceptor-supplied identity.
    /// </summary>
    [Fact]
    public async Task InterceptorComposedWithService_OpenSessionWithScope_RunsServiceWithIdentity()
    {
        GatewayRequestIdentityAccessor identityAccessor = new();
        RecordingSessionManager sessionManager = new();
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.SessionOpen)),
            identityAccessor);
        MxAccessGatewayService service = CreateService(sessionManager, identityAccessor);

        OpenSessionReply reply = await interceptor.UnaryServerHandler(
            new OpenSessionRequest { ClientSessionName = "operator-session" },
            ContextWithAuthorization("Bearer mxgw_operator01_secret"),
            (request, context) => service.OpenSession(request, context));

        Assert.Equal("session-1", reply.SessionId);
        Assert.Equal(1, sessionManager.OpenSessionCount);
        Assert.Equal("Operator Key", sessionManager.LastClientIdentity);
    }

    /// <summary>
    /// End-to-end composition test: an <c>Invoke</c> call through the real interceptor in
    /// front of the real service with a key holding only <c>invoke:read</c> is denied
    /// because the wrapped command is a write, confirming command-scope mapping is
    /// enforced through the full composition.
    /// </summary>
    [Fact]
    public async Task InterceptorComposedWithService_InvokeWriteCommandWithReadScope_DeniesBeforeServiceRuns()
    {
        GatewayRequestIdentityAccessor identityAccessor = new();
        RecordingSessionManager sessionManager = new();
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.InvokeRead)),
            identityAccessor);
        MxAccessGatewayService service = CreateService(sessionManager, identityAccessor);
        MxCommandRequest request = new()
        {
            SessionId = "session-1",
            Command = new MxCommand
            {
                Kind = MxCommandKind.Write,
                Write = new WriteCommand { ServerHandle = 1, ItemHandle = 2 },
            },
        };

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            () => interceptor.UnaryServerHandler(
                request,
                ContextWithAuthorization("Bearer mxgw_operator01_secret"),
                (req, context) => service.Invoke(req, context)));

        Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
        Assert.Contains(GatewayScopes.InvokeWrite, exception.Status.Detail, StringComparison.Ordinal);
        Assert.Equal(0, sessionManager.InvokeCount);
    }

    /// <summary>
    /// Verifies the interceptor denies <c>AcknowledgeAlarm</c> calls that lack
    /// <see cref="GatewayScopes.InvokeWrite"/>. Ack is a write-shaped mutation against
    /// alarm state, so it carries the same scope as <c>MxCommandKind.Write</c>.
    /// </summary>
    [Fact]
    public async Task UnaryServerHandler_AcknowledgeAlarmMissingScope_ReturnsPermissionDenied()
    {
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.InvokeRead)),
            new GatewayRequestIdentityAccessor());

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            () => interceptor.UnaryServerHandler(
                new AcknowledgeAlarmRequest { AlarmFullReference = "ref" },
                ContextWithAuthorization("Bearer mxgw_operator01_secret"),
                (_, _) => Task.FromResult(new AcknowledgeAlarmReply())));

        Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
        Assert.Contains(GatewayScopes.InvokeWrite, exception.Status.Detail, StringComparison.Ordinal);
    }

    /// <summary>Verifies that an API key holding <c>invoke:write</c> may call <c>AcknowledgeAlarm</c>.</summary>
    [Fact]
    public async Task UnaryServerHandler_AcknowledgeAlarmWithScope_RunsHandler()
    {
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.InvokeWrite)),
            new GatewayRequestIdentityAccessor());
        bool handlerRan = false;

        AcknowledgeAlarmReply reply = await interceptor.UnaryServerHandler(
            new AcknowledgeAlarmRequest { AlarmFullReference = "ref" },
            ContextWithAuthorization("Bearer mxgw_operator01_secret"),
            (_, _) =>
            {
                handlerRan = true;
                return Task.FromResult(new AcknowledgeAlarmReply());
            });

        Assert.NotNull(reply);
        Assert.True(handlerRan);
    }

    /// <summary>
    /// Verifies the interceptor denies <c>QueryActiveAlarms</c> server-streaming calls that
    /// lack <see cref="GatewayScopes.EventsRead"/>. Active-alarm snapshots are part of the
    /// alarm/event surface and share the same scope as <c>StreamEvents</c>.
    /// </summary>
    [Fact]
    public async Task ServerStreamingServerHandler_QueryActiveAlarmsMissingScope_ReturnsPermissionDenied()
    {
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.InvokeRead)),
            new GatewayRequestIdentityAccessor());

        RpcException exception = await Assert.ThrowsAsync<RpcException>(
            () => interceptor.ServerStreamingServerHandler(
                new StreamAlarmsRequest(),
                new RecordingServerStreamWriter<ActiveAlarmSnapshot>(),
                ContextWithAuthorization("Bearer mxgw_operator01_secret"),
                (_, _, _) => Task.CompletedTask));

        Assert.Equal(StatusCode.PermissionDenied, exception.StatusCode);
        Assert.Contains(GatewayScopes.EventsRead, exception.Status.Detail, StringComparison.Ordinal);
    }

    /// <summary>Verifies that an API key holding <c>events:read</c> may call <c>QueryActiveAlarms</c>.</summary>
    [Fact]
    public async Task ServerStreamingServerHandler_QueryActiveAlarmsWithScope_RunsHandler()
    {
        GatewayGrpcAuthorizationInterceptor interceptor = CreateInterceptor(
            new FakeApiKeyVerifier(SuccessWithScopes(GatewayScopes.EventsRead)),
            new GatewayRequestIdentityAccessor());
        RecordingServerStreamWriter<ActiveAlarmSnapshot> streamWriter = new();

        await interceptor.ServerStreamingServerHandler(
            new StreamAlarmsRequest(),
            streamWriter,
            ContextWithAuthorization("Bearer mxgw_operator01_secret"),
            async (_, writer, _) =>
            {
                await writer.WriteAsync(new ActiveAlarmSnapshot());
            });

        Assert.Single(streamWriter.Messages);
    }

    private static MxAccessGatewayService CreateService(
        ISessionManager sessionManager,
        IGatewayRequestIdentityAccessor identityAccessor)
    {
        return new MxAccessGatewayService(
            sessionManager,
            identityAccessor,
            new AllowAllConstraintEnforcer(),
            new MxAccessGrpcRequestValidator(),
            new MxAccessGrpcMapper(),
            new NoOpEventStreamService(),
            new GatewayMetrics(),
            NullLogger<MxAccessGatewayService>.Instance,
            new FakeGatewayAlarmService());
    }

    private static GatewayGrpcAuthorizationInterceptor CreateInterceptor(
        IApiKeyVerifier apiKeyVerifier,
        IGatewayRequestIdentityAccessor identityAccessor,
        AuthenticationMode authenticationMode = AuthenticationMode.ApiKey)
    {
        return new GatewayGrpcAuthorizationInterceptor(
            apiKeyVerifier,
            new GatewayGrpcScopeResolver(),
            identityAccessor,
            Options.Create(new GatewayOptions
            {
                Authentication = new AuthenticationOptions
                {
                    Mode = authenticationMode
                }
            }));
    }

    private static ApiKeyVerificationResult SuccessWithScopes(params string[] scopes)
    {
        return ApiKeyVerificationResult.Success(new ApiKeyIdentity(
            KeyId: "operator01",
            KeyPrefix: "mxgw_operator01",
            DisplayName: "Operator Key",
            Scopes: new HashSet<string>(scopes, StringComparer.Ordinal)));
    }

    private static TestServerCallContext ContextWithAuthorization(string authorizationHeader)
    {
        return new TestServerCallContext([new Metadata.Entry("authorization", authorizationHeader)]);
    }

    /// <summary>Records whether the gateway service ran past the interceptor for composition tests.</summary>
    private sealed class RecordingSessionManager : ISessionManager
    {
        /// <summary>Gets the number of times OpenSessionAsync was invoked.</summary>
        public int OpenSessionCount { get; private set; }

        /// <summary>Gets the number of times InvokeAsync was invoked.</summary>
        public int InvokeCount { get; private set; }

        /// <summary>Gets the last client identity passed to OpenSessionAsync.</summary>
        public string? LastClientIdentity { get; private set; }

        /// <inheritdoc />
        public Task<GatewaySession> OpenSessionAsync(
            SessionOpenRequest request,
            string? clientIdentity,
            CancellationToken cancellationToken)
        {
            OpenSessionCount++;
            LastClientIdentity = clientIdentity;

            GatewaySession session = new(
                "session-1",
                GatewayContractInfo.DefaultBackendName,
                "pipe",
                "nonce",
                clientIdentity ?? "client",
                "client-session",
                "client-correlation",
                TimeSpan.FromSeconds(7),
                TimeSpan.FromSeconds(30),
                TimeSpan.FromSeconds(10),
                DateTimeOffset.UtcNow);

            return Task.FromResult(session);
        }

        /// <inheritdoc />
        public bool TryGetSession(string sessionId, out GatewaySession session)
        {
            session = null!;
            return false;
        }

        /// <inheritdoc />
        public Task<WorkerCommandReply> InvokeAsync(
            string sessionId,
            WorkerCommand command,
            CancellationToken cancellationToken)
        {
            InvokeCount++;
            return Task.FromResult(new WorkerCommandReply());
        }

        /// <inheritdoc />
        public IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
            string sessionId,
            CancellationToken cancellationToken)
        {
            return AsyncEnumerable.Empty<WorkerEvent>();
        }

        /// <inheritdoc />
        public Task<SessionCloseResult> CloseSessionAsync(
            string sessionId,
            CancellationToken cancellationToken)
        {
            return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
        }

        /// <inheritdoc />
        public Task<SessionCloseResult> KillWorkerAsync(
            string sessionId,
            string reason,
            CancellationToken cancellationToken)
        {
            return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
        }

        /// <inheritdoc />
        public Task<int> CloseExpiredLeasesAsync(
            DateTimeOffset now,
            CancellationToken cancellationToken)
        {
            return Task.FromResult(0);
        }

        /// <inheritdoc />
        public Task ShutdownAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }

    /// <summary>Event stream service that yields nothing; alarm/event RPCs are not under test here.</summary>
    private sealed class NoOpEventStreamService : IEventStreamService
    {
        /// <inheritdoc />
        public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
            StreamEventsRequest request,
            [EnumeratorCancellation] CancellationToken cancellationToken)
        {
            await Task.CompletedTask;
            yield break;
        }
    }

    private sealed class FakeApiKeyVerifier(ApiKeyVerificationResult result) : IApiKeyVerifier
    {
        /// <summary>Gets whether the verifier was called.</summary>
        public bool WasCalled { get; private set; }

        /// <summary>Gets the last authorization header seen by the verifier.</summary>
        public string? LastAuthorizationHeader { get; private set; }

        /// <summary>Verifies the authorization header against stored result.</summary>
        /// <param name="authorizationHeader">The authorization header to verify.</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>Configured verification result.</returns>
        public Task<ApiKeyVerificationResult> VerifyAsync(
            string? authorizationHeader,
            CancellationToken cancellationToken)
        {
            WasCalled = true;
            LastAuthorizationHeader = authorizationHeader;

            return Task.FromResult(result);
        }
    }

}
