using Akka.Actor;
using Akka.TestKit.Xunit2;

namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Tests;

/// <summary>
/// Pins the Akka behaviour that <see cref="SecretReplicationActor"/> depends on: on a thread with
/// no actor context, <c>Self</c> and <c>Context</c> cannot be read.
/// </summary>
/// <remarks>
/// <para>
/// This is here because assuming otherwise produced a real bug. <c>Self</c> resolves through
/// <c>Context</c>, which is <c>[ThreadStatic]</c>; a continuation that resumes on a thread without
/// actor context throws <see cref="NotSupportedException"/> when reading either.
/// </para>
/// <para>
/// What made it dangerous is that it does <em>not</em> reliably throw. When the awaited operation
/// completes synchronously — which a local SQLite store usually does — the continuation stays on the
/// actor's mailbox thread and the context is still intact, so the illegal read succeeds. Correctness
/// then depends on store latency and thread-pool timing. The rule the production code follows is
/// therefore unconditional: capture <c>Self</c> (and <c>Sender</c>) into locals while still on the
/// actor thread, and never touch actor context after an <c>await</c>.
/// </para>
/// <para>
/// The illegal reads run on a <b>dedicated new thread</b>, not directly in the pool continuation.
/// An earlier revision read from the <c>ConfigureAwait(false)</c> continuation itself and flaked
/// (observed 2026-07-18 under parallel suite load): shared thread-pool threads are exactly where
/// Akka and the TestKit legitimately install the ThreadStatic cell during mailbox runs and async
/// continuations, so "no context here" is a timing accident, not a guarantee — worse, a cell whose
/// actor was already cleared makes <c>Context</c> return <c>null</c> and <c>Self</c> return the
/// cleared ref <em>without throwing</em>. The very unreliability this test documents made its old
/// formulation unsound. A fresh thread is the one place the no-context state is guaranteed.
/// </para>
/// </remarks>
public sealed class ActorContextAfterAwaitTests : TestKit
{
    private sealed record Probe;

    private sealed record Outcome(
        string? SelfError, string? ContextError, string? CapturedSelfPath,
        string? StaleSelfPath, string? StaleContextPath);

    private sealed class ProbeActor : ReceiveActor
    {
        public ProbeActor()
        {
            ReceiveAsync<Probe>(async probe =>
            {
                IActorRef replyTo = Sender;

                // The correct pattern: capture while the context is still current.
                IActorRef capturedSelf = Self;

                // The realistic escape: after this, the handler is off the mailbox thread.
                await Task.Delay(50).ConfigureAwait(false);

                string? selfError = null;
                string? contextError = null;
                string? staleSelfPath = null;
                string? staleContextPath = null;

                // LongRunning = a brand-new thread, where no actor cell can ever have been
                // installed. Reading Self/Context THERE is the deterministic form of the hazard.
                await Task.Factory.StartNew(
                    () =>
                    {
                        try
                        {
                            // If this ever succeeds the failure message needs to say whose
                            // context the thread was carrying — that is the whole diagnosis.
                            staleSelfPath = Self.Path.ToString();
                        }
                        catch (Exception ex)
                        {
                            selfError = ex.GetType().Name;
                        }

                        try
                        {
                            staleContextPath = Context.Self.Path.ToString();
                        }
                        catch (Exception ex)
                        {
                            contextError = ex.GetType().Name;
                        }
                    },
                    CancellationToken.None,
                    TaskCreationOptions.LongRunning,
                    TaskScheduler.Default).ConfigureAwait(false);

                replyTo.Tell(new Outcome(
                    selfError, contextError, capturedSelf.Path.ToString(),
                    staleSelfPath, staleContextPath));
            });
        }
    }

    [Fact]
    public void Self_and_Context_are_both_unreadable_on_a_thread_without_actor_context()
    {
        IActorRef probe = Sys.ActorOf(Props.Create(() => new ProbeActor()), "context-probe");
        probe.Tell(new Probe());

        Outcome outcome = ExpectMsg<Outcome>(TimeSpan.FromSeconds(10));

        // Both throw — Self is not a cached field, it goes through the ThreadStatic context.
        Assert.True(
            outcome.SelfError == "NotSupportedException",
            $"Self read did not throw NotSupportedException (got: {outcome.SelfError ?? "no exception"}; " +
            $"stale Self path: '{outcome.StaleSelfPath}').");
        Assert.True(
            outcome.ContextError == "NotSupportedException",
            $"Context read did not throw NotSupportedException (got: {outcome.ContextError ?? "no exception"}; " +
            $"stale Context path: '{outcome.StaleContextPath}').");

        // ...but a reference captured beforehand stays valid, which is what the actor relies on.
        Assert.EndsWith("/user/context-probe", outcome.CapturedSelfPath, StringComparison.Ordinal);
    }
}
