using Akka.Actor;
using Akka.Event;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;

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

/// <summary>
///     Covers two robustness nits on the generic <see cref="DriverInstanceActor"/>:
///     (1) writes that arrive while the actor is NOT in <c>Connected</c> (Stubbed / Connecting /
///     Reconnecting) must fast-fail with a negative <see cref="DriverInstanceActor.WriteAttributeResult"/>
///     rather than dead-letter and let the host Ask wait its full 8s timeout; and
///     (2) the self-Tell <c>Subscribe</c> issued by <c>ResubscribeDesired</c> on (re)connect must not
///     leave a stray <see cref="DriverInstanceActor.SubscriptionEstablished"/> dead-lettering when its
///     reply lands back at Self.
/// </summary>
public sealed class DriverInstanceActorWriteAndSubscribeTests : RuntimeActorTestBase
{
    /// <summary>
    ///     A write to a Stubbed actor fast-fails synchronously: a Stubbed driver never connects, yet
    ///     it must answer a <see cref="DriverInstanceActor.WriteAttribute"/> Ask with a negative result
    ///     well inside the host's 8s timeout instead of dead-lettering.
    /// </summary>
    [Fact]
    public async Task WriteAttribute_to_stubbed_driver_fast_fails()
    {
        var driver = new WritableStubDriver();
        var actor = Sys.ActorOf(DriverInstanceActor.Props(driver, startStubbed: true));

        var reply = await actor.Ask<DriverInstanceActor.WriteAttributeResult>(
            new DriverInstanceActor.WriteAttribute("tag-1", 42),
            TimeSpan.FromSeconds(2));

        // Stubbed drivers deterministically succeed writes without touching hardware (existing behaviour).
        reply.Success.ShouldBeTrue();
    }

    /// <summary>
    ///     A write to a Connecting actor (InitializeAsync still in flight, never resolves) fast-fails
    ///     with <c>Success=false</c> and a non-null reason in well under the 8s host Ask timeout —
    ///     proving the synchronous fast-fail in <c>Connecting()</c> fires instead of dead-lettering.
    /// </summary>
    [Fact]
    public async Task WriteAttribute_to_connecting_driver_fast_fails()
    {
        // BlockingInitDriver.InitializeAsync never completes, so the actor stays in Connecting.
        var driver = new BlockingInitDriver();
        var actor = Sys.ActorOf(DriverInstanceActor.Props(driver));

        actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
        AwaitCondition(() => driver.InitializeStarted, TimeSpan.FromSeconds(2));

        var sw = System.Diagnostics.Stopwatch.StartNew();
        var reply = await actor.Ask<DriverInstanceActor.WriteAttributeResult>(
            new DriverInstanceActor.WriteAttribute("tag-1", 42),
            TimeSpan.FromSeconds(2));
        sw.Stop();

        reply.Success.ShouldBeFalse();
        reply.Reason.ShouldNotBeNull();
        sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(2));
    }

    /// <summary>
    ///     A write to a Reconnecting actor (initial connect failed, retrying) fast-fails with a negative
    ///     result rather than dead-lettering and hanging the host Ask.
    /// </summary>
    [Fact]
    public async Task WriteAttribute_to_reconnecting_driver_fast_fails()
    {
        // InitializeShouldThrow drives Connecting → Reconnecting; the slow reconnect interval keeps it
        // parked there long enough to take a write.
        var driver = new WritableStubDriver { InitializeShouldThrow = true };
        var actor = Sys.ActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromSeconds(30)));

        actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
        // The first Initialize attempt throws and pushes the actor into Reconnecting.
        AwaitCondition(() => driver.InitializeCount >= 1, TimeSpan.FromSeconds(2));

        var reply = await actor.Ask<DriverInstanceActor.WriteAttributeResult>(
            new DriverInstanceActor.WriteAttribute("tag-1", 42),
            TimeSpan.FromSeconds(2));

        reply.Success.ShouldBeFalse();
        reply.Reason.ShouldNotBeNull();
    }

    /// <summary>
    ///     Driving a connect + auto-resubscribe cycle (the self-Tell <c>Subscribe</c> from
    ///     <c>ResubscribeDesired</c> whose <c>SubscriptionEstablished</c> reply lands at Self) must not
    ///     produce a <see cref="DriverInstanceActor.SubscriptionEstablished"/> dead-letter.
    /// </summary>
    [Fact]
    public async Task Self_resubscribe_does_not_deadletter_SubscriptionEstablished()
    {
        // The probe IGNORES every DeadLetter except those carrying a SubscriptionEstablished payload,
        // so its mailbox only ever holds the message we care about. That makes the assertion precise:
        // any swallow miss surfaces as a SubscriptionEstablished DeadLetter, and nothing else
        // (health-poll cruft, remoting-terminator letters) can give a false positive.
        var deadLetters = CreateTestProbe();
        deadLetters.IgnoreMessages(m => m is not AllDeadLetters { Message: DriverInstanceActor.SubscriptionEstablished });
        Sys.EventStream.Subscribe(deadLetters.Ref, typeof(AllDeadLetters));

        var driver = new SubscribableStubDriver();
        var actor = Sys.ActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));

        // Desired set arrives first; on connect, ResubscribeDesired self-Tells Subscribe, whose
        // SubscriptionEstablished reply lands at Self.
        actor.Tell(new DriverInstanceActor.SetDesiredSubscriptions(
            new[] { "tag-a", "tag-b" }, TimeSpan.FromMilliseconds(100)));
        actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
        AwaitCondition(() => driver.SubscribeCount >= 1, TimeSpan.FromSeconds(2));

        // Force a reconnect to exercise the resubscribe path a second time.
        actor.Tell(new DriverInstanceActor.DisconnectObserved("backend blip"));
        AwaitCondition(() => driver.SubscribeCount >= 2, TimeSpan.FromSeconds(3));

        // BARRIER: the self-resubscribe reply is published asynchronously, so SubscribeCount>=2 alone
        // doesn't guarantee the actor has yet PROCESSED the self-sent SubscriptionEstablished (the point
        // where it either swallows it or — pre-fix — dead-letters it). Drive a real Subscribe Ask from
        // the TEST and await its reply: Akka processes the mailbox in order, so once this round-trips,
        // every earlier self-resubscribe reply has already been handled and any dead-letter has already
        // been published to the EventStream (Unhandled publishes synchronously). Only THEN is ExpectNoMsg
        // race-free.
        await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
            new DriverInstanceActor.Subscribe(new[] { "tag-a", "tag-b" }, TimeSpan.FromMilliseconds(100)),
            TimeSpan.FromSeconds(3));

        deadLetters.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
    }

    /// <summary>
    ///     When <c>HandleSubscribeAsync</c> throws (e.g. the driver's <c>SubscribeAsync</c> faults),
    ///     it replies <see cref="DriverInstanceActor.SubscriptionFailed"/> to the sender.  On the
    ///     self-resubscribe path (<c>ResubscribeDesired</c> self-Tells <c>Subscribe</c>) the sender is
    ///     Self, so the failure reply lands back at the actor. Without a handler, it dead-letters.
    ///     This test verifies the symmetric swallow (added alongside the
    ///     <see cref="DriverInstanceActor.SubscriptionEstablished"/> one) prevents the dead-letter.
    /// </summary>
    [Fact]
    public async Task Self_resubscribe_failure_does_not_deadletter_SubscriptionFailed()
    {
        // Subscribe to AllDeadLetters but ignore everything EXCEPT those carrying a
        // SubscriptionFailed payload — keeps the probe mailbox precise.
        var deadLetterProbe = CreateTestProbe();
        deadLetterProbe.IgnoreMessages(
            m => m is not AllDeadLetters { Message: DriverInstanceActor.SubscriptionFailed });
        Sys.EventStream.Subscribe(deadLetterProbe.Ref, typeof(AllDeadLetters));

        // Bring the actor to Connected state using a subscribable driver.
        var driver = new SubscribableStubDriver();
        var actor = Sys.ActorOf(DriverInstanceActor.Props(driver, reconnectInterval: TimeSpan.FromSeconds(30)));
        actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
        // Wait until Connected (subscribe path available).
        AwaitCondition(() => driver.InitializeCount >= 1, TimeSpan.FromSeconds(2));

        // Tell SubscriptionFailed directly — a faithful stand-in for the self-reply landing on the
        // actor on the self-resubscribe path. This exercises exactly the Receive<SubscriptionFailed>
        // handler (or proves its absence via a dead-letter).
        actor.Tell(new DriverInstanceActor.SubscriptionFailed("boom"));

        // Drive a round-trip to flush the actor mailbox before asserting.
        // Subscribe Ask (which the Connected state can answer) as the barrier.
        await actor.Ask<DriverInstanceActor.SubscriptionEstablished>(
            new DriverInstanceActor.Subscribe(new[] { "barrier-ref" }, TimeSpan.FromMilliseconds(100)),
            TimeSpan.FromSeconds(3));

        deadLetterProbe.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
    }

    /// <summary>
    ///     A standalone <see cref="IDriver"/> (and <see cref="IWritable"/>) whose <c>InitializeAsync</c>
    ///     never completes, parking the actor in <c>Connecting</c>. Implements the interface directly
    ///     (not via <see cref="StubDriver"/>) so the never-completing Init is reached through the
    ///     polymorphic <see cref="IDriver"/> call the actor makes.
    /// </summary>
    private sealed class BlockingInitDriver : IDriver, IWritable
    {
        /// <summary>Set true the moment InitializeAsync is entered.</summary>
        public volatile bool InitializeStarted;
        private readonly TaskCompletionSource _gate = new(TaskCreationOptions.RunContinuationsAsynchronously);

        /// <summary>Gets the driver instance ID.</summary>
        public string DriverInstanceId => "blocking-init-driver";
        /// <summary>Gets the driver type.</summary>
        public string DriverType => "Stub";

        /// <summary>Never completes — keeps the actor in Connecting.</summary>
        public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
        {
            InitializeStarted = true;
            return _gate.Task;
        }

        /// <summary>Reinitializes the driver with the specified configuration JSON.</summary>
        public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) =>
            Task.CompletedTask;

        /// <summary>Shuts down the driver.</summary>
        public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask;
        /// <summary>Gets the health status of the driver.</summary>
        public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
        /// <summary>Gets the memory footprint of the driver.</summary>
        public long GetMemoryFootprint() => 0;
        /// <summary>Flushes optional caches in the driver.</summary>
        public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;

        /// <summary>Writes the specified requests (never reached — actor fast-fails in Connecting).</summary>
        public Task<IReadOnlyList<WriteResult>> WriteAsync(
            IReadOnlyList<WriteRequest> writes, CancellationToken cancellationToken)
        {
            IReadOnlyList<WriteResult> results = writes.Select(_ => new WriteResult(0u)).ToList();
            return Task.FromResult(results);
        }
    }

}
