using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using Akka.Actor;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
using ZB.MOM.WW.OtOpcUa.Runtime.Health;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;

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

public sealed class HealthProbeActorTests : RuntimeActorTestBase
{
    /// <summary>Verifies that the DB health probe actor returns reachable status against an in-memory database.</summary>
    [Fact]
    public async Task DbHealthProbeActor_returns_reachable_against_in_memory_db()
    {
        var db = NewInMemoryDbFactory();
        var actor = Sys.ActorOf(DbHealthProbeActor.Props(db));

        var status = await actor.Ask<DbHealthProbeActor.DbHealthStatus>(
            DbHealthProbeActor.GetStatus.Instance, TimeSpan.FromSeconds(3));

        status.Reachable.ShouldBeTrue();
        status.LastError.ShouldBeNull();
    }

    /// <summary>Verifies that the peer OPC UA probe actor reports Ok true against a live listener.</summary>
    [Fact]
    public void PeerOpcUaProbeActor_reports_Ok_true_against_a_live_listener()
    {
        using var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        var received = new System.Collections.Generic.List<object>();
        Sys.ActorOf(PeerOpcUaProbeActor.Props(
            NodeId.Parse($"127.0.0.1:{port}"),
            interval: TimeSpan.FromMilliseconds(50),
            connectTimeout: TimeSpan.FromMilliseconds(500),
            opcUaPort: port,
            broadcast: msg => received.Add(msg)));

        AwaitCondition(() => received.OfType<PeerOpcUaProbeActor.OpcUaProbeResult>().Any(r => r.Ok),
            TimeSpan.FromSeconds(3));
    }

    /// <summary>Verifies that the peer OPC UA probe actor reports Ok false against an unreachable endpoint.</summary>
    [Fact]
    public void PeerOpcUaProbeActor_reports_Ok_false_against_an_unreachable_endpoint()
    {
        // Port 1 is reserved (tcpmux) and almost never bound on dev machines, so the connect fails fast.
        var received = new System.Collections.Generic.List<object>();
        Sys.ActorOf(PeerOpcUaProbeActor.Props(
            NodeId.Parse("127.0.0.1:1"),
            interval: TimeSpan.FromMilliseconds(50),
            connectTimeout: TimeSpan.FromMilliseconds(300),
            opcUaPort: 1,
            broadcast: msg => received.Add(msg)));

        AwaitCondition(() => received.OfType<PeerOpcUaProbeActor.OpcUaProbeResult>().Any(r => !r.Ok),
            TimeSpan.FromSeconds(3));
    }

    /// <summary>Verifies that the historian adapter actor forwards events to the injected sink.</summary>
    [Fact]
    public void HistorianAdapterActor_forwards_events_to_injected_sink()
    {
        var sink = new RecordingSink();
        var actor = Sys.ActorOf(HistorianAdapterActor.Props(sink));

        for (var i = 0; i < 5; i++)
            actor.Tell(new AlarmHistorianEvent(
                AlarmId: $"alm-{i}",
                EquipmentPath: "Plant/LineA",
                AlarmName: $"Alarm{i}",
                AlarmTypeName: "LimitAlarm",
                Severity: AlarmSeverity.High,
                EventKind: "Activated",
                Message: $"Test alarm {i}",
                User: "system",
                Comment: null,
                TimestampUtc: DateTime.UtcNow));

        AwaitCondition(() => sink.Enqueued.Count == 5, TimeSpan.FromSeconds(2));
        sink.Enqueued.Select(e => e.AlarmId).OrderBy(s => s).ShouldBe(
            new[] { "alm-0", "alm-1", "alm-2", "alm-3", "alm-4" });
    }

    /// <summary>Verifies that the historian adapter actor returns sink status via GetStatus.</summary>
    [Fact]
    public async Task HistorianAdapterActor_returns_sink_status_via_GetStatus()
    {
        var sink = new RecordingSink();
        var actor = Sys.ActorOf(HistorianAdapterActor.Props(sink));

        actor.Tell(new AlarmHistorianEvent(
            "alm-x", "Plant/LineB", "OffNormal", "OffNormalAlarm",
            AlarmSeverity.Low, "Activated", "msg", "system", null, DateTime.UtcNow));
        AwaitCondition(() => sink.Enqueued.Count == 1, TimeSpan.FromSeconds(2));

        var status = await actor.Ask<HistorianSinkStatus>(
            HistorianAdapterActor.GetStatus.Instance, TimeSpan.FromSeconds(2));

        status.QueueDepth.ShouldBe(1);
        status.DrainState.ShouldBe(HistorianDrainState.Idle);
    }

    private sealed class RecordingSink : IAlarmHistorianSink
    {
        /// <summary>Gets the list of enqueued alarm historian events.</summary>
        public ConcurrentBag<AlarmHistorianEvent> Enqueued { get; } = [];

        /// <summary>Enqueues an alarm historian event.</summary>
        /// <param name="evt">The event to enqueue.</param>
        /// <param name="cancellationToken">The cancellation token.</param>
        public Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken)
        {
            Enqueued.Add(evt);
            return Task.CompletedTask;
        }

        /// <summary>Gets the current status of the historian sink.</summary>
        public HistorianSinkStatus GetStatus() => new(
            QueueDepth: Enqueued.Count,
            DeadLetterDepth: 0,
            LastDrainUtc: null,
            LastSuccessUtc: null,
            LastError: null,
            DrainState: HistorianDrainState.Idle);
    }
}
