using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
using StellaOps.Authority.OpenIddict;
using StellaOps.Authority.OpenIddict.Handlers;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.RateLimiting;
using StellaOps.Cryptography.Audit;
using Xunit;

namespace StellaOps.Authority.Tests.OpenIddict;

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

    [Fact]
    public async Task HandlePasswordGrant_EmitsSuccessAuditEvent()
    {
        var sink = new TestAuthEventSink();
        var metadataAccessor = new TestRateLimiterMetadataAccessor();
        var registry = CreateRegistry(new SuccessCredentialStore());
        var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
        var handle = new HandlePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<HandlePasswordGrantHandler>.Instance);

        var transaction = CreatePasswordTransaction("alice", "Password1!");

        await validate.HandleAsync(new OpenIddictServerEvents.ValidateTokenRequestContext(transaction));
        await handle.HandleAsync(new OpenIddictServerEvents.HandleTokenRequestContext(transaction));

        Assert.Contains(sink.Events, record => record.EventType == "authority.password.grant" && record.Outcome == AuthEventOutcome.Success);
    }

    [Fact]
    public async Task HandlePasswordGrant_EmitsFailureAuditEvent()
    {
        var sink = new TestAuthEventSink();
        var metadataAccessor = new TestRateLimiterMetadataAccessor();
        var registry = CreateRegistry(new FailureCredentialStore());
        var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
        var handle = new HandlePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<HandlePasswordGrantHandler>.Instance);

        var transaction = CreatePasswordTransaction("alice", "BadPassword!");

        await validate.HandleAsync(new OpenIddictServerEvents.ValidateTokenRequestContext(transaction));
        await handle.HandleAsync(new OpenIddictServerEvents.HandleTokenRequestContext(transaction));

        Assert.Contains(sink.Events, record => record.EventType == "authority.password.grant" && record.Outcome == AuthEventOutcome.Failure);
    }

    [Fact]
    public async Task HandlePasswordGrant_EmitsLockoutAuditEvent()
    {
        var sink = new TestAuthEventSink();
        var metadataAccessor = new TestRateLimiterMetadataAccessor();
        var registry = CreateRegistry(new LockoutCredentialStore());
        var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);
        var handle = new HandlePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<HandlePasswordGrantHandler>.Instance);

        var transaction = CreatePasswordTransaction("alice", "Locked!");

        await validate.HandleAsync(new OpenIddictServerEvents.ValidateTokenRequestContext(transaction));
        await handle.HandleAsync(new OpenIddictServerEvents.HandleTokenRequestContext(transaction));

        Assert.Contains(sink.Events, record => record.EventType == "authority.password.grant" && record.Outcome == AuthEventOutcome.LockedOut);
    }

    [Fact]
    public async Task ValidatePasswordGrant_EmitsTamperAuditEvent_WhenUnexpectedParametersPresent()
    {
        var sink = new TestAuthEventSink();
        var metadataAccessor = new TestRateLimiterMetadataAccessor();
        var registry = CreateRegistry(new SuccessCredentialStore());
        var validate = new ValidatePasswordGrantHandler(registry, TestActivitySource, sink, metadataAccessor, TimeProvider.System, NullLogger<ValidatePasswordGrantHandler>.Instance);

        var transaction = CreatePasswordTransaction("alice", "Password1!");
        transaction.Request?.SetParameter("unexpected_param", "value");

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

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

    private static AuthorityIdentityProviderRegistry CreateRegistry(IUserCredentialStore store)
    {
        var plugin = new StubIdentityProviderPlugin("stub", store);
        return new AuthorityIdentityProviderRegistry(new[] { plugin }, NullLogger<AuthorityIdentityProviderRegistry>.Instance);
    }

    private static OpenIddictServerTransaction CreatePasswordTransaction(string username, string password)
    {
        var request = new OpenIddictRequest
        {
            GrantType = OpenIddictConstants.GrantTypes.Password,
            Username = username,
            Password = password
        };

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

    private sealed class StubIdentityProviderPlugin : IIdentityProviderPlugin
    {
        public StubIdentityProviderPlugin(string name, IUserCredentialStore store)
        {
            Name = name;
            Type = "stub";
            var manifest = new AuthorityPluginManifest(
                Name: name,
                Type: "stub",
                Enabled: true,
                AssemblyName: null,
                AssemblyPath: null,
                Capabilities: new[] { AuthorityPluginCapabilities.Password },
                Metadata: new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase),
                ConfigPath: $"{name}.yaml");
            Context = new AuthorityPluginContext(manifest, new ConfigurationBuilder().Build());
            Credentials = store;
            ClaimsEnricher = new NoopClaimsEnricher();
            Capabilities = new AuthorityIdentityProviderCapabilities(SupportsPassword: true, SupportsMfa: false, SupportsClientProvisioning: false);
        }

        public string Name { get; }
        public string Type { get; }
        public AuthorityPluginContext Context { get; }
        public IUserCredentialStore Credentials { get; }
        public IClaimsEnricher ClaimsEnricher { get; }
        public IClientProvisioningStore? ClientProvisioning => null;
        public AuthorityIdentityProviderCapabilities Capabilities { get; }

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

    private sealed class NoopClaimsEnricher : IClaimsEnricher
    {
        public ValueTask EnrichAsync(ClaimsIdentity identity, AuthorityClaimsEnrichmentContext context, CancellationToken cancellationToken)
            => ValueTask.CompletedTask;
    }

    private sealed class SuccessCredentialStore : IUserCredentialStore
    {
        public ValueTask<AuthorityCredentialVerificationResult> VerifyPasswordAsync(string username, string password, CancellationToken cancellationToken)
        {
            var descriptor = new AuthorityUserDescriptor("subject", username, "User", requiresPasswordReset: false);
            return ValueTask.FromResult(AuthorityCredentialVerificationResult.Success(descriptor));
        }

        public ValueTask<AuthorityPluginOperationResult<AuthorityUserDescriptor>> UpsertUserAsync(AuthorityUserRegistration registration, CancellationToken cancellationToken)
            => throw new NotImplementedException();

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

    private sealed class FailureCredentialStore : IUserCredentialStore
    {
        public ValueTask<AuthorityCredentialVerificationResult> VerifyPasswordAsync(string username, string password, CancellationToken cancellationToken)
            => ValueTask.FromResult(AuthorityCredentialVerificationResult.Failure(AuthorityCredentialFailureCode.InvalidCredentials, "Invalid username or password."));

        public ValueTask<AuthorityPluginOperationResult<AuthorityUserDescriptor>> UpsertUserAsync(AuthorityUserRegistration registration, CancellationToken cancellationToken)
            => throw new NotImplementedException();

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

    private sealed class LockoutCredentialStore : IUserCredentialStore
    {
        public ValueTask<AuthorityCredentialVerificationResult> VerifyPasswordAsync(string username, string password, CancellationToken cancellationToken)
        {
            var retry = TimeSpan.FromMinutes(5);
            var properties = new[]
            {
                new AuthEventProperty
                {
                    Name = "plugin.lockout_until",
                    Value = ClassifiedString.Public(timeProvider.GetUtcNow().Add(retry).ToString("O", CultureInfo.InvariantCulture))
                }
            };

            return ValueTask.FromResult(AuthorityCredentialVerificationResult.Failure(
                AuthorityCredentialFailureCode.LockedOut,
                "Account locked.",
                retryAfter: retry,
                auditProperties: properties));
        }

        private static readonly TimeProvider timeProvider = TimeProvider.System;

        public ValueTask<AuthorityPluginOperationResult<AuthorityUserDescriptor>> UpsertUserAsync(AuthorityUserRegistration registration, CancellationToken cancellationToken)
            => throw new NotImplementedException();

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

}
