namespace Books.Api.Tests.Helpers;

/// <summary>
/// Helper for testing eventually consistent systems.
/// Polls a condition until it's met or times out.
/// </summary>
public static class Eventually
{
    /// <summary>
    /// Polls a condition until it returns true or times out.
    /// </summary>
    public static async Task AssertAsync(
        Func<Task<bool>> condition,
        TimeSpan? timeout = null,
        TimeSpan? pollInterval = null,
        string? failMessage = null)
    {
        timeout ??= TimeSpan.FromSeconds(5);
        pollInterval ??= TimeSpan.FromMilliseconds(50);

        var start = DateTime.UtcNow;

        while (DateTime.UtcNow - start < timeout)
        {
            if (await condition())
                return;

            await Task.Delay(pollInterval.Value);
        }

        throw new TimeoutException(
            failMessage ?? $"Condition was not met within {timeout}.");
    }

    /// <summary>
    /// Polls until a non-null result is returned or times out.
    /// </summary>
    public static async Task<T> GetAsync<T>(
        Func<Task<T?>> getter,
        TimeSpan? timeout = null,
        TimeSpan? pollInterval = null,
        string? failMessage = null) where T : class
    {
        timeout ??= TimeSpan.FromSeconds(10);
        pollInterval ??= TimeSpan.FromMilliseconds(100);

        var start = DateTime.UtcNow;

        while (DateTime.UtcNow - start < timeout)
        {
            var result = await getter();
            if (result != null)
                return result;

            await Task.Delay(pollInterval.Value);
        }

        throw new TimeoutException(
            failMessage ?? $"Expected non-null result was not returned within {timeout}.");
    }

    /// <summary>
    /// Polls until a collection has the expected count or times out.
    /// </summary>
    public static async Task<IReadOnlyList<T>> GetListAsync<T>(
        Func<Task<IReadOnlyList<T>>> getter,
        int expectedCount,
        TimeSpan? timeout = null,
        TimeSpan? pollInterval = null,
        string? failMessage = null)
    {
        timeout ??= TimeSpan.FromSeconds(10);
        pollInterval ??= TimeSpan.FromMilliseconds(100);

        var start = DateTime.UtcNow;

        while (DateTime.UtcNow - start < timeout)
        {
            var result = await getter();
            if (result.Count >= expectedCount)
                return result;

            await Task.Delay(pollInterval.Value);
        }

        throw new TimeoutException(
            failMessage ?? $"Expected {expectedCount} items but condition was not met within {timeout}.");
    }
}
