using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Authority.Bootstrap;
using StellaOps.Authority.Storage.Mongo.Documents;
using StellaOps.Authority.Storage.Mongo.Stores;
using StellaOps.Cryptography.Audit;
using Xunit;

namespace StellaOps.Authority.Tests.Bootstrap;

public sealed class BootstrapInviteCleanupServiceTests
{
    [Fact]
    public async Task SweepExpiredInvitesAsync_ExpiresInvitesAndEmitsAuditRecords()
    {
        var now = new DateTimeOffset(2025, 10, 14, 12, 0, 0, TimeSpan.Zero);
        var timeProvider = new FakeTimeProvider(now);

        var invites = new List<AuthorityBootstrapInviteDocument>
        {
            new()
            {
                Token = "token-1",
                Type = BootstrapInviteTypes.User,
                ExpiresAt = now.AddMinutes(-5),
                Provider = "standard",
                Target = "alice@example.com",
                Status = AuthorityBootstrapInviteStatuses.Pending
            },
            new()
            {
                Token = "token-2",
                Type = BootstrapInviteTypes.Client,
                ExpiresAt = now.AddMinutes(-1),
                Provider = "standard",
                Target = "client-1",
                Status = AuthorityBootstrapInviteStatuses.Reserved
            }
        };

        var store = new FakeInviteStore(invites);
        var sink = new CapturingAuthEventSink();
        var service = new BootstrapInviteCleanupService(store, sink, timeProvider, NullLogger<BootstrapInviteCleanupService>.Instance);

        await service.SweepExpiredInvitesAsync(CancellationToken.None);

        Assert.True(store.ExpireCalled);
        Assert.Equal(2, sink.Events.Count);
        Assert.All(sink.Events, record => Assert.Equal("authority.bootstrap.invite.expired", record.EventType));
        Assert.Contains(sink.Events, record => record.Properties.Any(property => property.Name == "invite.token" && property.Value.Value == "token-1"));
        Assert.Contains(sink.Events, record => record.Properties.Any(property => property.Name == "invite.token" && property.Value.Value == "token-2"));
    }

    private sealed class FakeInviteStore : IAuthorityBootstrapInviteStore
    {
        private readonly IReadOnlyList<AuthorityBootstrapInviteDocument> invites;

        public FakeInviteStore(IReadOnlyList<AuthorityBootstrapInviteDocument> invites)
            => this.invites = invites;

        public bool ExpireCalled { get; private set; }

        public ValueTask<AuthorityBootstrapInviteDocument> CreateAsync(AuthorityBootstrapInviteDocument document, CancellationToken cancellationToken)
            => throw new NotImplementedException();

        public ValueTask<BootstrapInviteReservationResult> TryReserveAsync(string token, string expectedType, DateTimeOffset now, string? reservedBy, CancellationToken cancellationToken)
            => ValueTask.FromResult(new BootstrapInviteReservationResult(BootstrapInviteReservationStatus.NotFound, null));

        public ValueTask<bool> ReleaseAsync(string token, CancellationToken cancellationToken)
            => ValueTask.FromResult(false);

        public ValueTask<bool> MarkConsumedAsync(string token, string? consumedBy, DateTimeOffset consumedAt, CancellationToken cancellationToken)
            => ValueTask.FromResult(false);

        public ValueTask<IReadOnlyList<AuthorityBootstrapInviteDocument>> ExpireAsync(DateTimeOffset now, CancellationToken cancellationToken)
        {
            ExpireCalled = true;
            return ValueTask.FromResult(invites);
        }
    }

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

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