using System.Collections;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace ZB.MOM.WW.LocalDb.Tests;

public sealed class DependencyInjectionTests : IDisposable
{
    private readonly string _path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".db");

    public void Dispose()
    {
        Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
        if (File.Exists(_path))
            File.Delete(_path);
    }

    private IConfiguration Config() =>
        new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?> { ["LocalDb:Path"] = _path })
            .Build();

    [Fact]
    public async Task AddZbLocalDb_ResolvesILocalDb_Singleton()
    {
        var services = new ServiceCollection();
        services.AddZbLocalDb(Config());
        await using var provider = services.BuildServiceProvider();

        var a = provider.GetRequiredService<ILocalDb>();
        var b = provider.GetRequiredService<ILocalDb>();
        Assert.Same(a, b);

        await a.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY)");
        await a.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", new { id = 1 });
        var rows = await a.QueryAsync("SELECT COUNT(*) FROM t", r => r.GetInt64(0));
        Assert.Equal(1L, rows[0]);
    }

    [Fact]
    public void AddZbLocalDb_WithRegistrations_TablesRegisteredOnStart()
    {
        var services = new ServiceCollection();
        services.AddZbLocalDb(Config(), db =>
        {
            using var conn = db.CreateConnection();
            using var cmd = conn.CreateCommand();
            cmd.CommandText = "CREATE TABLE widget (id INTEGER PRIMARY KEY, name TEXT)";
            cmd.ExecuteNonQuery();
            db.RegisterReplicated("widget");
        });
        using var provider = services.BuildServiceProvider();

        var db = provider.GetRequiredService<ILocalDb>();
        Assert.Contains("widget", db.ReplicatedTables.Keys);
    }

    [Fact]
    public void AddZbLocalDb_OnReadyThrows_ExceptionPropagates()
    {
        var services = new ServiceCollection();
        services.AddZbLocalDb(Config(), _ => throw new InvalidOperationException("boom"));
        using var provider = services.BuildServiceProvider();

        var first = Assert.Throws<InvalidOperationException>(() => provider.GetRequiredService<ILocalDb>());
        Assert.Equal("boom", first.Message);

        // No half-initialized singleton is cached: the next resolution re-runs the factory and fails again.
        var second = Assert.Throws<InvalidOperationException>(() => provider.GetRequiredService<ILocalDb>());
        Assert.Equal("boom", second.Message);
    }

    [Fact]
    public void Validator_EmptyPath_Fails()
    {
        var result = new LocalDbOptionsValidator().Validate(
            null, new LocalDbOptions { Path = "  " });

        Assert.True(result.Failed);
        Assert.Contains(result.Failures, f => f.Contains("LocalDb:Path", StringComparison.Ordinal));
    }

    [Fact]
    public void Validator_NonPositiveBusyTimeout_Fails()
    {
        var result = new LocalDbOptionsValidator().Validate(
            null, new LocalDbOptions { Path = "x.db", BusyTimeoutMs = 0 });

        Assert.True(result.Failed);
        Assert.Contains(result.Failures, f => f.Contains("BusyTimeoutMs", StringComparison.Ordinal));
    }

    [Fact]
    public void Validator_InvalidSynchronous_Fails()
    {
        var result = new LocalDbOptionsValidator().Validate(
            null, new LocalDbOptions { Path = "x.db", Synchronous = "SOMETIMES" });

        Assert.True(result.Failed);
        Assert.Contains(result.Failures, f => f.Contains("Synchronous", StringComparison.Ordinal));
    }

    [Fact]
    public void Validator_ValidOptions_Succeeds()
    {
        var result = new LocalDbOptionsValidator().Validate(
            null, new LocalDbOptions { Path = "x.db", BusyTimeoutMs = 5000, Synchronous = "normal" });

        Assert.True(result.Succeeded);
    }

    [Fact]
    public async Task Bind_NonObjectDictionary_Throws()
    {
        var services = new ServiceCollection();
        services.AddZbLocalDb(Config());
        await using var provider = services.BuildServiceProvider();
        var db = provider.GetRequiredService<ILocalDb>();
        await db.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)");

        var stringDict = new Dictionary<string, string> { ["id"] = "1" };
        await Assert.ThrowsAsync<ArgumentException>(() =>
            db.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", stringDict));

        var hashtable = new Hashtable { ["id"] = 1 };
        await Assert.ThrowsAsync<ArgumentException>(() =>
            db.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", hashtable));
    }
}
