using Akka.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;

namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests;

/// <summary>
/// Verifies <c>WithOtOpcUaRuntimeActors</c> spawns <c>DriverHostActor</c> + <c>DbHealthProbeActor</c>
/// on the host's <c>ActorSystem</c> and registers both under their marker keys. This is the
/// driver-role mirror of the admin-role <c>WithOtOpcUaControlPlaneSingletons</c> bootstrap.
/// </summary>
public sealed class ServiceCollectionExtensionsTests
{
    /// <summary>Verifies that WithOtOpcUaRuntimeActors spawns driver host and DB health probe actors.</summary>
    [Fact]
    public async Task WithOtOpcUaRuntimeActors_spawns_driver_host_and_db_health_probe()
    {
        using var host = Host.CreateDefaultBuilder()
            .ConfigureServices((_, services) =>
            {
                services.AddSingleton<IDbContextFactory<OtOpcUaConfigDbContext>>(
                    new InMemoryConfigDbFactory(Guid.NewGuid().ToString("N")));
                services.AddSingleton<IClusterRoleInfo>(new FakeClusterRoleInfo());

                services.AddAkka("otopcua-test", (ab, _) =>
                {
                    ab.AddHocon(@"
                        akka.actor.provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster""
                        akka.remote.dot-netty.tcp.hostname = ""127.0.0.1""
                        akka.remote.dot-netty.tcp.port = 0
                        akka.cluster.seed-nodes = []
                        akka.cluster.roles = [""driver""]
                    ", HoconAddMode.Prepend);
                    ab.WithOtOpcUaRuntimeActors();
                });
            })
            .Build();

        await host.StartAsync();
        try
        {
            var driverHost = host.Services.GetRequiredService<IRequiredActor<DriverHostActorKey>>();
            var dbHealth = host.Services.GetRequiredService<IRequiredActor<DbHealthProbeActorKey>>();
            var historian = host.Services.GetRequiredService<IRequiredActor<HistorianAdapterActorKey>>();
            var mux = host.Services.GetRequiredService<IRequiredActor<DependencyMuxActorKey>>();
            var publish = host.Services.GetRequiredService<IRequiredActor<OpcUaPublishActorKey>>();
            var peerProbes = host.Services.GetRequiredService<IRequiredActor<PeerProbeSupervisorKey>>();

            driverHost.ActorRef.ShouldNotBeNull();
            dbHealth.ActorRef.ShouldNotBeNull();
            historian.ActorRef.ShouldNotBeNull();
            mux.ActorRef.ShouldNotBeNull();
            publish.ActorRef.ShouldNotBeNull();
            peerProbes.ActorRef.ShouldNotBeNull();
            driverHost.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.DriverHostActorName);
            dbHealth.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.DbHealthProbeActorName);
            historian.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.HistorianAdapterActorName);
            mux.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.DependencyMuxActorName);
            publish.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.OpcUaPublishActorName);
            peerProbes.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.PeerProbeSupervisorName);
        }
        finally
        {
            await host.StopAsync();
        }
    }

    /// <summary>
    ///     When <c>ContinuousHistorization</c> is not enabled (no options registered), the recorder
    ///     actor is NOT spawned — its key does not resolve in the registry.
    /// </summary>
    [Fact]
    public async Task Recorder_not_spawned_when_continuous_historization_disabled()
    {
        using var host = BuildRuntimeActorHost(extra: null);

        await host.StartAsync();
        try
        {
            var registry = host.Services.GetRequiredService<ActorRegistry>();
            registry.TryGet<ContinuousHistorizationRecorderKey>(out _).ShouldBeFalse();
        }
        finally
        {
            await host.StopAsync();
        }
    }

    /// <summary>
    ///     When <c>ContinuousHistorization</c> is enabled and a value-writer + outbox are registered,
    ///     the recorder actor IS spawned and its key resolves under the expected actor name.
    /// </summary>
    [Fact]
    public async Task Recorder_spawned_when_enabled_with_writer_and_outbox()
    {
        using var host = BuildRuntimeActorHost(extra: services =>
        {
            services.AddSingleton(new ContinuousHistorizationOptions { Enabled = true, OutboxPath = "x" });
            services.AddSingleton<IHistorianValueWriter>(new FakeValueWriter());
            services.AddSingleton<IHistorizationOutbox>(new FakeOutbox());
        });

        await host.StartAsync();
        try
        {
            var recorder = host.Services.GetRequiredService<IRequiredActor<ContinuousHistorizationRecorderKey>>();
            recorder.ActorRef.ShouldNotBeNull();
            recorder.ActorRef.Path.Name.ShouldBe(ServiceCollectionExtensions.ContinuousHistorizationRecorderActorName);
        }
        finally
        {
            await host.StopAsync();
        }
    }

    /// <summary>Builds a driver-role host that runs <c>WithOtOpcUaRuntimeActors</c>, with optional
    /// extra DI registrations applied before <c>AddAkka</c>.</summary>
    private static IHost BuildRuntimeActorHost(Action<IServiceCollection>? extra)
        => Host.CreateDefaultBuilder()
            .ConfigureServices((_, services) =>
            {
                services.AddSingleton<IDbContextFactory<OtOpcUaConfigDbContext>>(
                    new InMemoryConfigDbFactory(Guid.NewGuid().ToString("N")));
                services.AddSingleton<IClusterRoleInfo>(new FakeClusterRoleInfo());
                extra?.Invoke(services);

                services.AddAkka("otopcua-test", (ab, _) =>
                {
                    ab.AddHocon(@"
                        akka.actor.provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster""
                        akka.remote.dot-netty.tcp.hostname = ""127.0.0.1""
                        akka.remote.dot-netty.tcp.port = 0
                        akka.cluster.seed-nodes = []
                        akka.cluster.roles = [""driver""]
                    ", HoconAddMode.Prepend);
                    ab.WithOtOpcUaRuntimeActors();
                });
            })
            .Build();

    /// <summary>Non-throwing fake value writer that acks every batch.</summary>
    private sealed class FakeValueWriter : IHistorianValueWriter
    {
        /// <summary>Acks the write unconditionally.</summary>
        public Task<bool> WriteLiveValuesAsync(
            string tag, IReadOnlyList<HistorizationValue> values, CancellationToken ct)
            => Task.FromResult(true);
    }

    /// <summary>Empty in-memory outbox fake — the spawn test only needs construction, not draining.</summary>
    private sealed class FakeOutbox : IHistorizationOutbox
    {
        /// <summary>Never drops (unbounded).</summary>
        public long DroppedCount => 0;
        /// <summary>No-op append.</summary>
        public ValueTask AppendAsync(HistorizationOutboxEntry entry, CancellationToken ct) => ValueTask.CompletedTask;
        /// <summary>Always returns an empty batch.</summary>
        public ValueTask<IReadOnlyList<HistorizationOutboxEntry>> PeekBatchAsync(int max, CancellationToken ct)
            => ValueTask.FromResult<IReadOnlyList<HistorizationOutboxEntry>>(Array.Empty<HistorizationOutboxEntry>());
        /// <summary>No-op remove.</summary>
        public ValueTask RemoveAsync(Guid id, CancellationToken ct) => ValueTask.CompletedTask;
        /// <summary>Always empty.</summary>
        public ValueTask<int> CountAsync(CancellationToken ct) => ValueTask.FromResult(0);
        /// <summary>No-op dispose.</summary>
        public void Dispose() { }
    }

    /// <summary>In-memory database factory for testing.</summary>
    private sealed class InMemoryConfigDbFactory(string dbName) : IDbContextFactory<OtOpcUaConfigDbContext>
    {
        /// <summary>Creates a new in-memory database context.</summary>
        public OtOpcUaConfigDbContext CreateDbContext()
        {
            var opts = new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
                .UseInMemoryDatabase(dbName)
                .Options;
            return new OtOpcUaConfigDbContext(opts);
        }
    }

    /// <summary>Fake cluster role information for testing.</summary>
    private sealed class FakeClusterRoleInfo : IClusterRoleInfo
    {
        /// <summary>Gets the local node ID.</summary>
        public NodeId LocalNode { get; } = NodeId.Parse("test-node");
        /// <summary>Gets the local roles.</summary>
        public IReadOnlySet<string> LocalRoles { get; } = new HashSet<string>(["driver"]);
        /// <summary>Gets the local node's cluster-scoped role. None configured in this fake.</summary>
        public string? ClusterRole => null;
        /// <summary>Gets the ClusterId extracted from <see cref="ClusterRole"/>. None configured in this fake.</summary>
        public string? ClusterId => null;
        /// <summary>Determines whether the local node has the specified role.</summary>
        /// <param name="role">The role to check.</param>
        public bool HasRole(string role) => LocalRoles.Contains(role);

        /// <summary>Gets the members with the specified role.</summary>
        /// <param name="role">The role to query.</param>
        public IReadOnlyList<NodeId> MembersWithRole(string role) => Array.Empty<NodeId>();

        /// <summary>Gets the leader node for the specified role.</summary>
        /// <param name="role">The role to query.</param>
        public NodeId? RoleLeader(string role) => null;
        /// <summary>Raised when the role leader changes.</summary>
        public event EventHandler<RoleLeaderChangedEventArgs>? RoleLeaderChanged
        {
            add { _ = value; }
            remove { _ = value; }
        }
    }
}
