using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
using ZB.MOM.WW.ScadaBridge.TestSupport;
using System.Text.Json;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;

/// <summary>
/// Regression test for SiteRuntime-015 — <see cref="DeploymentManagerActor"/> must
/// reuse a single, injected <see cref="ILoggerFactory"/> for every Instance Actor it
/// creates rather than newing (and leaking) a fresh <see cref="LoggerFactory"/> per
/// instance.
/// </summary>
public class DeploymentManagerLoggerFactoryTests : TestKit, IDisposable
{
    private readonly SiteStorageService _storage;
    private readonly ScriptCompilationService _compilationService;
    private readonly SharedScriptLibrary _sharedScriptLibrary;
    private readonly TestLocalDb _localDb;

    public DeploymentManagerLoggerFactoryTests()
    {
        _localDb = TestLocalDb.CreateTemp("dm-loggerfactory-test");
        _storage = new SiteStorageService(
            _localDb.Db,
            NullLogger<SiteStorageService>.Instance);
        _storage.InitializeAsync().GetAwaiter().GetResult();
        _compilationService = new ScriptCompilationService(
            NullLogger<ScriptCompilationService>.Instance);
        _sharedScriptLibrary = new SharedScriptLibrary(
            _compilationService, NullLogger<SharedScriptLibrary>.Instance);
    }

    void IDisposable.Dispose()
    {
        // TestKit teardown first, so no in-flight actor can reach a disposed ILocalDb;
        // then dispose the database before deleting — the master connection anchors the WAL.
        Shutdown();
        var path = _localDb.Path;
        _localDb.Dispose();
        TestLocalDb.DeleteFiles(path);
    }

    private static string MakeConfigJson(string instanceName)
    {
        var config = new FlattenedConfiguration
        {
            InstanceUniqueName = instanceName,
            Attributes =
            [
                new ResolvedAttribute { CanonicalName = "TestAttr", Value = "1", DataType = "Int32" }
            ]
        };
        return JsonSerializer.Serialize(config);
    }

    /// <summary>
    /// Counts <see cref="ILoggerFactory.CreateLogger"/> calls and records whether
    /// the factory was disposed. A passing test proves the single injected factory
    /// is the one used for every Instance Actor.
    /// </summary>
    private sealed class CountingLoggerFactory : ILoggerFactory
    {
        public int CreateLoggerCalls;
        public bool Disposed;

        public ILogger CreateLogger(string categoryName)
        {
            Interlocked.Increment(ref CreateLoggerCalls);
            return NullLogger.Instance;
        }

        public void AddProvider(ILoggerProvider provider) { }
        public void Dispose() => Disposed = true;
    }

    [Fact]
    public async Task CreateInstanceActor_ReusesInjectedLoggerFactory_ForEveryInstance()
    {
        // Pre-populate several enabled instances so startup creates multiple
        // Instance Actors.
        const int instanceCount = 6;
        for (int i = 0; i < instanceCount; i++)
        {
            var name = $"Inst{i}";
            await _storage.StoreDeployedConfigAsync(name, MakeConfigJson(name), $"d{i}", $"h{i}", true);
        }

        var loggerFactory = new CountingLoggerFactory();

        var actor = ActorOf(Props.Create(() => new DeploymentManagerActor(
            _storage,
            _compilationService,
            _sharedScriptLibrary,
            null,
            new SiteRuntimeOptions { StartupBatchSize = 100, StartupBatchDelayMs = 5 },
            NullLogger<DeploymentManagerActor>.Instance,
            // dclManager, healthCollector, serviceProvider — padded positionally because
            // Props.Create is an expression tree and rejects out-of-position named args.
            null, null, null,
            loggerFactory: loggerFactory)));

        // Allow async startup (load configs + staggered creation).
        await Task.Delay(2000);

        // Every Instance Actor logger must come from the single injected factory.
        // Before the fix, each CreateInstanceActor allocated its own LoggerFactory,
        // so the injected factory would never be touched (CreateLoggerCalls == 0).
        Assert.Equal(instanceCount, loggerFactory.CreateLoggerCalls);
    }
}
