using Baya.Application.Contracts.Payments;
using Baya.Infrastructure.Persistence.Services.Scheduling;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;

namespace Baya.Test.Foundation.Scheduling;

/// <summary>
/// Orchestration tests for <c>RecurringJobSchedulerHostedService</c> (refinement-phase-7): it runs each job at
/// startup under the distributed lock, keeps sibling loops alive when one job throws, and stays dormant under the
/// Testing environment so integration tests are deterministic.
/// </summary>
public sealed class RecurringJobSchedulerTests
{
    private sealed class FakeEnvironment(string environmentName) : IHostEnvironment
    {
        public string EnvironmentName { get; set; } = environmentName;
        public string ApplicationName { get; set; } = "Baya.Test";
        public string ContentRootPath { get; set; } = ".";
        public Microsoft.Extensions.FileProviders.IFileProvider ContentRootFileProvider { get; set; } = null!;
    }

    private sealed class CountingJob(string name, bool throws = false) : IRecurringJob
    {
        private int _runCount;
        public int RunCount => Volatile.Read(ref _runCount);
        public string Name => name;

        // Long interval: run once at startup, then idle for the whole test window.
        public ValueTask<TimeSpan> GetIntervalAsync(IServiceProvider services, CancellationToken cancellationToken)
            => ValueTask.FromResult(TimeSpan.FromHours(1));

        public ValueTask RunAsync(IServiceProvider services, CancellationToken cancellationToken)
        {
            Interlocked.Increment(ref _runCount);
            if (throws)
                throw new InvalidOperationException("boom");
            return ValueTask.CompletedTask;
        }
    }

    private static (IServiceScopeFactory scopeFactory, IDistributedLock @lock) Fakes(out IDistributedLock recordingLock)
    {
        var scopeFactory = new ServiceCollection().BuildServiceProvider().GetRequiredService<IServiceScopeFactory>();
        recordingLock = Substitute.For<IDistributedLock>();
        recordingLock.AcquireAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(new ValueTask<IAsyncDisposable>(Substitute.For<IAsyncDisposable>()));
        return (scopeFactory, recordingLock);
    }

    private static async Task WaitUntilAsync(Func<bool> condition, TimeSpan timeout)
    {
        var deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            if (condition())
                return;
            await Task.Delay(20);
        }
    }

    [Fact]
    public async Task RunsJobAtStartup_UnderPerJobLock()
    {
        var (scopeFactory, @lock) = Fakes(out var recordingLock);
        var job = new CountingJob("fake_job");
        var scheduler = new RecurringJobSchedulerHostedService(
            scopeFactory, [job], @lock, new FakeEnvironment("Development"),
            NullLogger<RecurringJobSchedulerHostedService>.Instance);

        await scheduler.StartAsync(default);
        await WaitUntilAsync(() => job.RunCount >= 1, TimeSpan.FromSeconds(3));
        await scheduler.StopAsync(default);

        Assert.True(job.RunCount >= 1);
        await recordingLock.Received().AcquireAsync("scheduler:fake_job", Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task OneJobThrowing_DoesNotStopSiblingJobs()
    {
        var (scopeFactory, @lock) = Fakes(out _);
        var throwing = new CountingJob("throwing", throws: true);
        var healthy = new CountingJob("healthy");
        var scheduler = new RecurringJobSchedulerHostedService(
            scopeFactory, [throwing, healthy], @lock, new FakeEnvironment("Development"),
            NullLogger<RecurringJobSchedulerHostedService>.Instance);

        await scheduler.StartAsync(default);
        await WaitUntilAsync(() => healthy.RunCount >= 1, TimeSpan.FromSeconds(3));
        await scheduler.StopAsync(default);

        Assert.True(throwing.RunCount >= 1);
        Assert.True(healthy.RunCount >= 1);
    }

    [Fact]
    public async Task TestingEnvironment_KeepsSchedulerDormant()
    {
        var (scopeFactory, @lock) = Fakes(out _);
        var job = new CountingJob("fake_job");
        var scheduler = new RecurringJobSchedulerHostedService(
            scopeFactory, [job], @lock, new FakeEnvironment("Testing"),
            NullLogger<RecurringJobSchedulerHostedService>.Instance);

        await scheduler.StartAsync(default);
        await Task.Delay(200);
        await scheduler.StopAsync(default);

        Assert.Equal(0, job.RunCount);
    }
}
