using Microsoft.AspNetCore.Builder;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
using ZB.MOM.WW.LocalDb;
using ZB.MOM.WW.LocalDb.Replication;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
using ZB.MOM.WW.OtOpcUa.Host.Health;
using ZB.MOM.WW.OtOpcUa.Host.Observability;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
using ZB.MOM.WW.OtOpcUa.Runtime.DeploymentCache;
using ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;

namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;

/// <summary>
///     LocalDb Phase 1 (Task 11) — DI pins over the real composition methods.
/// </summary>
/// <remarks>
///     <para>
///         DI extension methods have shipped inert three times in this family (Secrets 0.2.0 / 0.2.2,
///         ScadaBridge #22): a registration that compiles, deploys, and does nothing because the seam
///         was never resolved from a built container. These tests build a real container from the
///         same <c>AddOtOpcUa*</c> methods <c>Program.cs</c> calls and resolve through it.
///     </para>
///     <para>
///         They deliberately do NOT boot the full host. <c>WebApplicationFactory&lt;Program&gt;</c>
///         would start hosted services — Akka, the OPC UA server, <c>ValidateOnStart</c> — which need
///         SQL Server and an Akka mesh (see <c>TwoNodeClusterHarness</c> on why the real host is not
///         driven here). What is under test is DI resolution, so the container is built and resolved
///         without ever starting it.
///     </para>
/// </remarks>
public sealed class LocalDbWiringTests : IDisposable
{
    private readonly List<string> _dbPaths = [];
    private readonly List<WebApplication> _apps = [];

    /// <summary>
    ///     Builds a container the way <c>Program.cs</c>'s driver branch does for LocalDb: the real
    ///     <see cref="LocalDbRegistration.AddOtOpcUaLocalDb"/>, health, and observability methods,
    ///     over a temp-file <c>LocalDb:Path</c>.
    /// </summary>
    private IServiceProvider BuildDriverGraph()
    {
        var dbPath = Path.Combine(Path.GetTempPath(), $"otopcua-wiring-{Guid.NewGuid():N}.db");
        _dbPaths.Add(dbPath);

        var builder = WebApplication.CreateBuilder(new WebApplicationOptions { Args = [] });
        builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["LocalDb:Path"] = dbPath,
        });

        // The exact LocalDb composition Program.cs runs under hasDriver.
        builder.Services.AddOtOpcUaLocalDb(builder.Configuration);
        builder.Services.AddGrpc(o => o.Interceptors.Add<LocalDbSyncAuthInterceptor>());
        builder.Services.AddOtOpcUaHealth();
        builder.Services.AddOtOpcUaObservability(builder.Configuration);

        var app = builder.Build();
        _apps.Add(app);
        return app.Services;
    }

    /// <summary>
    ///     Builds an admin-only container: the shared registrations that run on every node, but NOT
    ///     <see cref="LocalDbRegistration.AddOtOpcUaLocalDb"/> — exactly as Program.cs gates it under
    ///     hasDriver.
    /// </summary>
    private IServiceProvider BuildAdminOnlyGraph()
    {
        var builder = WebApplication.CreateBuilder(new WebApplicationOptions { Args = [] });

        // No LocalDb:Path configured — an admin-only node has no reason to set it.
        builder.Services.AddOtOpcUaHealth();
        builder.Services.AddOtOpcUaObservability(builder.Configuration);

        var app = builder.Build();
        _apps.Add(app);
        return app.Services;
    }

    [Fact]
    public void DriverGraph_LocalDbResolvesAsASingletonWithEveryReplicatedTableRegistered()
    {
        var sp = BuildDriverGraph();

        var first = sp.GetService<ILocalDb>();
        first.ShouldNotBeNull();
        sp.GetService<ILocalDb>().ShouldBeSameAs(first); // singleton

        // Exact set, both directions — a dropped registration replicates nothing; an extra one costs
        // three triggers and oplog volume on every write.
        first.ReplicatedTables.Keys.OrderBy(k => k, StringComparer.Ordinal)
            .ShouldBe(["alarm_condition_state", "alarm_sf_events", "deployment_artifacts", "deployment_pointer"]);
    }

    [Fact]
    public void DriverGraph_ArtifactCacheResolvesToTheLocalDbBackedImplementation()
    {
        var sp = BuildDriverGraph();

        sp.GetService<IDeploymentArtifactCache>()
            .ShouldBeOfType<LocalDbDeploymentArtifactCache>();
    }

    [Fact]
    public void DriverGraph_AlarmStateStoreResolvesToTheLocalDbBackedImplementation()
    {
        // Phase 4: scripted-alarm condition state is served from the replicated LocalDb store on a
        // driver node. A dropped registration would silently fall the actor back to the EF store (or,
        // DB-less, skip the alarm host) — this pins the LocalDb store as the resolved implementation.
        var sp = BuildDriverGraph();

        sp.GetService<IAlarmStateStore>()
            .ShouldBeOfType<LocalDbAlarmConditionStateStore>();
    }

    [Fact]
    public void DriverGraph_DefaultOff_SyncStatusReportsNotConnectedAndNoPeer()
    {
        // The default-OFF posture pin: registering the replication engine must not connect it.
        var sp = BuildDriverGraph();

        var status = sp.GetService<ISyncStatus>();
        status.ShouldNotBeNull();
        status.Connected.ShouldBeFalse();
        status.PeerNodeId.ShouldBeNull();
    }

    [Fact]
    public void DriverGraph_LocalDbReplicationHealthCheckIsRegistered()
    {
        var sp = BuildDriverGraph();

        var registrations = sp.GetRequiredService<IOptions<HealthCheckServiceOptions>>()
            .Value.Registrations
            .Select(r => r.Name)
            .ToArray();

        registrations.ShouldContain("localdb-replication");
    }

    [Fact]
    public void AdminOnlyGraph_HasNoLocalDb_AndDoesNotDemandLocalDbPath()
    {
        // Boot must not require LocalDb:Path on a node that never registers LocalDb. Building the
        // container without a configured path and resolving the shared services proves nothing in
        // the always-on registrations reaches for ILocalDb or its options.
        var sp = BuildAdminOnlyGraph();

        sp.GetService<ILocalDb>().ShouldBeNull();
        sp.GetService<IDeploymentArtifactCache>().ShouldBeNull();
        // Phase 4: an admin-only node never registers the LocalDb alarm store either.
        sp.GetService<IAlarmStateStore>().ShouldBeNull();

        // The unconditionally-registered health check must still resolve and construct — it is meant
        // to no-op to Healthy when LocalDb is absent, not to fail because ISyncStatus is missing.
        var registrations = sp.GetRequiredService<IOptions<HealthCheckServiceOptions>>()
            .Value.Registrations
            .Where(r => r.Name == "localdb-replication")
            .ToArray();
        var localDbCheck = registrations.ShouldHaveSingleItem();
        Should.NotThrow(() => localDbCheck.Factory(sp));
    }

    public void Dispose()
    {
        foreach (var app in _apps)
            ((IDisposable)app).Dispose();

        SqliteConnection.ClearAllPools();

        foreach (var basePath in _dbPaths)
        {
            foreach (var path in new[] { basePath, $"{basePath}-wal", $"{basePath}-shm" })
            {
                if (File.Exists(path))
                    File.Delete(path);
            }
        }
    }
}
