using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Claims;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using OpenIddict.Abstractions;
using OpenIddict.Extensions;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
using StellaOps.Auth.Abstractions;
using StellaOps.Authority.OpenIddict;
using StellaOps.Authority.OpenIddict.Handlers;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Storage.Mongo.Documents;
using StellaOps.Authority.Storage.Mongo.Stores;
using StellaOps.Authority.RateLimiting;
using StellaOps.Cryptography.Audit;
using Xunit;
using MongoDB.Bson;
using static StellaOps.Authority.Tests.OpenIddict.TestHelpers;

namespace StellaOps.Authority.Tests.OpenIddict;

public class ClientCredentialsHandlersTests
{
    private static readonly ActivitySource TestActivitySource = new("StellaOps.Authority.Tests");

    [Fact]
    public async Task ValidateClientCredentials_Rejects_WhenScopeNotAllowed()
    {
        var clientDocument = CreateClient(
            secret: "s3cr3t!",
            allowedGrantTypes: "client_credentials",
            allowedScopes: "jobs:read");

        var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
        var handler = new ValidateClientCredentialsHandler(
            new TestClientStore(clientDocument),
            registry,
            TestActivitySource,
            new TestAuthEventSink(),
            new TestRateLimiterMetadataAccessor(),
            TimeProvider.System,
            NullLogger<ValidateClientCredentialsHandler>.Instance);

        var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:write");
        var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);

        await handler.HandleAsync(context);

        Assert.True(context.IsRejected);
        Assert.Equal(OpenIddictConstants.Errors.InvalidScope, context.Error);
        Assert.Equal("Scope 'jobs:write' is not allowed for this client.", context.ErrorDescription);
    }

    [Fact]
    public async Task ValidateClientCredentials_Allows_WhenConfigurationMatches()
    {
        var clientDocument = CreateClient(
            secret: "s3cr3t!",
            allowedGrantTypes: "client_credentials",
            allowedScopes: "jobs:read jobs:trigger");

        var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
        var handler = new ValidateClientCredentialsHandler(
            new TestClientStore(clientDocument),
            registry,
            TestActivitySource,
            new TestAuthEventSink(),
            new TestRateLimiterMetadataAccessor(),
            TimeProvider.System,
            NullLogger<ValidateClientCredentialsHandler>.Instance);

        var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
        var context = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);

        await handler.HandleAsync(context);

        Assert.False(context.IsRejected, $"Rejected: {context.Error} - {context.ErrorDescription}");
        Assert.Same(clientDocument, context.Transaction.Properties[AuthorityOpenIddictConstants.ClientTransactionProperty]);

        var grantedScopes = Assert.IsType<string[]>(context.Transaction.Properties[AuthorityOpenIddictConstants.ClientGrantedScopesProperty]);
        Assert.Equal(new[] { "jobs:read" }, grantedScopes);
        Assert.Equal(clientDocument.Plugin, context.Transaction.Properties[AuthorityOpenIddictConstants.ClientProviderTransactionProperty]);
    }

    [Fact]
    public async Task ValidateClientCredentials_EmitsTamperAuditEvent_WhenUnexpectedParametersPresent()
    {
        var clientDocument = CreateClient(
            secret: "s3cr3t!",
            allowedGrantTypes: "client_credentials",
            allowedScopes: "jobs:read");

        var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(clientDocument));
        var sink = new TestAuthEventSink();
        var handler = new ValidateClientCredentialsHandler(
            new TestClientStore(clientDocument),
            registry,
            TestActivitySource,
            sink,
            new TestRateLimiterMetadataAccessor(),
            TimeProvider.System,
            NullLogger<ValidateClientCredentialsHandler>.Instance);

        var transaction = CreateTokenTransaction(clientDocument.ClientId, "s3cr3t!", scope: "jobs:read");
        transaction.Request?.SetParameter("unexpected_param", "value");

        await handler.HandleAsync(new OpenIddictServerEvents.ValidateTokenRequestContext(transaction));

        var tamperEvent = Assert.Single(sink.Events, record => record.EventType == "authority.token.tamper");
        Assert.Contains(tamperEvent.Properties, property =>
            string.Equals(property.Name, "request.unexpected_parameter", StringComparison.OrdinalIgnoreCase) &&
            string.Equals(property.Value.Value, "unexpected_param", StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public async Task HandleClientCredentials_PersistsTokenAndEnrichesClaims()
    {
        var clientDocument = CreateClient(
            secret: null,
            clientType: "public",
            allowedGrantTypes: "client_credentials",
            allowedScopes: "jobs:trigger");

        var descriptor = CreateDescriptor(clientDocument);
        var registry = CreateRegistry(withClientProvisioning: true, clientDescriptor: descriptor);
        var tokenStore = new TestTokenStore();
        var authSink = new TestAuthEventSink();
        var metadataAccessor = new TestRateLimiterMetadataAccessor();
        var validateHandler = new ValidateClientCredentialsHandler(
            new TestClientStore(clientDocument),
            registry,
            TestActivitySource,
            authSink,
            metadataAccessor,
            TimeProvider.System,
            NullLogger<ValidateClientCredentialsHandler>.Instance);

        var transaction = CreateTokenTransaction(clientDocument.ClientId, secret: null, scope: "jobs:trigger");
        transaction.Options.AccessTokenLifetime = TimeSpan.FromMinutes(30);

        var validateContext = new OpenIddictServerEvents.ValidateTokenRequestContext(transaction);
        await validateHandler.HandleAsync(validateContext);
        Assert.False(validateContext.IsRejected);

        var handler = new HandleClientCredentialsHandler(
            registry,
            tokenStore,
            TimeProvider.System,
            TestActivitySource,
            NullLogger<HandleClientCredentialsHandler>.Instance);
        var persistHandler = new PersistTokensHandler(tokenStore, TimeProvider.System, TestActivitySource, NullLogger<PersistTokensHandler>.Instance);

        var context = new OpenIddictServerEvents.HandleTokenRequestContext(transaction);

        await handler.HandleAsync(context);

        Assert.True(context.IsRequestHandled);
        Assert.NotNull(context.Principal);

        Assert.Contains(authSink.Events, record => record.EventType == "authority.client_credentials.grant" && record.Outcome == AuthEventOutcome.Success);

        var identityProviderClaim = context.Principal?.GetClaim(StellaOpsClaimTypes.IdentityProvider);
        Assert.Equal(clientDocument.Plugin, identityProviderClaim);

        var principal = context.Principal ?? throw new InvalidOperationException("Principal missing");
        var tokenId = principal.GetClaim(OpenIddictConstants.Claims.JwtId);
        Assert.False(string.IsNullOrWhiteSpace(tokenId));

        var signInContext = new OpenIddictServerEvents.ProcessSignInContext(transaction)
        {
            Principal = principal,
            AccessTokenPrincipal = principal
        };

        await persistHandler.HandleAsync(signInContext);

        var persisted = Assert.IsType<AuthorityTokenDocument>(tokenStore.Inserted);
        Assert.Equal(tokenId, persisted.TokenId);
        Assert.Equal(clientDocument.ClientId, persisted.ClientId);
        Assert.Equal("valid", persisted.Status);
        Assert.Equal(new[] { "jobs:trigger" }, persisted.Scope);
    }
}

public class TokenValidationHandlersTests
{
    private static readonly ActivitySource TestActivitySource = new("StellaOps.Authority.Tests.TokenValidation");

    [Fact]
    public async Task ValidateAccessTokenHandler_Rejects_WhenTokenRevoked()
    {
        var tokenStore = new TestTokenStore();
        tokenStore.Inserted = new AuthorityTokenDocument
        {
            TokenId = "token-1",
            Status = "revoked",
            ClientId = "feedser"
        };

        var metadataAccessor = new TestRateLimiterMetadataAccessor();
        var auditSink = new TestAuthEventSink();
        var handler = new ValidateAccessTokenHandler(
            tokenStore,
            new TestClientStore(CreateClient()),
            CreateRegistry(withClientProvisioning: true, clientDescriptor: CreateDescriptor(CreateClient())),
            metadataAccessor,
            auditSink,
            TimeProvider.System,
            TestActivitySource,
            NullLogger<ValidateAccessTokenHandler>.Instance);

        var transaction = new OpenIddictServerTransaction
        {
            Options = new OpenIddictServerOptions(),
            EndpointType = OpenIddictServerEndpointType.Token,
            Request = new OpenIddictRequest()
        };

        var principal = CreatePrincipal("feedser", "token-1", "standard");
        var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
        {
            Principal = principal,
            TokenId = "token-1"
        };

        await handler.HandleAsync(context);

        Assert.True(context.IsRejected);
        Assert.Equal(OpenIddictConstants.Errors.InvalidToken, context.Error);
    }

    [Fact]
    public async Task ValidateAccessTokenHandler_EnrichesClaims_WhenProviderAvailable()
    {
        var clientDocument = CreateClient();
        var userDescriptor = new AuthorityUserDescriptor("user-1", "alice", displayName: "Alice", requiresPasswordReset: false);

        var plugin = CreatePlugin(
            name: "standard",
            supportsClientProvisioning: true,
            descriptor: CreateDescriptor(clientDocument),
            user: userDescriptor);

        var registry = new AuthorityIdentityProviderRegistry(new[] { plugin }, NullLogger<AuthorityIdentityProviderRegistry>.Instance);

        var metadataAccessorSuccess = new TestRateLimiterMetadataAccessor();
        var auditSinkSuccess = new TestAuthEventSink();
        var handler = new ValidateAccessTokenHandler(
            new TestTokenStore(),
            new TestClientStore(clientDocument),
            registry,
            metadataAccessorSuccess,
            auditSinkSuccess,
            TimeProvider.System,
            TestActivitySource,
            NullLogger<ValidateAccessTokenHandler>.Instance);

        var transaction = new OpenIddictServerTransaction
        {
            Options = new OpenIddictServerOptions(),
            EndpointType = OpenIddictServerEndpointType.Token,
            Request = new OpenIddictRequest()
        };

        var principal = CreatePrincipal(clientDocument.ClientId, "token-123", plugin.Name, subject: userDescriptor.SubjectId);
        var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
        {
            Principal = principal
        };

        await handler.HandleAsync(context);

        Assert.False(context.IsRejected);
        Assert.Contains(principal.Claims, claim => claim.Type == "enriched" && claim.Value == "true");
    }

    [Fact]
    public async Task ValidateAccessTokenHandler_EmitsReplayAudit_WhenStoreDetectsSuspectedReplay()
    {
        var tokenStore = new TestTokenStore();
        tokenStore.Inserted = new AuthorityTokenDocument
        {
            TokenId = "token-replay",
            Status = "valid",
            ClientId = "agent",
            Devices = new List<BsonDocument>
            {
                new BsonDocument
                {
                    { "remoteAddress", "10.0.0.1" },
                    { "userAgent", "agent/1.0" },
                    { "firstSeen", BsonDateTime.Create(DateTimeOffset.UtcNow.AddMinutes(-15)) },
                    { "lastSeen", BsonDateTime.Create(DateTimeOffset.UtcNow.AddMinutes(-5)) },
                    { "useCount", 2 }
                }
            }
        };

        tokenStore.UsageCallback = (remote, agent) => new TokenUsageUpdateResult(TokenUsageUpdateStatus.SuspectedReplay, remote, agent);

        var metadataAccessor = new TestRateLimiterMetadataAccessor();
        var metadata = metadataAccessor.GetMetadata();
        if (metadata is not null)
        {
            metadata.RemoteIp = "203.0.113.7";
            metadata.UserAgent = "agent/2.0";
        }

        var clientDocument = CreateClient();
        clientDocument.ClientId = "agent";
        var auditSink = new TestAuthEventSink();
        var registry = CreateRegistry(withClientProvisioning: false, clientDescriptor: null);
        var handler = new ValidateAccessTokenHandler(
            tokenStore,
            new TestClientStore(clientDocument),
            registry,
            metadataAccessor,
            auditSink,
            TimeProvider.System,
            TestActivitySource,
            NullLogger<ValidateAccessTokenHandler>.Instance);

        var transaction = new OpenIddictServerTransaction
        {
            Options = new OpenIddictServerOptions(),
            EndpointType = OpenIddictServerEndpointType.Introspection,
            Request = new OpenIddictRequest()
        };

        var principal = CreatePrincipal("agent", "token-replay", "standard");
        var context = new OpenIddictServerEvents.ValidateTokenContext(transaction)
        {
            Principal = principal,
            TokenId = "token-replay"
        };

        await handler.HandleAsync(context);

        Assert.False(context.IsRejected);
        var replayEvent = Assert.Single(auditSink.Events, record => record.EventType == "authority.token.replay.suspected");
        Assert.Equal(AuthEventOutcome.Error, replayEvent.Outcome);
        Assert.NotNull(replayEvent.Network);
        Assert.Equal("203.0.113.7", replayEvent.Network?.RemoteAddress.Value);
        Assert.Contains(replayEvent.Properties, property => property.Name == "token.devices.total");
    }
}

internal sealed class TestClientStore : IAuthorityClientStore
{
    private readonly Dictionary<string, AuthorityClientDocument> clients = new(StringComparer.OrdinalIgnoreCase);

    public TestClientStore(params AuthorityClientDocument[] documents)
    {
        foreach (var document in documents)
        {
            clients[document.ClientId] = document;
        }
    }

    public ValueTask<AuthorityClientDocument?> FindByClientIdAsync(string clientId, CancellationToken cancellationToken)
    {
        clients.TryGetValue(clientId, out var document);
        return ValueTask.FromResult(document);
    }

    public ValueTask UpsertAsync(AuthorityClientDocument document, CancellationToken cancellationToken)
    {
        clients[document.ClientId] = document;
        return ValueTask.CompletedTask;
    }

    public ValueTask<bool> DeleteByClientIdAsync(string clientId, CancellationToken cancellationToken)
        => ValueTask.FromResult(clients.Remove(clientId));
}

internal sealed class TestTokenStore : IAuthorityTokenStore
{
    public AuthorityTokenDocument? Inserted { get; set; }

    public Func<string?, string?, TokenUsageUpdateResult>? UsageCallback { get; set; }

    public ValueTask InsertAsync(AuthorityTokenDocument document, CancellationToken cancellationToken)
    {
        Inserted = document;
        return ValueTask.CompletedTask;
    }

    public ValueTask<AuthorityTokenDocument?> FindByTokenIdAsync(string tokenId, CancellationToken cancellationToken)
        => ValueTask.FromResult(Inserted is not null && string.Equals(Inserted.TokenId, tokenId, StringComparison.OrdinalIgnoreCase) ? Inserted : null);

    public ValueTask<AuthorityTokenDocument?> FindByReferenceIdAsync(string referenceId, CancellationToken cancellationToken)
        => ValueTask.FromResult<AuthorityTokenDocument?>(null);

    public ValueTask UpdateStatusAsync(string tokenId, string status, DateTimeOffset? revokedAt, string? reason, string? reasonDescription, IReadOnlyDictionary<string, string?>? metadata, CancellationToken cancellationToken)
        => ValueTask.CompletedTask;

    public ValueTask<long> DeleteExpiredAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
        => ValueTask.FromResult(0L);

    public ValueTask<TokenUsageUpdateResult> RecordUsageAsync(string tokenId, string? remoteAddress, string? userAgent, DateTimeOffset observedAt, CancellationToken cancellationToken)
        => ValueTask.FromResult(UsageCallback?.Invoke(remoteAddress, userAgent) ?? new TokenUsageUpdateResult(TokenUsageUpdateStatus.Recorded, remoteAddress, userAgent));

    public ValueTask<IReadOnlyList<AuthorityTokenDocument>> ListRevokedAsync(DateTimeOffset? issuedAfter, CancellationToken cancellationToken)
        => ValueTask.FromResult<IReadOnlyList<AuthorityTokenDocument>>(Array.Empty<AuthorityTokenDocument>());
}

internal sealed class TestClaimsEnricher : IClaimsEnricher
{
    public ValueTask EnrichAsync(ClaimsIdentity identity, AuthorityClaimsEnrichmentContext context, CancellationToken cancellationToken)
    {
        if (!identity.HasClaim(c => c.Type == "enriched"))
        {
            identity.AddClaim(new Claim("enriched", "true"));
        }

        return ValueTask.CompletedTask;
    }
}

internal sealed class TestUserCredentialStore : IUserCredentialStore
{
    private readonly AuthorityUserDescriptor? user;

    public TestUserCredentialStore(AuthorityUserDescriptor? user)
    {
        this.user = user;
    }

    public ValueTask<AuthorityCredentialVerificationResult> VerifyPasswordAsync(string username, string password, CancellationToken cancellationToken)
        => ValueTask.FromResult(AuthorityCredentialVerificationResult.Failure(AuthorityCredentialFailureCode.InvalidCredentials));

    public ValueTask<AuthorityPluginOperationResult<AuthorityUserDescriptor>> UpsertUserAsync(AuthorityUserRegistration registration, CancellationToken cancellationToken)
        => ValueTask.FromResult(AuthorityPluginOperationResult<AuthorityUserDescriptor>.Failure("unsupported", "not implemented"));

    public ValueTask<AuthorityUserDescriptor?> FindBySubjectAsync(string subjectId, CancellationToken cancellationToken)
        => ValueTask.FromResult(user);
}

internal sealed class TestClientProvisioningStore : IClientProvisioningStore
{
    private readonly AuthorityClientDescriptor? descriptor;

    public TestClientProvisioningStore(AuthorityClientDescriptor? descriptor)
    {
        this.descriptor = descriptor;
    }

    public ValueTask<AuthorityPluginOperationResult<AuthorityClientDescriptor>> CreateOrUpdateAsync(AuthorityClientRegistration registration, CancellationToken cancellationToken)
        => ValueTask.FromResult(AuthorityPluginOperationResult<AuthorityClientDescriptor>.Failure("unsupported", "not implemented"));

    public ValueTask<AuthorityClientDescriptor?> FindByClientIdAsync(string clientId, CancellationToken cancellationToken)
        => ValueTask.FromResult(descriptor);

    public ValueTask<AuthorityPluginOperationResult> DeleteAsync(string clientId, CancellationToken cancellationToken)
        => ValueTask.FromResult(AuthorityPluginOperationResult.Success());
}

internal sealed class TestIdentityProviderPlugin : IIdentityProviderPlugin
{
    public TestIdentityProviderPlugin(
        AuthorityPluginContext context,
        IUserCredentialStore credentialStore,
        IClaimsEnricher claimsEnricher,
        IClientProvisioningStore? clientProvisioning,
        AuthorityIdentityProviderCapabilities capabilities)
    {
        Context = context;
        Credentials = credentialStore;
        ClaimsEnricher = claimsEnricher;
        ClientProvisioning = clientProvisioning;
        Capabilities = capabilities;
    }

    public string Name => Context.Manifest.Name;

    public string Type => Context.Manifest.Type;

    public AuthorityPluginContext Context { get; }

    public IUserCredentialStore Credentials { get; }

    public IClaimsEnricher ClaimsEnricher { get; }

    public IClientProvisioningStore? ClientProvisioning { get; }

    public AuthorityIdentityProviderCapabilities Capabilities { get; }

    public ValueTask<AuthorityPluginHealthResult> CheckHealthAsync(CancellationToken cancellationToken)
        => ValueTask.FromResult(AuthorityPluginHealthResult.Healthy());
}

internal sealed class TestAuthEventSink : IAuthEventSink
{
    public List<AuthEventRecord> Events { get; } = new();

    public ValueTask WriteAsync(AuthEventRecord record, CancellationToken cancellationToken)
    {
        Events.Add(record);
        return ValueTask.CompletedTask;
    }
}

internal sealed class TestRateLimiterMetadataAccessor : IAuthorityRateLimiterMetadataAccessor
{
    private readonly AuthorityRateLimiterMetadata metadata = new();

    public AuthorityRateLimiterMetadata? GetMetadata() => metadata;

    public void SetClientId(string? clientId) => metadata.ClientId = clientId;

    public void SetSubjectId(string? subjectId) => metadata.SubjectId = subjectId;

    public void SetTag(string name, string? value) => metadata.SetTag(name, value);
}

internal static class TestHelpers
{
    public static AuthorityClientDocument CreateClient(
        string? secret = "s3cr3t!",
        string clientType = "confidential",
        string allowedGrantTypes = "client_credentials",
        string allowedScopes = "jobs:read")
    {
        return new AuthorityClientDocument
        {
            ClientId = "feedser",
            ClientType = clientType,
            SecretHash = secret is null ? null : AuthoritySecretHasher.ComputeHash(secret),
            Plugin = "standard",
            Properties = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
            {
                [AuthorityClientMetadataKeys.AllowedGrantTypes] = allowedGrantTypes,
                [AuthorityClientMetadataKeys.AllowedScopes] = allowedScopes
            }
        };
    }

    public static AuthorityClientDescriptor CreateDescriptor(AuthorityClientDocument document)
    {
        var allowedGrantTypes = document.Properties.TryGetValue(AuthorityClientMetadataKeys.AllowedGrantTypes, out var grants) ? grants?.Split(' ', StringSplitOptions.RemoveEmptyEntries) : Array.Empty<string>();
        var allowedScopes = document.Properties.TryGetValue(AuthorityClientMetadataKeys.AllowedScopes, out var scopes) ? scopes?.Split(' ', StringSplitOptions.RemoveEmptyEntries) : Array.Empty<string>();

        return new AuthorityClientDescriptor(
            document.ClientId,
            document.DisplayName,
            confidential: string.Equals(document.ClientType, "confidential", StringComparison.OrdinalIgnoreCase),
            allowedGrantTypes,
            allowedScopes,
            redirectUris: Array.Empty<Uri>(),
            postLogoutRedirectUris: Array.Empty<Uri>(),
            properties: document.Properties);
    }

    public static AuthorityIdentityProviderRegistry CreateRegistry(bool withClientProvisioning, AuthorityClientDescriptor? clientDescriptor)
    {
        var plugin = CreatePlugin(
            name: "standard",
            supportsClientProvisioning: withClientProvisioning,
            descriptor: clientDescriptor,
            user: null);

        return new AuthorityIdentityProviderRegistry(new[] { plugin }, NullLogger<AuthorityIdentityProviderRegistry>.Instance);
    }

    public static TestIdentityProviderPlugin CreatePlugin(
        string name,
        bool supportsClientProvisioning,
        AuthorityClientDescriptor? descriptor,
        AuthorityUserDescriptor? user)
    {
        var capabilities = supportsClientProvisioning
            ? new[] { AuthorityPluginCapabilities.ClientProvisioning }
            : Array.Empty<string>();

        var manifest = new AuthorityPluginManifest(
            name,
            "standard",
            true,
            null,
            null,
            capabilities,
            new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase),
            $"{name}.yaml");

        var context = new AuthorityPluginContext(manifest, new ConfigurationBuilder().Build());

        return new TestIdentityProviderPlugin(
            context,
            new TestUserCredentialStore(user),
            new TestClaimsEnricher(),
            supportsClientProvisioning ? new TestClientProvisioningStore(descriptor) : null,
            new AuthorityIdentityProviderCapabilities(
                SupportsPassword: true,
                SupportsMfa: false,
                SupportsClientProvisioning: supportsClientProvisioning));
    }

    public static OpenIddictServerTransaction CreateTokenTransaction(string clientId, string? secret, string? scope)
    {
        var request = new OpenIddictRequest
        {
            GrantType = OpenIddictConstants.GrantTypes.ClientCredentials,
            ClientId = clientId,
            ClientSecret = secret
        };

        if (!string.IsNullOrWhiteSpace(scope))
        {
            request.Scope = scope;
        }

        return new OpenIddictServerTransaction
        {
            EndpointType = OpenIddictServerEndpointType.Token,
            Options = new OpenIddictServerOptions(),
            Request = request
        };
    }

    public static ClaimsPrincipal CreatePrincipal(string clientId, string tokenId, string provider, string? subject = null)
    {
        var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        identity.AddClaim(new Claim(OpenIddictConstants.Claims.ClientId, clientId));
        identity.AddClaim(new Claim(OpenIddictConstants.Claims.JwtId, tokenId));
        identity.AddClaim(new Claim(StellaOpsClaimTypes.IdentityProvider, provider));

        if (!string.IsNullOrWhiteSpace(subject))
        {
            identity.AddClaim(new Claim(OpenIddictConstants.Claims.Subject, subject));
        }

        return new ClaimsPrincipal(identity);
    }
}
