using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;

namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests;

/// <summary>
/// WP2.6b (arch-review misc — Inbound API per-request SQL): <see cref="ApiMethodCache"/>
/// hit/miss/expiry/invalidation behavior, independent of the endpoint and subscriber wiring
/// (covered separately by <see cref="ScriptArtifactChangeSubscriberTests"/>).
/// </summary>
public class ApiMethodCacheTests
{
    private static ApiMethod Method(string name) => new(name, "return 1;") { Id = 1 };

    [Fact]
    public async Task GetOrFetchAsync_SecondCallWithinTtl_ServesFromCache_NoRefetch()
    {
        var cache = new ApiMethodCache(TimeSpan.FromMinutes(5));
        var fetchCount = 0;
        Task<ApiMethod?> Fetch(CancellationToken _)
        {
            fetchCount++;
            return Task.FromResult<ApiMethod?>(Method("m"));
        }

        var first = await cache.GetOrFetchAsync("m", Fetch, CancellationToken.None);
        var second = await cache.GetOrFetchAsync("m", Fetch, CancellationToken.None);

        Assert.Equal(1, fetchCount);
        Assert.Same(first, second);
    }

    [Fact]
    public async Task GetOrFetchAsync_AfterTtlExpiry_Refetches()
    {
        var time = new FakeTimeProvider(DateTimeOffset.UtcNow);
        var cache = new ApiMethodCache(TimeSpan.FromSeconds(1), time);
        var fetchCount = 0;
        Task<ApiMethod?> Fetch(CancellationToken _)
        {
            fetchCount++;
            return Task.FromResult<ApiMethod?>(Method("m"));
        }

        await cache.GetOrFetchAsync("m", Fetch, CancellationToken.None);
        time.Advance(TimeSpan.FromSeconds(2));
        await cache.GetOrFetchAsync("m", Fetch, CancellationToken.None);

        Assert.Equal(2, fetchCount);
    }

    [Fact]
    public async Task GetOrFetchAsync_NegativeResult_IsCachedUntilTtlExpires()
    {
        var cache = new ApiMethodCache(TimeSpan.FromMinutes(5));
        var fetchCount = 0;
        Task<ApiMethod?> Fetch(CancellationToken _)
        {
            fetchCount++;
            return Task.FromResult<ApiMethod?>(null);
        }

        var first = await cache.GetOrFetchAsync("missing", Fetch, CancellationToken.None);
        var second = await cache.GetOrFetchAsync("missing", Fetch, CancellationToken.None);

        Assert.Null(first);
        Assert.Null(second);
        Assert.Equal(1, fetchCount);
    }

    [Fact]
    public async Task Invalidate_DropsEntry_NextCallRefetches()
    {
        var cache = new ApiMethodCache(TimeSpan.FromMinutes(5));
        var fetchCount = 0;
        Task<ApiMethod?> Fetch(CancellationToken _)
        {
            fetchCount++;
            return Task.FromResult<ApiMethod?>(Method("m"));
        }

        await cache.GetOrFetchAsync("m", Fetch, CancellationToken.None);
        cache.Invalidate("m");
        await cache.GetOrFetchAsync("m", Fetch, CancellationToken.None);

        Assert.Equal(2, fetchCount);
    }

    [Fact]
    public void Invalidate_UnknownName_DoesNotThrow()
    {
        var cache = new ApiMethodCache(TimeSpan.FromMinutes(5));
        cache.Invalidate("never-cached");
    }

    [Fact]
    public void Constructor_NonPositiveTtl_Throws()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() => new ApiMethodCache(TimeSpan.Zero));
        Assert.Throws<ArgumentOutOfRangeException>(() => new ApiMethodCache(TimeSpan.FromSeconds(-1)));
    }

    /// <summary>Minimal controllable <see cref="TimeProvider"/> for TTL-expiry tests.</summary>
    private sealed class FakeTimeProvider : TimeProvider
    {
        private DateTimeOffset _now;
        public FakeTimeProvider(DateTimeOffset start) => _now = start;
        public void Advance(TimeSpan by) => _now += by;
        public override DateTimeOffset GetUtcNow() => _now;
    }
}
