using Akka.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.AuditLog;
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;
using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.Host;
using ZB.MOM.WW.ScadaBridge.Host.Actors;
using ZB.MOM.WW.ScadaBridge.StoreAndForward;

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

/// <summary>
/// Bundle E (M2 Task E1) — verifies the Audit Log (#23) DI surface is wired
/// into both composition roots and that the HOCON document emitted by
/// <see cref="AkkaHostedService.BuildHocon"/> includes the dedicated
/// <c>audit-telemetry-dispatcher</c> the site telemetry actor binds to.
/// </summary>
/// <remarks>
/// <para>
/// Full cluster bring-up is exercised by the existing
/// <see cref="CompositionRootTests"/> pattern — these tests reuse the same
/// <see cref="AkkaHostedServiceRemover"/> trick to short-circuit
/// <see cref="AkkaHostedService.StartAsync"/> so DI resolution is exercised
/// without the actor system actually being created.
/// </para>
/// </remarks>
public class AkkaHostedServiceAuditWiringHoconTests
{
    [Fact]
    public void BuildHocon_Emits_AuditTelemetryDispatcher_Block()
    {
        // Bundle E acceptance: the HOCON document the host parses must declare
        // the dedicated dispatcher the SiteAuditTelemetryActor binds to. A
        // missing dispatcher block would route the actor to the default
        // dispatcher and silently lose the isolation guarantee.
        var nodeOptions = new NodeOptions
        {
            Role = "Site",
            NodeHostname = "site-test-1",
            RemotingPort = 0,
            SiteId = "TestSite",
        };
        var clusterOptions = new ClusterOptions
        {
            SeedNodes = new List<string> { "akka.tcp://scadabridge@localhost:2551" },
            SplitBrainResolverStrategy = "keep-oldest",
            MinNrOfMembers = 1,
            StableAfter = TimeSpan.FromSeconds(15),
            HeartbeatInterval = TimeSpan.FromSeconds(2),
            FailureDetectionThreshold = TimeSpan.FromSeconds(10),
        };

        var hocon = AkkaHostedService.BuildHocon(
            nodeOptions,
            clusterOptions,
            new[] { "Site", "site-TestSite" },
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(15));

        var config = ConfigurationFactory.ParseString(hocon);

        // The dispatcher is declared at the root, so the lookup is by its
        // unqualified name. The HOCON parser must accept the block as a
        // standalone dispatcher definition the actor system can resolve.
        var dispatcherType = config.GetString("audit-telemetry-dispatcher.type");
        Assert.Equal("ForkJoinDispatcher", dispatcherType);

        var throughput = config.GetInt("audit-telemetry-dispatcher.throughput");
        Assert.Equal(100, throughput);

        var threadCount = config.GetInt("audit-telemetry-dispatcher.dedicated-thread-pool.thread-count");
        Assert.Equal(2, threadCount);
    }
}

/// <summary>
/// Verifies Audit Log (#23) services land in the Central composition root.
/// </summary>
[Collection(HostBootCollection.Name)]
public class CentralAuditWiringTests : IDisposable
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly string? _previousEnv;
    private readonly CentralDbTestEnvironment _dbEnv;

    public CentralAuditWiringTests()
    {
        _previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
        Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central");

        // Supply the pepper, MachineDataDb and the G-4 secret-backed keys
        // (ConfigurationDb / LDAP service-account password / JWT signing key) as
        // whole-key env overrides so the pre-host config builder — which calls
        // AddEnvironmentVariables() before the ${secret:...} expander runs — sees
        // concrete values and the Central-role StartupValidator preflight passes.
        _dbEnv = new CentralDbTestEnvironment();

        _factory = new WebApplicationFactory<Program>()
            .WithWebHostBuilder(builder =>
            {
                builder.ConfigureAppConfiguration((_, config) =>
                {
                    config.AddInMemoryCollection(new Dictionary<string, string?>
                    {
                        ["ScadaBridge:Node:NodeName"] = "central-a",
                        ["ScadaBridge:Node:NodeHostname"] = "localhost",
                        ["ScadaBridge:Node:RemotingPort"] = "0",
                        ["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551",
                        ["ScadaBridge:Cluster:SeedNodes:1"] = "akka.tcp://scadabridge@localhost:2552",
                        ["ScadaBridge:Database:SkipMigrations"] = "true",
                        ["ScadaBridge:Security:JwtSigningKey"] = "test-signing-key-must-be-at-least-32-chars-long!",
                        // Task 1.4: LDAP settings nest under Security:Ldap (shared LdapOptions).
                        // ServiceAccountDn is now required by the library's LdapOptionsValidator
                        // (ValidateOnStart), so it must be present for the host to start.
                        ["ScadaBridge:Security:Ldap:Server"] = "localhost",
                        ["ScadaBridge:Security:Ldap:Port"] = "3893",
                        ["ScadaBridge:Security:Ldap:Transport"] = "None",
                        ["ScadaBridge:Security:Ldap:AllowInsecure"] = "true",
                        ["ScadaBridge:Security:Ldap:SearchBase"] = "dc=zb,dc=local",
                        ["ScadaBridge:Security:Ldap:ServiceAccountDn"] = "cn=admin,dc=zb,dc=local",
                        ["ScadaBridge:InboundApi:ApiKeyPepper"] = "test-inbound-api-key-pepper-at-least-32-chars!",
                    });
                });
                builder.UseSetting("ScadaBridge:Node:Role", "Central");
                builder.UseSetting("ScadaBridge:Database:SkipMigrations", "true");
                builder.ConfigureServices(services =>
                {
                    var descriptorsToRemove = services
                        .Where(d =>
                            d.ServiceType == typeof(DbContextOptions<ScadaBridgeDbContext>) ||
                            d.ServiceType == typeof(DbContextOptions) ||
                            d.ServiceType == typeof(ScadaBridgeDbContext) ||
                            d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true)
                        .ToList();
                    foreach (var d in descriptorsToRemove)
                        services.Remove(d);

                    services.AddDbContext<ScadaBridgeDbContext>(options =>
                        options.UseInMemoryDatabase($"CentralAuditWiringTests_{Guid.NewGuid()}"));

                    AkkaHostedServiceRemover.RemoveAkkaHostedServiceOnly(services);
                });
            });

        _ = _factory.Server;
    }

    public void Dispose()
    {
        // try/finally: a throwing host teardown must not strand the process-wide
        // environment variables for the rest of the run.
        try
        {
            _factory.Dispose();
        }
        finally
        {
            Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", _previousEnv);
            _dbEnv.Dispose();
        }
    }

    [Fact]
    public void Central_Resolves_IAuditWriter_AsFallbackAuditWriter()
    {
        // Central nodes still register the writer chain because AddAuditLog is
        // shared between roles — the registrations are lazy singletons and the
        // writer is never resolved on a central node in production. Asserting
        // it resolves here confirms the chain is intact and ready for the
        // future case where a central-only actor needs to emit audit events.
        var writer = _factory.Services.GetService<IAuditWriter>();
        Assert.NotNull(writer);
        Assert.IsType<FallbackAuditWriter>(writer);
    }

    [Fact]
    public void Central_Resolves_AuditLogOptions()
    {
        var opts = _factory.Services.GetService<IOptions<ZB.MOM.WW.ScadaBridge.AuditLog.Configuration.AuditLogOptions>>();
        Assert.NotNull(opts);
        Assert.NotNull(opts!.Value);
    }

    [Fact]
    public void Central_Resolves_SqliteAuditWriterOptions()
    {
        var opts = _factory.Services.GetService<IOptions<SqliteAuditWriterOptions>>();
        Assert.NotNull(opts);
        Assert.NotNull(opts!.Value);
    }

    [Fact]
    public void Central_Resolves_SiteAuditTelemetryOptions()
    {
        var opts = _factory.Services.GetService<IOptions<SiteAuditTelemetryOptions>>();
        Assert.NotNull(opts);
        Assert.NotNull(opts!.Value);
    }

    [Fact]
    public void Central_Resolves_ISiteStreamAuditClient_AsNoOpDefault()
    {
        var client = _factory.Services.GetService<ISiteStreamAuditClient>();
        Assert.NotNull(client);
        Assert.IsType<NoOpSiteStreamAuditClient>(client);
    }

    /// <summary>
    /// M3 Bundle F (T15): the Central composition root calls
    /// <c>AddSiteCallAudit()</c>. Today that extension is a no-op placeholder,
    /// but invoking it must not throw and the central host's service collection
    /// must build successfully — the actor's Props are constructed inline in
    /// <c>AkkaHostedService</c> (via the root <see cref="IServiceProvider"/>),
    /// not from a DI factory. Asserting the host built confirms the wiring
    /// call is in place; this test guards against accidentally removing it
    /// from <c>Program.cs</c>.
    /// </summary>
    [Fact]
    public void Central_HostBuilds_With_AddSiteCallAudit_Wired()
    {
        // Reaching _factory.Services means WebApplicationFactory built the host
        // (DI validation completed). The fact this test is in the
        // CentralAuditWiringTests fixture means it ran against the Central
        // composition root path through Program.cs.
        Assert.NotNull(_factory.Services);
    }

    /// <summary>
    /// M3 Bundle F: the Central composition root registers
    /// <c>ICachedCallTelemetryForwarder</c> as a lazy singleton (the
    /// forwarder degrades to audit-only emission when the site-only
    /// <c>IOperationTrackingStore</c> is absent, matching the M2 lazy chain
    /// pattern). The binding is exercised here so a future regression that
    /// removes the registration or makes IOperationTrackingStore mandatory
    /// fails on the Central node, not just at first script execution.
    /// </summary>
    [Fact]
    public void Central_Resolves_ICachedCallTelemetryForwarder_LazySingleton()
    {
        var forwarder = _factory.Services.GetService<ICachedCallTelemetryForwarder>();
        Assert.NotNull(forwarder);
        Assert.IsType<CachedCallTelemetryForwarder>(forwarder);
    }

    /// <summary>
    /// I4 (review): the central composition root must register the production
    /// reconciliation collaborators via
    /// <c>AddAuditLogCentralReconciliationClient</c>. Asserting the concrete
    /// implementations resolve here is a faster, clearer signal than a runtime
    /// "actor not found" / cryptic <c>GetRequiredService</c> throw in
    /// <c>AkkaHostedService.RegisterCentralActors</c> if that helper is ever
    /// dropped from <c>Program.cs</c>.
    /// </summary>
    [Fact]
    public void Central_Resolves_ISiteEnumerator_AsSiteEnumerator()
    {
        var enumerator = _factory.Services.GetService<ISiteEnumerator>();
        Assert.NotNull(enumerator);
        Assert.IsType<SiteEnumerator>(enumerator);
    }

    /// <summary>
    /// I4 (review): companion to <see cref="Central_Resolves_ISiteEnumerator_AsSiteEnumerator"/>
    /// — the production gRPC pull client must resolve on the central composition
    /// root so the SiteAuditReconciliationActor singleton can dial sites.
    /// </summary>
    [Fact]
    public void Central_Resolves_IPullAuditEventsClient_AsGrpcClient()
    {
        var client = _factory.Services.GetService<IPullAuditEventsClient>();
        Assert.NotNull(client);
        Assert.IsType<GrpcPullAuditEventsClient>(client);
    }
}

/// <summary>
/// Verifies Audit Log (#23) services land in the Site composition root.
/// </summary>
public class SiteAuditWiringTests : IDisposable
{
    private readonly WebApplication _host;
    private readonly string _tempDbPath;
    private readonly string _tempLocalDbPath;

    public SiteAuditWiringTests()
    {
        _tempDbPath = Path.Combine(Path.GetTempPath(), $"scadabridge_audit_wiring_{Guid.NewGuid()}.db");
        _tempLocalDbPath = Path.Combine(Path.GetTempPath(), $"scadabridge_audit_wiring_localdb_{Guid.NewGuid()}.db");

        var builder = WebApplication.CreateBuilder();
        builder.Configuration.Sources.Clear();
        builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["ScadaBridge:Node:Role"] = "Site",
            ["ScadaBridge:Node:NodeName"] = "node-a",
            ["ScadaBridge:Node:NodeHostname"] = "test-site",
            ["ScadaBridge:Node:SiteId"] = "TestSite",
            ["ScadaBridge:Node:RemotingPort"] = "0",
            ["ScadaBridge:Node:GrpcPort"] = "0",
            ["ScadaBridge:Database:SiteDbPath"] = _tempDbPath,
            ["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551",
            ["ScadaBridge:Cluster:SeedNodes:1"] = "akka.tcp://scadabridge@localhost:2552",
            // SqliteAuditWriter would attempt to open a SQLite file when first
            // resolved; point it at an in-memory connection so the test doesn't
            // pollute the working directory.
            ["AuditLog:SiteWriter:DatabasePath"] = ":memory:",
            // The cached-call telemetry forwarder pulls in IOperationTrackingStore,
            // which now resolves ILocalDb — so the consolidated site database must be
            // configured or the whole graph fails to resolve. There is no in-memory
            // mode: LocalDbOptions.Path is a filesystem path.
            ["LocalDb:Path"] = _tempLocalDbPath,
        });

        builder.Services.AddGrpc();
        builder.Services.AddSingleton<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
        SiteServiceRegistration.Configure(builder.Services, builder.Configuration);
        AkkaHostedServiceRemover.RemoveAkkaHostedServiceOnly(builder.Services);

        _host = builder.Build();
    }

    public void Dispose()
    {
        (_host as IDisposable)?.Dispose();
        try { File.Delete(_tempDbPath); } catch { /* best effort */ }
        try { File.Delete(_tempLocalDbPath); } catch { /* best effort */ }
    }

    [Fact]
    public void Site_Resolves_IAuditWriter_AsFallbackAuditWriter()
    {
        var writer = _host.Services.GetService<IAuditWriter>();
        Assert.NotNull(writer);
        Assert.IsType<FallbackAuditWriter>(writer);
    }

    [Fact]
    public void Site_Resolves_SqliteAuditWriter_AsSingleton()
    {
        var a = _host.Services.GetService<SqliteAuditWriter>();
        var b = _host.Services.GetService<SqliteAuditWriter>();
        Assert.NotNull(a);
        Assert.NotNull(b);
        Assert.Same(a, b);
    }

    [Fact]
    public void Site_ISiteAuditQueue_AndSqliteAuditWriter_AreSameInstance()
    {
        // The telemetry actor reads from ISiteAuditQueue while ScriptRuntimeContext
        // writes through IAuditWriter → SqliteAuditWriter. If these don't resolve
        // to the same instance, pending rows are invisible to the actor.
        var queue = _host.Services.GetService<ISiteAuditQueue>();
        var writer = _host.Services.GetService<SqliteAuditWriter>();
        Assert.NotNull(queue);
        Assert.NotNull(writer);
        Assert.Same(writer, queue);
    }

    [Fact]
    public void Site_Resolves_RingBufferFallback()
    {
        var ring = _host.Services.GetService<RingBufferFallback>();
        Assert.NotNull(ring);
    }

    [Fact]
    public void Site_Resolves_IAuditWriteFailureCounter_AsHealthMetricsBridge()
    {
        // Bundle G (M2-T11): site composition root calls
        // AddAuditLogHealthMetricsBridge() after AddAuditLog + AddSiteHealthMonitoring,
        // which swaps the NoOp default for the real health-metrics bridge so
        // FallbackAuditWriter primary failures surface in the site health
        // report payload as SiteAuditWriteFailures.
        var counter = _host.Services.GetService<IAuditWriteFailureCounter>();
        Assert.NotNull(counter);
        Assert.IsType<HealthMetricsAuditWriteFailureCounter>(counter);
    }

    [Fact]
    public void Site_Resolves_ISiteStreamAuditClient_AsNoOpDefault()
    {
        var client = _host.Services.GetService<ISiteStreamAuditClient>();
        Assert.NotNull(client);
        Assert.IsType<NoOpSiteStreamAuditClient>(client);
    }

    [Fact]
    public void Site_Resolves_SiteAuditTelemetryOptions_WithDefaults()
    {
        var opts = _host.Services.GetService<IOptions<SiteAuditTelemetryOptions>>();
        Assert.NotNull(opts);
        Assert.Equal(256, opts!.Value.BatchSize);
        Assert.Equal(5, opts.Value.BusyIntervalSeconds);
        Assert.Equal(30, opts.Value.IdleIntervalSeconds);
    }

    /// <summary>
    /// M3 Bundle F (T15): the site composition root resolves the cached-call
    /// telemetry forwarder. ScriptExecutionActor consumes this through
    /// <c>GetService&lt;ICachedCallTelemetryForwarder&gt;()</c> on every script
    /// execution; a missing registration would silently degrade
    /// <c>ExternalSystem.CachedCall</c> / <c>Database.CachedWrite</c> to the
    /// "no-emission" path and break the M3 audit pipeline.
    /// </summary>
    [Fact]
    public void Site_Resolves_ICachedCallTelemetryForwarder()
    {
        var forwarder = _host.Services.GetService<ICachedCallTelemetryForwarder>();
        Assert.NotNull(forwarder);
        Assert.IsType<CachedCallTelemetryForwarder>(forwarder);
    }

    /// <summary>
    /// M3 Bundle F (T15): the site composition root resolves the lifecycle
    /// bridge that translates S&amp;F retry-loop attempt notifications into
    /// cached-call telemetry packets.
    /// </summary>
    [Fact]
    public void Site_Resolves_CachedCallLifecycleBridge_AsSingleton()
    {
        var a = _host.Services.GetService<CachedCallLifecycleBridge>();
        var b = _host.Services.GetService<CachedCallLifecycleBridge>();
        Assert.NotNull(a);
        Assert.NotNull(b);
        Assert.Same(a, b);
    }

    /// <summary>
    /// M3 Bundle F (T15): the lifecycle bridge is bound to the
    /// <see cref="ICachedCallLifecycleObserver"/> contract that
    /// StoreAndForwardService consults at construction time. Without this
    /// binding the S&amp;F service is built with a null observer and the
    /// retry-loop telemetry never reaches the audit pipeline.
    /// </summary>
    [Fact]
    public void Site_ICachedCallLifecycleObserver_IsTheLifecycleBridge()
    {
        var observer = _host.Services.GetService<ICachedCallLifecycleObserver>();
        var bridge = _host.Services.GetService<CachedCallLifecycleBridge>();
        Assert.NotNull(observer);
        Assert.NotNull(bridge);
        Assert.Same(bridge, observer);
    }

    /// <summary>
    /// M3 Bundle F (T15): the Host registers an
    /// <see cref="IStoreAndForwardSiteContext"/> adapter so the S&amp;F service
    /// can resolve the site id at composition time WITHOUT introducing a
    /// StoreAndForward → HealthMonitoring project-reference cycle.
    /// </summary>
    [Fact]
    public void Site_Resolves_IStoreAndForwardSiteContext_FromHost()
    {
        var ctx = _host.Services.GetService<IStoreAndForwardSiteContext>();
        Assert.NotNull(ctx);
        Assert.Equal("TestSite", ctx!.SiteId);
    }
}
