using Akka.Actor;
using Akka.Cluster;
using Akka.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
using ZB.MOM.WW.OtOpcUa.Host.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Telemetry;

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

/// <summary>
///     Pure-DI (no fixture) coverage of the publisher's snapshot→message mapping — the only unit-testable
///     seam of <see cref="DriverResilienceStatusPublisherService"/>. The DPS <c>Publish</c> shell is
///     verified by the R2-10 live gate; this asserts the mapping the shell publishes.
/// </summary>
public sealed class DriverResilienceStatusPublisherServiceTests
{
    private static readonly DateTime Published = new(2026, 7, 13, 9, 0, 0, DateTimeKind.Utc);
    private static readonly DateTime Sampled = new(2026, 7, 13, 8, 59, 0, DateTimeKind.Utc);

    [Fact]
    public void BuildMessages_EmptyTracker_ReturnsEmpty()
    {
        var tracker = new DriverResilienceStatusTracker();

        var messages = DriverResilienceStatusPublisherService.BuildMessages(tracker, Published);

        messages.ShouldBeEmpty();
    }

    [Fact]
    public void BuildMessages_MapsTrackerSnapshot_OneMessagePerInstanceHost()
    {
        var tracker = new DriverResilienceStatusTracker();
        // (drv-1, host-a): an open breaker with a standing failure count.
        tracker.RecordFailure("drv-1", "host-a", Sampled);
        tracker.RecordFailure("drv-1", "host-a", Sampled);
        tracker.RecordBreakerOpen("drv-1", "host-a", Sampled);
        // (drv-1, host-b): a healthy host with in-flight calls.
        tracker.RecordCallStart("drv-1", "host-b");

        var messages = DriverResilienceStatusPublisherService.BuildMessages(tracker, Published);

        messages.Count.ShouldBe(2);

        var open = messages.Single(m => m.HostName == "host-a");
        open.DriverInstanceId.ShouldBe("drv-1");
        open.BreakerOpen.ShouldBeTrue();
        open.ConsecutiveFailures.ShouldBe(2);
        open.LastBreakerOpenUtc.ShouldBe(Sampled);
        open.PublishedUtc.ShouldBe(Published);

        var healthy = messages.Single(m => m.HostName == "host-b");
        healthy.BreakerOpen.ShouldBeFalse();
        healthy.ConsecutiveFailures.ShouldBe(0);
        healthy.CurrentInFlight.ShouldBe(1);
        healthy.PublishedUtc.ShouldBe(Published);
    }

    /// <summary>Per-cluster mesh Phase 5 — each snapshot the publish tick sends to the
    /// <c>driver-resilience-status</c> DPS topic is also emitted into the node-local
    /// <see cref="ITelemetryLocalHub"/> as a <see cref="TelemetryItem.Resilience"/>. Runs the background
    /// service against a single-node cluster (so DistributedPubSub resolves) for a few ticks and asserts the
    /// hub captured the tracked pair.</summary>
    [Fact]
    public async Task ExecuteAsync_emits_Resilience_item_per_published_snapshot()
    {
        // Plain self-joined single-node cluster (mirrors SecretsReplicationRegistrationTests — the
        // xunit.v3 project cannot use Akka.TestKit.Xunit2). public-hostname is load-bearing for the bind.
        var hocon = ConfigurationFactory.ParseString("""
            akka {
              loglevel = WARNING
              actor.provider = cluster
              remote.dot-netty.tcp {
                hostname = "127.0.0.1"
                public-hostname = "127.0.0.1"
                port = 0
              }
              cluster { roles = ["driver"] }
            }
            """);
        var sys = ActorSystem.Create("resil-hub-test", hocon);
        try
        {
            var cluster = Akka.Cluster.Cluster.Get(sys);
            cluster.Join(cluster.SelfAddress);
            var upDeadline = DateTime.UtcNow.AddSeconds(10);
            while (!cluster.State.Members.Any(m => m.Status == MemberStatus.Up) && DateTime.UtcNow < upDeadline)
                await Task.Delay(50, TestContext.Current.CancellationToken);
            cluster.State.Members.ShouldContain(m => m.Status == MemberStatus.Up);

            var tracker = new DriverResilienceStatusTracker();
            tracker.RecordCallStart("drv-1", "host-a");

            var hub = new CapturingHub();
            var service = new DriverResilienceStatusPublisherService(
                tracker, () => sys, NullLogger<DriverResilienceStatusPublisherService>.Instance, hub,
                interval: TimeSpan.FromMilliseconds(50));

            await service.StartAsync(TestContext.Current.CancellationToken);
            try
            {
                var deadline = DateTime.UtcNow.AddSeconds(5);
                while (hub.Items.Count == 0 && DateTime.UtcNow < deadline)
                    await Task.Delay(50, TestContext.Current.CancellationToken);
            }
            finally
            {
                await service.StopAsync(TestContext.Current.CancellationToken);
            }

            hub.Items.ShouldNotBeEmpty();
            var item = hub.Items[0].ShouldBeOfType<TelemetryItem.Resilience>();
            item.E.DriverInstanceId.ShouldBe("drv-1");
            item.E.HostName.ShouldBe("host-a");
            item.E.CurrentInFlight.ShouldBe(1);
        }
        finally
        {
            await sys.Terminate();
        }
    }

    /// <summary>Fake hub recording every emit; <see cref="Subscribe"/> is unused here.</summary>
    private sealed class CapturingHub : ITelemetryLocalHub
    {
        public List<TelemetryItem> Items { get; } = new();

        public void Emit(TelemetryItem item)
        {
            lock (Items) Items.Add(item);
        }

        public ITelemetrySubscription Subscribe(int boundedCapacity) => throw new NotSupportedException();
    }
}
