using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using ZB.MOM.WW.Health.EntityFrameworkCore;

namespace ZB.MOM.WW.Health.EntityFrameworkCore.Tests;

/// <summary>
/// Verifies <see cref="DatabaseHealthCheck{TContext}"/> against a real SQLite database (in-memory,
/// connection kept open) so the <c>CanConnectAsync</c> semantics exercise an actual provider:
/// reachable → Healthy, unopenable connection → Unhealthy (no throw escapes), a custom
/// <see cref="DatabaseHealthCheckOptions{TContext}.ProbeQuery"/> that queries → Healthy, a
/// throwing <c>ProbeQuery</c> → Unhealthy, and a timed-out probe → Unhealthy. Both the
/// <see cref="IDbContextFactory{TContext}"/> and the scoped-<c>TContext</c> resolution paths
/// are covered.
/// </summary>
public sealed class DatabaseHealthCheckTests
{
    /// <summary>A minimal context with one entity, used purely to drive provider behaviour.</summary>
    private sealed class WidgetContext : DbContext
    {
        public WidgetContext(DbContextOptions<WidgetContext> options) : base(options) { }

        public DbSet<Widget> Widgets => Set<Widget>();
    }

    private sealed class Widget
    {
        public int Id { get; set; }
    }

    private static HealthCheckContext NewContext() => new()
    {
        Registration = new HealthCheckRegistration(
            "database",
            sp => throw new InvalidOperationException("not used"),
            HealthStatus.Unhealthy,
            tags: null),
    };

    /// <summary>
    /// Builds a provider whose <typeparamref name="WidgetContext"/> is backed by the supplied open
    /// SQLite connection (and creates the schema). When <paramref name="useFactory"/> is true the
    /// context is registered via <c>AddDbContextFactory</c>; otherwise via <c>AddDbContext</c> (scoped).
    /// </summary>
    private static ServiceProvider BuildProvider(SqliteConnection connection, bool useFactory)
    {
        connection.Open();

        var services = new ServiceCollection();
        if (useFactory)
        {
            services.AddDbContextFactory<WidgetContext>(o => o.UseSqlite(connection));
        }
        else
        {
            services.AddDbContext<WidgetContext>(o => o.UseSqlite(connection));
        }

        var provider = services.BuildServiceProvider();

        using var scope = provider.CreateScope();
        scope.ServiceProvider.GetRequiredService<WidgetContext>().Database.EnsureCreated();

        return provider;
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task ReachableContext_Healthy(bool useFactory)
    {
        using var connection = new SqliteConnection("DataSource=:memory:");
        await using var provider = BuildProvider(connection, useFactory);

        var check = new DatabaseHealthCheck<WidgetContext>(provider);

        var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);

        Assert.Equal(HealthStatus.Healthy, result.Status);
    }

    [Fact]
    public async Task UnopenableConnection_Unhealthy_NoThrow()
    {
        // Point the context at a file path that cannot be opened (parent directory does not exist).
        var bogusPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"), "missing", "db.sqlite");

        var services = new ServiceCollection();
        services.AddDbContext<WidgetContext>(o => o.UseSqlite($"DataSource={bogusPath};Mode=ReadWrite"));
        await using var provider = services.BuildServiceProvider();

        var check = new DatabaseHealthCheck<WidgetContext>(provider);

        var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);

        Assert.Equal(HealthStatus.Unhealthy, result.Status);
    }

    [Fact]
    public async Task CustomProbeQuery_RunsQuery_Healthy()
    {
        using var connection = new SqliteConnection("DataSource=:memory:");
        await using var provider = BuildProvider(connection, useFactory: true);

        var options = new DatabaseHealthCheckOptions<WidgetContext>
        {
            ProbeQuery = (ctx, ct) => ctx.Widgets.AsNoTracking().AnyAsync(ct),
        };
        var check = new DatabaseHealthCheck<WidgetContext>(provider, options);

        var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);

        Assert.Equal(HealthStatus.Healthy, result.Status);
    }

    [Fact]
    public async Task ProbeQueryThrows_Unhealthy()
    {
        using var connection = new SqliteConnection("DataSource=:memory:");
        await using var provider = BuildProvider(connection, useFactory: false);

        var options = new DatabaseHealthCheckOptions<WidgetContext>
        {
            // Use a faulted task rather than a synchronous throw to accurately model
            // async probe delegates that encounter an error.
            ProbeQuery = (_, _) => Task.FromException(new InvalidOperationException("boom")),
        };
        var check = new DatabaseHealthCheck<WidgetContext>(provider, options);

        var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);

        Assert.Equal(HealthStatus.Unhealthy, result.Status);
    }

    [Fact]
    public async Task ProbeTimeout_Unhealthy()
    {
        using var connection = new SqliteConnection("DataSource=:memory:");
        await using var provider = BuildProvider(connection, useFactory: true);

        // Use a very short timeout and a probe that blocks indefinitely (until cancelled).
        var options = new DatabaseHealthCheckOptions<WidgetContext>
        {
            Timeout = TimeSpan.FromMilliseconds(50),
            ProbeQuery = async (_, ct) => await Task.Delay(Timeout.Infinite, ct),
        };
        var check = new DatabaseHealthCheck<WidgetContext>(provider, options);

        var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);

        Assert.Equal(HealthStatus.Unhealthy, result.Status);
    }
}
