using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.TestSupport;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;

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

/// <summary>
/// Bundle E (M2 Task E1) DI surface tests for <c>AddAuditLog</c>. M1 shipped
/// the options-only scaffold; M2 extends it with the site writer chain
/// (<see cref="SqliteAuditWriter"/> + <see cref="RingBufferFallback"/> +
/// <see cref="FallbackAuditWriter"/>) and the telemetry collaborators
/// (<see cref="ISiteAuditQueue"/>, <see cref="ISiteStreamAuditClient"/>,
/// <see cref="IAuditWriteFailureCounter"/>).
/// </summary>
public class AddAuditLogTests
{
    private static ServiceProvider BuildProvider(IDictionary<string, string?>? settings = null)
    {
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(settings ?? new Dictionary<string, string?>())
            .Build();

        var services = new ServiceCollection();
        services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
        services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
        // INodeIdentityProvider is registered by the Host's
        // SiteServiceRegistration in production; AddAuditLog assumes its
        // presence so SqliteAuditWriter and CentralAuditWriter can resolve.
        services.AddSingleton<INodeIdentityProvider>(new FakeNodeIdentityProvider());
        services.AddAuditLog(config);
        return services.BuildServiceProvider();
    }

    [Fact]
    public void AddAuditLog_RegistersAuditLogOptions()
    {
        using var provider = BuildProvider();

        var opts = provider.GetService<IOptions<AuditLogOptions>>();

        Assert.NotNull(opts);
        Assert.NotNull(opts!.Value);
    }

    [Fact]
    public void AddAuditLog_NullServices_Throws()
    {
        var config = new ConfigurationBuilder().Build();

        Assert.Throws<ArgumentNullException>(
            () => ServiceCollectionExtensions.AddAuditLog(null!, config));
    }

    [Fact]
    public void AddAuditLog_NullConfig_Throws()
    {
        var services = new ServiceCollection();

        Assert.Throws<ArgumentNullException>(
            () => services.AddAuditLog(null!));
    }

    // -- Bundle E (M2 Task E1) ---------------------------------------------

    [Fact]
    public void AddAuditLog_Registers_SqliteAuditWriter_Singleton_FromDI()
    {
        using var provider = BuildProvider(new Dictionary<string, string?>
        {
            // In-memory database keeps the writer's owned connection portable
            // across tests; the per-instance Cache=Shared in the writer's
            // default connection string ensures no on-disk file is touched.
            ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
        });

        var writer = provider.GetService<SqliteAuditWriter>();

        Assert.NotNull(writer);
        // Singleton — same instance on a second resolve.
        Assert.Same(writer, provider.GetService<SqliteAuditWriter>());
    }

    [Fact]
    public void AddAuditLog_Registers_IAuditWriter_AsFallbackAuditWriter()
    {
        using var provider = BuildProvider(new Dictionary<string, string?>
        {
            ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
        });

        var writer = provider.GetService<IAuditWriter>();

        Assert.NotNull(writer);
        Assert.IsType<FallbackAuditWriter>(writer);
    }

    [Fact]
    public void AddAuditLog_Registers_ISiteAuditQueue_AsSameInstance_As_SqliteAuditWriter()
    {
        // The telemetry actor reads from ISiteAuditQueue while scripts write
        // through IAuditWriter → SqliteAuditWriter. Both surfaces MUST resolve
        // to the same instance or pending rows will never be visible.
        using var provider = BuildProvider(new Dictionary<string, string?>
        {
            ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
        });

        var queue = provider.GetService<ISiteAuditQueue>();
        var writer = provider.GetService<SqliteAuditWriter>();

        Assert.NotNull(queue);
        Assert.NotNull(writer);
        Assert.Same(writer, queue);
    }

    [Fact]
    public void AddAuditLog_Registers_RingBufferFallback_Singleton()
    {
        using var provider = BuildProvider(new Dictionary<string, string?>
        {
            ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
        });

        var ring = provider.GetService<RingBufferFallback>();
        Assert.NotNull(ring);
        Assert.Same(ring, provider.GetService<RingBufferFallback>());
    }

    [Fact]
    public void AddAuditLog_Registers_AuditWriteFailureCounter_AsNoOpDefault()
    {
        using var provider = BuildProvider(new Dictionary<string, string?>
        {
            ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
        });

        var counter = provider.GetService<IAuditWriteFailureCounter>();
        Assert.NotNull(counter);
        Assert.IsType<NoOpAuditWriteFailureCounter>(counter);
    }

    [Fact]
    public void AddAuditLog_Registers_SiteStreamAuditClient_AsNoOpDefault()
    {
        using var provider = BuildProvider(new Dictionary<string, string?>
        {
            ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
        });

        var client = provider.GetService<ISiteStreamAuditClient>();
        Assert.NotNull(client);
        Assert.IsType<NoOpSiteStreamAuditClient>(client);
    }

    // -- M4 Bundle B (B1) central direct-write audit writer -----------------

    [Fact]
    public void AddAuditLog_Registers_ICentralAuditWriter_AsCentralAuditWriter()
    {
        using var provider = BuildProvider(new Dictionary<string, string?>
        {
            ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
        });

        var writer = provider.GetService<ICentralAuditWriter>();
        Assert.NotNull(writer);
        Assert.IsType<CentralAuditWriter>(writer);
    }

    [Fact]
    public void AddAuditLog_ICentralAuditWriter_IsSingleton()
    {
        using var provider = BuildProvider(new Dictionary<string, string?>
        {
            ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
        });

        var w1 = provider.GetService<ICentralAuditWriter>();
        var w2 = provider.GetService<ICentralAuditWriter>();
        Assert.Same(w1, w2);
    }

    [Fact]
    public void AddAuditLog_Options_Bind_RoundTrip_SqliteWriter()
    {
        using var provider = BuildProvider(new Dictionary<string, string?>
        {
            ["AuditLog:SiteWriter:DatabasePath"] = "/tmp/test-audit.db",
            ["AuditLog:SiteWriter:ChannelCapacity"] = "8192",
            ["AuditLog:SiteWriter:BatchSize"] = "128",
            ["AuditLog:SiteWriter:FlushIntervalMs"] = "75",
        });

        var opts = provider.GetRequiredService<IOptions<SqliteAuditWriterOptions>>().Value;
        Assert.Equal("/tmp/test-audit.db", opts.DatabasePath);
        Assert.Equal(8192, opts.ChannelCapacity);
        Assert.Equal(128, opts.BatchSize);
        Assert.Equal(75, opts.FlushIntervalMs);
    }

    [Fact]
    public void AddAuditLog_Options_Bind_RoundTrip_SiteTelemetry()
    {
        using var provider = BuildProvider(new Dictionary<string, string?>
        {
            ["AuditLog:SiteTelemetry:BatchSize"] = "512",
            ["AuditLog:SiteTelemetry:BusyIntervalSeconds"] = "3",
            ["AuditLog:SiteTelemetry:IdleIntervalSeconds"] = "60",
        });

        var opts = provider.GetRequiredService<IOptions<SiteAuditTelemetryOptions>>().Value;
        Assert.Equal(512, opts.BatchSize);
        Assert.Equal(3, opts.BusyIntervalSeconds);
        Assert.Equal(60, opts.IdleIntervalSeconds);
    }

    // -- Bundle G (M2 Task G1) Site Health Monitoring bridge ----------------

    [Fact]
    public void AddAuditLogHealthMetricsBridge_Swaps_FailureCounter_To_HealthMetrics_Implementation()
    {
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
            })
            .Build();

        var services = new ServiceCollection();
        services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
        services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
        services.AddAuditLog(config);
        // The bridge depends on ISiteHealthCollector; AddHealthMonitoring is
        // what registers it on the site (and the central self-host).
        services.AddHealthMonitoring();
        services.AddAuditLogHealthMetricsBridge();
        using var provider = services.BuildServiceProvider();

        var counter = provider.GetRequiredService<IAuditWriteFailureCounter>();

        Assert.IsType<HealthMetricsAuditWriteFailureCounter>(counter);
    }

    [Fact]
    public void AddAuditLogHealthMetricsBridge_Without_HealthMonitoring_Still_Resolves_But_Errors_On_Use()
    {
        // The bridge replaces the registration unconditionally; resolving the
        // counter when ISiteHealthCollector is missing throws at GetRequiredService
        // time. This documents the contract — callers must register
        // AddHealthMonitoring() before the bridge.
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
            })
            .Build();

        var services = new ServiceCollection();
        services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
        services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
        services.AddAuditLog(config);
        services.AddAuditLogHealthMetricsBridge();
        using var provider = services.BuildServiceProvider();

        Assert.Throws<InvalidOperationException>(
            () => provider.GetRequiredService<IAuditWriteFailureCounter>());
    }

    [Fact]
    public void AddAuditLogHealthMetricsBridge_IsIdempotent_DoesNotDoubleRegister_HostedService()
    {
        // AuditLog-011: AddHostedService has no TryAdd variant, so a second
        // call without the sentinel guard would spin up a second
        // SiteAuditBacklogReporter on the same SQLite file. The helper must
        // be a no-op on the second call — exactly one hosted-service
        // descriptor for SiteAuditBacklogReporter survives.
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
            })
            .Build();

        var services = new ServiceCollection();
        services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
        services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));
        services.AddSingleton<INodeIdentityProvider>(new FakeNodeIdentityProvider());
        services.AddAuditLog(config);
        services.AddHealthMonitoring();

        services.AddAuditLogHealthMetricsBridge();
        services.AddAuditLogHealthMetricsBridge();

        var reporterCount = services.Count(d =>
            d.ServiceType == typeof(IHostedService) &&
            d.ImplementationType == typeof(SiteAuditBacklogReporter));

        Assert.Equal(1, reporterCount);
    }
}
