using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Auth.Client;
using StellaOps.Scanner.Core.Security;
using Xunit;

namespace StellaOps.Scanner.Core.Tests.Security;

public sealed class AuthorityTokenSourceTests
{
    [Fact]
    public async Task GetAsync_ReusesCachedTokenUntilRefreshSkew()
    {
        var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
        var client = new FakeTokenClient(timeProvider);
        var source = new AuthorityTokenSource(client, TimeSpan.FromSeconds(30), timeProvider, NullLogger<AuthorityTokenSource>.Instance);

        var token1 = await source.GetAsync("scanner", new[] { "scanner.read" });
        Assert.Equal(1, client.RequestCount);

        var token2 = await source.GetAsync("scanner", new[] { "scanner.read" });
        Assert.Equal(1, client.RequestCount);
        Assert.Equal(token1.AccessToken, token2.AccessToken);

        timeProvider.Advance(TimeSpan.FromMinutes(3));
        var token3 = await source.GetAsync("scanner", new[] { "scanner.read" });
        Assert.Equal(2, client.RequestCount);
        Assert.NotEqual(token1.AccessToken, token3.AccessToken);
    }

    [Fact]
    public async Task InvalidateAsync_RemovesCachedToken()
    {
        var timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
        var client = new FakeTokenClient(timeProvider);
        var source = new AuthorityTokenSource(client, TimeSpan.FromSeconds(30), timeProvider, NullLogger<AuthorityTokenSource>.Instance);

        _ = await source.GetAsync("scanner", new[] { "scanner.read" });
        Assert.Equal(1, client.RequestCount);

        await source.InvalidateAsync("scanner", new[] { "scanner.read" });
        _ = await source.GetAsync("scanner", new[] { "scanner.read" });

        Assert.Equal(2, client.RequestCount);
    }

    private sealed class FakeTokenClient : IStellaOpsTokenClient
    {
        private readonly FakeTimeProvider timeProvider;
        private int counter;

        public FakeTokenClient(FakeTimeProvider timeProvider)
        {
            this.timeProvider = timeProvider;
        }

        public int RequestCount => counter;

        public Task<StellaOpsTokenResult> RequestClientCredentialsTokenAsync(string? scope = null, CancellationToken cancellationToken = default)
        {
            var access = $"token-{Interlocked.Increment(ref counter)}";
            var expires = timeProvider.GetUtcNow().AddMinutes(2);
            var scopes = scope is null
                ? Array.Empty<string>()
                : scope.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

            return Task.FromResult(new StellaOpsTokenResult(access, "Bearer", expires, scopes));
        }

        public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(string username, string password, string? scope = null, CancellationToken cancellationToken = default)
            => throw new NotSupportedException();

        public Task<JsonWebKeySet> GetJsonWebKeySetAsync(CancellationToken cancellationToken = default)
            => throw new NotSupportedException();

        public ValueTask<StellaOpsTokenCacheEntry?> GetCachedTokenAsync(string key, CancellationToken cancellationToken = default)
            => ValueTask.FromResult<StellaOpsTokenCacheEntry?>(null);

        public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default)
            => ValueTask.CompletedTask;

        public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default)
            => ValueTask.CompletedTask;
    }
}
