using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.ScriptExecution;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using IAuditWriter = ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.IAuditWriter;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;

/// <summary>
/// Audit Log #23 (M5.4 — ParentExecutionId tag-cascade): nested
/// <c>CallScript</c> / <c>CallShared</c> invocations and alarm on-trigger runs
/// must form a true execution tree, where each spawned run records its
/// immediate spawner's <c>ExecutionId</c> as its <c>ParentExecutionId</c>.
///
/// <list type="bullet">
/// <item><description>
/// A nested <c>CallScript</c> (actor-routed) emits a
/// <see cref="ScriptCallRequest"/> whose <c>ParentExecutionId</c> is the
/// CALLING run's OWN <c>ExecutionId</c> — NOT the inherited grandparent — so
/// <c>A → CallScript(B)</c> yields <c>B.Parent == A.ExecutionId</c>.
/// </description></item>
/// <item><description>
/// A nested <c>CallShared</c> (inline) runs in a child context that mints a
/// fresh <c>ExecutionId</c> and records the caller's <c>ExecutionId</c> as its
/// parent — so <c>B → CallShared(C)</c> yields <c>C.Parent == B.ExecutionId</c>
/// (and NOT B's inherited parent A), proving a multi-level tree.
/// </description></item>
/// <item><description>
/// The alarm on-trigger plumbing carries a <c>parentExecutionId</c> into the
/// script context, and the alarm run is itself a proper execution node whose
/// own <c>ExecutionId</c> cascades onward. Which firing execution the alarm run
/// is parented TO is covered separately by
/// <c>Actors.AlarmCascadeParentExecutionTests</c>.
/// </description></item>
/// </list>
/// </summary>
public class ParentExecutionTreeTests : TestKit
{
    private const string InstanceName = "Plant.Pump42";

    /// <summary>
    /// In-memory <see cref="IAuditWriter"/> capturing every emitted event
    /// (mirrors <c>ExecutionCorrelationContextTests.CapturingAuditWriter</c>).
    /// </summary>
    private sealed class CapturingAuditWriter : IAuditWriter
    {
        public List<AuditRowProjection.AuditRowValues> Events { get; } = new();

        public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
        {
            Events.Add(evt.AsRow());
            return Task.CompletedTask;
        }
    }

    private static SharedScriptLibrary NewLibrary()
    {
        var compilationService = new ScriptCompilationService(
            NullLogger<ScriptCompilationService>.Instance);
        return new SharedScriptLibrary(
            compilationService, NullLogger<SharedScriptLibrary>.Instance);
    }

    /// <summary>
    /// Builds a context whose <c>CallScript</c> Ask targets <paramref name="instanceActor"/>
    /// (a probe), so the forwarded <see cref="ScriptCallRequest"/> can be captured.
    /// </summary>
    private static ScriptRuntimeContext CreateContext(
        IActorRef instanceActor,
        SharedScriptLibrary library,
        IExternalSystemClient? externalSystemClient = null,
        IAuditWriter? auditWriter = null,
        Guid? executionId = null,
        Guid? parentExecutionId = null)
    {
        return new ScriptRuntimeContext(
            instanceActor,
            library,
            currentCallDepth: 0,
            maxCallDepth: 10,
            askTimeout: TimeSpan.FromSeconds(5),
            instanceName: InstanceName,
            logger: NullLogger.Instance,
            externalSystemClient: externalSystemClient,
            siteId: "site-77",
            sourceScript: "ScriptActor:A",
            auditWriter: auditWriter,
            executionId: executionId,
            parentExecutionId: parentExecutionId);
    }

    // -------------------------------------------------------------------------
    // Nested CallScript (actor-routed) — A → CallScript(B)
    // -------------------------------------------------------------------------

    [Fact]
    public async Task CallScript_StampsCallingRunsOwnExecutionId_AsChildParent()
    {
        // A → CallScript(B): the child request's ParentExecutionId must be A's
        // OWN ExecutionId, forming the A→B tree edge.
        var probe = CreateTestProbe();
        var aExecutionId = Guid.NewGuid();
        var context = CreateContext(probe.Ref, NewLibrary(), executionId: aExecutionId);

        var call = context.CallScript("B");

        var request = probe.ExpectMsg<ScriptCallRequest>(TimeSpan.FromSeconds(5));
        Assert.Equal("B", request.ScriptName);
        // B's parent is A's own execution id — the A→B tree edge.
        Assert.Equal(aExecutionId, request.ParentExecutionId);

        // Unblock the Ask so the test completes cleanly.
        probe.Reply(new ScriptCallResult(request.CorrelationId, true, null, null));
        await call;
    }

    [Fact]
    public async Task CallScript_FromRoutedRun_UsesOwnExecutionId_NotInheritedParent()
    {
        // A 2-level tree edge: B was itself spawned (it carries a parent = A).
        // When B does CallScript(C), C.Parent must be B's OWN ExecutionId — NOT
        // the inherited A. This is the regression that distinguishes a true tree
        // from a flattened "everything under the original spawner" model.
        var probe = CreateTestProbe();
        var bExecutionId = Guid.NewGuid();
        var aExecutionId = Guid.NewGuid(); // B's inherited parent
        var context = CreateContext(
            probe.Ref, NewLibrary(),
            executionId: bExecutionId,
            parentExecutionId: aExecutionId);

        var call = context.CallScript("C");

        var request = probe.ExpectMsg<ScriptCallRequest>(TimeSpan.FromSeconds(5));
        Assert.Equal(bExecutionId, request.ParentExecutionId);
        Assert.NotEqual(aExecutionId, request.ParentExecutionId);

        probe.Reply(new ScriptCallResult(request.CorrelationId, true, null, null));
        await call;
    }

    // -------------------------------------------------------------------------
    // Nested CallShared (inline) — B → CallShared(C)
    // -------------------------------------------------------------------------

    [Fact]
    public async Task CallShared_ChildRun_ParentIsCallersExecutionId_FreshOwnExecutionId()
    {
        // B → CallShared(C): the shared script C runs inline but is modelled as
        // its OWN execution node — a fresh ExecutionId parented to B's
        // ExecutionId. Asserted via the audit row C emits through
        // Instance.ExternalSystem.Call.
        var client = new Mock<IExternalSystemClient>();
        client
            .Setup(c => c.CallAsync("ERP", "GetOrder", It.IsAny<IReadOnlyDictionary<string, object?>?>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ExternalCallResult(true, "{}", null));
        var writer = new CapturingAuditWriter();

        var library = NewLibrary();
        Assert.True(library.CompileAndRegister(
            "C", "await Instance.ExternalSystem.Call(\"ERP\", \"GetOrder\"); return null;"));

        var bExecutionId = Guid.NewGuid();
        var context = CreateContext(
            ActorRefs.Nobody, library,
            externalSystemClient: client.Object,
            auditWriter: writer,
            executionId: bExecutionId);

        await context.Scripts.CallShared("C");

        var evt = Assert.Single(writer.Events);
        // C's parent is B's execution id — the B→C tree edge.
        Assert.Equal(bExecutionId, evt.ParentExecutionId);
        // C minted its OWN fresh, non-empty execution id, distinct from B.
        Assert.NotNull(evt.ExecutionId);
        Assert.NotEqual(Guid.Empty, evt.ExecutionId!.Value);
        Assert.NotEqual(bExecutionId, evt.ExecutionId!.Value);
    }

    [Fact]
    public async Task CallShared_FromRoutedRun_ChildParentIsCaller_NotInheritedGrandparent()
    {
        // Regression / multi-level: B itself carries a parent A. When B does
        // CallShared(C), C.Parent must be B's OWN ExecutionId — NOT A. This is
        // the A→B→C chain proving each level points at its immediate spawner.
        var client = new Mock<IExternalSystemClient>();
        client
            .Setup(c => c.CallAsync("ERP", "GetOrder", It.IsAny<IReadOnlyDictionary<string, object?>?>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ExternalCallResult(true, "{}", null));
        var writer = new CapturingAuditWriter();

        var library = NewLibrary();
        Assert.True(library.CompileAndRegister(
            "C", "await Instance.ExternalSystem.Call(\"ERP\", \"GetOrder\"); return null;"));

        var bExecutionId = Guid.NewGuid();
        var aExecutionId = Guid.NewGuid(); // B's inherited parent
        var context = CreateContext(
            ActorRefs.Nobody, library,
            externalSystemClient: client.Object,
            auditWriter: writer,
            executionId: bExecutionId,
            parentExecutionId: aExecutionId);

        await context.Scripts.CallShared("C");

        var evt = Assert.Single(writer.Events);
        Assert.Equal(bExecutionId, evt.ParentExecutionId);
        Assert.NotEqual(aExecutionId, evt.ParentExecutionId);
    }

    // -------------------------------------------------------------------------
    // Alarm on-trigger plumbing
    // -------------------------------------------------------------------------

    [Fact]
    public void CreateChildContextForSharedScript_ParentIsCallerExecution_FreshOwnId()
    {
        // Unit-level proof of the child-context contract the CallShared path uses.
        var bExecutionId = Guid.NewGuid();
        var context = CreateContext(
            ActorRefs.Nobody, NewLibrary(), executionId: bExecutionId);

        var child = context.CreateChildContextForSharedScript(childCallDepth: 1);

        Assert.Equal(bExecutionId, child.ParentExecutionId);
        Assert.NotEqual(Guid.Empty, child.ExecutionId);
        Assert.NotEqual(bExecutionId, child.ExecutionId);
    }

    [Fact]
    public void AlarmOnTrigger_NestedCallScript_CarriesAlarmRunsOwnExecutionId_AsParent()
    {
        // End-to-end alarm plumbing: when an alarm fires, its on-trigger script
        // runs in a ScriptRuntimeContext built by AlarmExecutionActor. The change
        // below carries no SourceExecutionId (it stands in for DCL data), so the
        // alarm run is a ROOT — but it still mints its OWN fresh ExecutionId. A
        // nested CallScript from that on-trigger script must therefore carry the
        // alarm run's OWN (non-null) ExecutionId as the child's ParentExecutionId —
        // proving the alarm context is a proper execution node feeding the
        // cascade and the parentExecutionId parameter is plumbed end-to-end.
        var compilationService = new ScriptCompilationService(
            NullLogger<ScriptCompilationService>.Instance);
        var sharedLibrary = new SharedScriptLibrary(
            compilationService, NullLogger<SharedScriptLibrary>.Instance);
        var options = new SiteRuntimeOptions();

        var onTrigger = compilationService.Compile(
            "OnTrigger", "await Instance.CallScript(\"Child\"); return null;");
        Assert.NotNull(onTrigger.CompiledScript);

        var alarmConfig = new ResolvedAlarm
        {
            CanonicalName = "HighTemp",
            TriggerType = "ValueMatch",
            TriggerConfiguration = "{\"attributeName\":\"Status\",\"matchValue\":\"Critical\"}",
            PriorityLevel = 1
        };

        var instanceProbe = CreateTestProbe();
        var alarm = ActorOf(Props.Create(() => new AlarmActor(
            "HighTemp", "Pump1", instanceProbe.Ref, alarmConfig,
            onTrigger.CompiledScript, sharedLibrary, options,
            NullLogger<AlarmActor>.Instance)));

        alarm.Tell(new AttributeValueChanged(
            "Pump1", "Status", "Status", "Critical", "Good", DateTimeOffset.UtcNow));

        // The alarm raises (instance gets AlarmStateChanged) AND the on-trigger
        // script fires its nested CallScript at the instance.
        instanceProbe.ExpectMsg<AlarmStateChanged>(TimeSpan.FromSeconds(5));
        var request = instanceProbe.ExpectMsg<ScriptCallRequest>(TimeSpan.FromSeconds(5));

        Assert.Equal("Child", request.ScriptName);
        // This alarm run is a root (no writer on the firing change), but its OWN
        // freshly-minted ExecutionId cascades to the child — so the child's
        // ParentExecutionId is a real, non-empty value, NOT null.
        Assert.NotNull(request.ParentExecutionId);
        Assert.NotEqual(Guid.Empty, request.ParentExecutionId!.Value);

        instanceProbe.Reply(new ScriptCallResult(request.CorrelationId, true, null, null));
    }
}
