using Akka.Actor;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.LocalDb;
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.Commons.Interfaces;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Integration;
using ZB.MOM.WW.ScadaBridge.AuditLog.Tests.TestSupport;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tracking;

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

/// <summary>
/// Shared end-to-end harness for the M3 cached-call combined telemetry tests
/// (G2/G3/G4). Composes the full pipeline:
/// <list type="bullet">
///   <item><description>Site-local SQLite <see cref="SqliteAuditWriter"/> (in-memory) +
///   <see cref="RingBufferFallback"/> + <see cref="FallbackAuditWriter"/>.</description></item>
///   <item><description>Site-local SQLite <see cref="OperationTrackingStore"/> (in-memory).</description></item>
///   <item><description>Production <see cref="CachedCallTelemetryForwarder"/> wrapped by a
///   test-side <see cref="CombinedTelemetryDispatcher"/> that also pushes each
///   packet through the stub gRPC client.</description></item>
///   <item><description><see cref="CachedCallLifecycleBridge"/> wired to the
///   dispatcher so a single observer call fans out audit + tracking + wire.</description></item>
///   <item><description><see cref="DirectActorSiteStreamAuditClient"/> connected
///   to an <see cref="AuditLogIngestActor"/> backed by the real
///   <see cref="AuditLogRepository"/> + <see cref="SiteCallAuditRepository"/>
///   against the per-test <see cref="MsSqlMigrationFixture"/> database.</description></item>
/// </list>
/// </summary>
/// <remarks>
/// <para>
/// Disposal cleans up the in-memory SQLite stores. The Akka actor system is
/// owned by the calling <see cref="Akka.TestKit.Xunit2.TestKit"/>; the harness
/// only owns the ingest actor IActorRef and the underlying repositories'
/// DbContext lifecycle.
/// </para>
/// </remarks>
public sealed class CombinedTelemetryHarness : IAsyncDisposable
{
    public SqliteAuditWriter SqliteWriter { get; }
    public RingBufferFallback Ring { get; }
    public FallbackAuditWriter FallbackWriter { get; }
    public OperationTrackingStore TrackingStore { get; }
    public CachedCallTelemetryForwarder InnerForwarder { get; }
    public CombinedTelemetryDispatcher Dispatcher { get; }
    public CachedCallLifecycleBridge Bridge { get; }
    public DirectActorSiteStreamAuditClient StubClient { get; }
    public IActorRef IngestActor { get; }
    public IServiceProvider ServiceProvider { get; }

    private readonly MsSqlMigrationFixture _fixture;
    private readonly string _trackingDbPath;
    private readonly ServiceProvider _trackingLocalDbProvider;
    private bool _disposed;

    public CombinedTelemetryHarness(
        MsSqlMigrationFixture fixture,
        Akka.TestKit.Xunit2.TestKit testKit,
        Func<ScadaBridgeDbContext, ISiteCallAuditRepository>? siteCallRepoOverride = null)
    {
        _fixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
        ArgumentNullException.ThrowIfNull(testKit);

        // Site SQLite — unique in-memory database per harness so tests don't share
        // an audit queue. Mode=Memory + Cache=Shared keeps the file alive for the
        // lifetime of the writer connection.
        SqliteWriter = new SqliteAuditWriter(
            Options.Create(new SqliteAuditWriterOptions
            {
                DatabasePath = "ignored",
                BatchSize = 64,
                ChannelCapacity = 1024,
            }),
            NullLogger<SqliteAuditWriter>.Instance,
            new FakeNodeIdentityProvider(),
            connectionStringOverride:
                $"Data Source=file:cachedcall-g-{Guid.NewGuid():N}?mode=memory&cache=shared");

        Ring = new RingBufferFallback();
        FallbackWriter = new FallbackAuditWriter(
            SqliteWriter, Ring, new NoOpAuditWriteFailureCounter(),
            NullLogger<FallbackAuditWriter>.Instance);

        // The tracking store now runs on the consolidated ILocalDb rather than its own
        // connection string, so it needs a real database FILE — LocalDbOptions.Path is
        // a filesystem path and there is no in-memory mode. Unique per harness, torn
        // down in DisposeAsync alongside its WAL sidecars.
        _trackingDbPath = Path.Combine(
            Path.GetTempPath(), $"tracking-g-{Guid.NewGuid():N}.db");
        _trackingLocalDbProvider = new ServiceCollection()
            .AddZbLocalDb(new ConfigurationBuilder()
                .AddInMemoryCollection(new Dictionary<string, string?>
                {
                    ["LocalDb:Path"] = _trackingDbPath,
                })
                .Build())
            .BuildServiceProvider();

        TrackingStore = new OperationTrackingStore(
            _trackingLocalDbProvider.GetRequiredService<ILocalDb>(),
            NullLogger<OperationTrackingStore>.Instance);

        // Central wiring: real repositories backed by the MSSQL fixture's DB.
        ServiceProvider = BuildCentralServiceProvider(siteCallRepoOverride);
        IngestActor = testKit.Sys.ActorOf(Props.Create(() => new AuditLogIngestActor(
            ServiceProvider,
            NullLogger<AuditLogIngestActor>.Instance)));

        StubClient = new DirectActorSiteStreamAuditClient(IngestActor);

        // Production forwarder writes the local stores; the dispatcher wraps
        // it to ALSO push the same packet to central via the stub client.
        InnerForwarder = new CachedCallTelemetryForwarder(
            FallbackWriter, TrackingStore, NullLogger<CachedCallTelemetryForwarder>.Instance);
        Dispatcher = new CombinedTelemetryDispatcher(InnerForwarder, StubClient);

        Bridge = new CachedCallLifecycleBridge(Dispatcher, NullLogger<CachedCallLifecycleBridge>.Instance);
    }

    /// <summary>
    /// Convenience: emit the initial submit packet directly through the
    /// dispatcher (the bridge's hooks fire only for S&amp;F retry-loop
    /// attempts; submit-row emission happens at the script call site).
    /// </summary>
    public Task EmitSubmitAsync(CachedCallTelemetry submit, CancellationToken ct = default) =>
        Dispatcher.ForwardAsync(submit, ct);

    /// <summary>
    /// Convenience: route a per-attempt or terminal outcome through the bridge.
    /// </summary>
    public Task EmitAttemptAsync(CachedCallAttemptContext context, CancellationToken ct = default) =>
        Bridge.OnAttemptCompletedAsync(context, ct);

    public ScadaBridgeDbContext CreateReadContext()
    {
        var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
            .UseSqlServer(_fixture.ConnectionString)
            .Options;
        return new ScadaBridgeDbContext(options);
    }

    private IServiceProvider BuildCentralServiceProvider(
        Func<ScadaBridgeDbContext, ISiteCallAuditRepository>? siteCallRepoOverride)
    {
        var services = new ServiceCollection();
        services.AddDbContext<ScadaBridgeDbContext>(opts =>
            opts.UseSqlServer(_fixture.ConnectionString)
                .ConfigureWarnings(w => w.Ignore(
                    Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)));
        services.AddScoped<IAuditLogRepository>(sp =>
            new AuditLogRepository(sp.GetRequiredService<ScadaBridgeDbContext>()));
        if (siteCallRepoOverride is null)
        {
            services.AddScoped<ISiteCallAuditRepository>(sp =>
                new SiteCallAuditRepository(sp.GetRequiredService<ScadaBridgeDbContext>()));
        }
        else
        {
            services.AddScoped(sp =>
                siteCallRepoOverride(sp.GetRequiredService<ScadaBridgeDbContext>()));
        }
        return services.BuildServiceProvider();
    }

    public async ValueTask DisposeAsync()
    {
        if (_disposed) return;
        _disposed = true;
        await SqliteWriter.DisposeAsync().ConfigureAwait(false);
        await TrackingStore.DisposeAsync().ConfigureAwait(false);

        // Dispose the ILocalDb before deleting the file — it holds an open master
        // connection anchoring the WAL.
        await _trackingLocalDbProvider.DisposeAsync().ConfigureAwait(false);
        foreach (var suffix in new[] { "", "-wal", "-shm" })
        {
            try { File.Delete(_trackingDbPath + suffix); } catch { /* best effort */ }
        }

        if (ServiceProvider is IAsyncDisposable asyncSp)
        {
            await asyncSp.DisposeAsync().ConfigureAwait(false);
        }
        else if (ServiceProvider is IDisposable sp)
        {
            sp.Dispose();
        }
    }
}
