using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
using Xunit;

namespace StellaOps.Cryptography.Tests;

public class CryptoProviderRegistryTests
{
    [Fact]
    public void ResolveOrThrow_RespectsPreferredProviderOrder()
    {
        var providerA = new FakeCryptoProvider("providerA")
            .WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256)
            .WithSigner(SignatureAlgorithms.Es256, "key-a");

        var providerB = new FakeCryptoProvider("providerB")
            .WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256)
            .WithSigner(SignatureAlgorithms.Es256, "key-b");

        var registry = new CryptoProviderRegistry(new[] { providerA, providerB }, new[] { "providerB" });

        var resolved = registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256);

        Assert.Same(providerB, resolved);
    }

    [Fact]
    public void ResolveSigner_UsesPreferredProviderHint()
    {
        var providerA = new FakeCryptoProvider("providerA")
            .WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256)
            .WithSigner(SignatureAlgorithms.Es256, "key-a");

        var providerB = new FakeCryptoProvider("providerB")
            .WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256)
            .WithSigner(SignatureAlgorithms.Es256, "key-b");

        var registry = new CryptoProviderRegistry(new[] { providerA, providerB }, Array.Empty<string>());

        var hintResolution = registry.ResolveSigner(
            CryptoCapability.Signing,
            SignatureAlgorithms.Es256,
            new CryptoKeyReference("key-b"),
            preferredProvider: "providerB");

        Assert.Equal("providerB", hintResolution.ProviderName);
        Assert.Equal("key-b", hintResolution.Signer.KeyId);

        var fallbackResolution = registry.ResolveSigner(
            CryptoCapability.Signing,
            SignatureAlgorithms.Es256,
            new CryptoKeyReference("key-a"));

        Assert.Equal("providerA", fallbackResolution.ProviderName);
        Assert.Equal("key-a", fallbackResolution.Signer.KeyId);
    }

    private sealed class FakeCryptoProvider : ICryptoProvider
    {
        private readonly Dictionary<string, FakeSigner> signers = new(StringComparer.Ordinal);
        private readonly HashSet<(CryptoCapability Capability, string Algorithm)> supported;

        public FakeCryptoProvider(string name)
        {
            Name = name;
            supported = new HashSet<(CryptoCapability, string)>(new CapabilityAlgorithmComparer());
        }

        public string Name { get; }

        public FakeCryptoProvider WithSupport(CryptoCapability capability, string algorithm)
        {
            supported.Add((capability, algorithm));
            return this;
        }

        public FakeCryptoProvider WithSigner(string algorithm, string keyId)
        {
            WithSupport(CryptoCapability.Signing, algorithm);
            var signer = new FakeSigner(Name, keyId, algorithm);
            signers[keyId] = signer;
            return this;
        }

        public bool Supports(CryptoCapability capability, string algorithmId)
            => supported.Contains((capability, algorithmId));

        public IPasswordHasher GetPasswordHasher(string algorithmId)
            => throw new NotSupportedException();

        public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
        {
            if (!signers.TryGetValue(keyReference.KeyId, out var signer))
            {
                throw new KeyNotFoundException();
            }

            if (!string.Equals(signer.AlgorithmId, algorithmId, StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException("Signer algorithm mismatch.");
            }

            return signer;
        }

        public void UpsertSigningKey(CryptoSigningKey signingKey)
            => signers[signingKey.Reference.KeyId] = new FakeSigner(Name, signingKey.Reference.KeyId, signingKey.AlgorithmId);

        public bool RemoveSigningKey(string keyId) => signers.Remove(keyId);

        public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys() => Array.Empty<CryptoSigningKey>();

        private sealed class CapabilityAlgorithmComparer : IEqualityComparer<(CryptoCapability Capability, string Algorithm)>
        {
            public bool Equals((CryptoCapability Capability, string Algorithm) x, (CryptoCapability Capability, string Algorithm) y)
                => x.Capability == y.Capability && string.Equals(x.Algorithm, y.Algorithm, StringComparison.OrdinalIgnoreCase);

            public int GetHashCode((CryptoCapability Capability, string Algorithm) obj)
                => HashCode.Combine(obj.Capability, obj.Algorithm.ToUpperInvariant());
        }
    }

    private sealed class FakeSigner : ICryptoSigner
    {
        public FakeSigner(string provider, string keyId, string algorithmId)
        {
            Provider = provider;
            KeyId = keyId;
            AlgorithmId = algorithmId;
        }

        public string Provider { get; }

        public string KeyId { get; }

        public string AlgorithmId { get; }

        public ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
            => ValueTask.FromResult(Array.Empty<byte>());

        public ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
            => ValueTask.FromResult(true);

        public JsonWebKey ExportPublicJsonWebKey() => new()
        {
            Kid = KeyId,
            Alg = AlgorithmId,
            Kty = JsonWebAlgorithmsKeyTypes.Octet,
            Use = JsonWebKeyUseNames.Sig
        };
    }
}
