using ErsatzTV.Core.Health;
using ErsatzTV.Core.Health.Checks;
using ErsatzTV.Infrastructure.Health;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Infrastructure.Health;

[TestFixture]
public class HealthCheckServiceTests
{
    private IFFmpegVersionHealthCheck _representativeCheck = null!;
    private IMemoryCache _memoryCache = null!;
    private IMediator _mediator = null!;
    private HealthCheckService _service = null!;

    [SetUp]
    public void SetUp()
    {
        _memoryCache = new MemoryCache(new MemoryCacheOptions());
        _mediator = Substitute.For<IMediator>();
        _representativeCheck = PassCheck<IFFmpegVersionHealthCheck>();

        // All 14 checks run together on a single PerformHealthChecks call, so the representative
        // check's invocation count equals the number of actual (non-cached) runs.
        _service = new HealthCheckService(
            PassCheck<IMacOsConfigFolderHealthCheck>(),
            _representativeCheck,
            PassCheck<IFFmpegCapabilitiesHealthCheck>(),
            PassCheck<IFFmpegReportsHealthCheck>(),
            PassCheck<IHardwareAccelerationHealthCheck>(),
            PassCheck<IMovieMetadataHealthCheck>(),
            PassCheck<IEpisodeMetadataHealthCheck>(),
            PassCheck<IZeroDurationHealthCheck>(),
            PassCheck<IFileNotFoundHealthCheck>(),
            PassCheck<IUnavailableHealthCheck>(),
            PassCheck<IVaapiDriverHealthCheck>(),
            PassCheck<IUnifiedDockerHealthCheck>(),
            PassCheck<IDowngradeHealthCheck>(),
            PassCheck<IEmptyScheduleHealthCheck>(),
            _memoryCache,
            _mediator,
            NullLogger<HealthCheckService>.Instance);
    }

    [TearDown]
    public void TearDown() => (_memoryCache as MemoryCache)?.Dispose();

    [Test]
    public async Task PerformHealthChecks_Should_Serve_Cache_Within_Window()
    {
        await _service.PerformHealthChecks(false, CancellationToken.None);
        await _service.PerformHealthChecks(false, CancellationToken.None);

        // Second call is served from cache: checks are not re-run, summary is not re-published.
        await _representativeCheck.Received(1).Check(Arg.Any<CancellationToken>());
        await _mediator.Received(1).Publish(Arg.Any<HealthCheckSummary>(), Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task PerformHealthChecks_Should_Return_Cached_Instance_Within_Window()
    {
        List<HealthCheckResult> first = await _service.PerformHealthChecks(false, CancellationToken.None);
        List<HealthCheckResult> second = await _service.PerformHealthChecks(false, CancellationToken.None);

        second.ShouldBeSameAs(first);
    }

    [Test]
    public async Task PerformHealthChecks_Should_Bypass_Cache_On_Force_Refresh()
    {
        await _service.PerformHealthChecks(false, CancellationToken.None);
        await _service.PerformHealthChecks(true, CancellationToken.None);

        // Force refresh re-runs the checks and re-publishes, regardless of the warm cache.
        await _representativeCheck.Received(2).Check(Arg.Any<CancellationToken>());
        await _mediator.Received(2).Publish(Arg.Any<HealthCheckSummary>(), Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Force_Refresh_Should_Repopulate_Cache_For_Later_Reads()
    {
        // A forced run should warm the cache so a following non-forced read is a hit.
        await _service.PerformHealthChecks(true, CancellationToken.None);
        await _service.PerformHealthChecks(false, CancellationToken.None);

        await _representativeCheck.Received(1).Check(Arg.Any<CancellationToken>());
    }

    private static T PassCheck<T>() where T : class, IHealthCheck
    {
        var check = Substitute.For<T>();
        check.Check(Arg.Any<CancellationToken>())
            .Returns(_ => new HealthCheckResult(
                typeof(T).Name,
                HealthCheckStatus.Pass,
                "ok",
                "ok",
                Option<HealthCheckLink>.None));
        return check;
    }
}
