using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Logging.Abstractions;
using ScadaLink.Commons.Messages.ScriptExecution;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.Commons.Types.Scripts;
using ScadaLink.SiteRuntime.Actors;
using ScadaLink.SiteRuntime.Scripts;

namespace ScadaLink.SiteRuntime.Tests.Actors;

/// <summary>
/// Regression coverage for SiteRuntime-016 — the short-lived execution actors
/// (<see cref="ScriptExecutionActor"/>, <see cref="AlarmExecutionActor"/>) were
/// previously untested. Covers success, exception, timeout, Ask-reply, and the
/// PoisonPill self-stop after completion.
/// </summary>
public class ExecutionActorTests : TestKit, IDisposable
{
    private readonly SharedScriptLibrary _sharedLibrary;
    private readonly ScriptCompilationService _compilationService;

    public ExecutionActorTests()
    {
        _compilationService = new ScriptCompilationService(
            NullLogger<ScriptCompilationService>.Instance);
        _sharedLibrary = new SharedScriptLibrary(
            _compilationService, NullLogger<SharedScriptLibrary>.Instance);
    }

    void IDisposable.Dispose() => Shutdown();

    private static Script<object?> CompileScript(string code)
    {
        var scriptOptions = ScriptOptions.Default
            .WithReferences(typeof(object).Assembly, typeof(Enumerable).Assembly)
            .WithImports("System", "System.Collections.Generic", "System.Linq", "System.Threading.Tasks");
        var script = CSharpScript.Create<object?>(code, scriptOptions, typeof(ScriptGlobals));
        script.Compile();
        return script;
    }

    private static SiteRuntimeOptions Options(int timeoutSeconds = 30)
        => new() { MaxScriptCallDepth = 10, ScriptExecutionTimeoutSeconds = timeoutSeconds };

    // ── ScriptExecutionActor ──

    [Fact]
    public void ScriptExecutionActor_Success_RepliesWithResultAndStops()
    {
        var compiled = CompileScript("return 7 * 6;");
        var replyTo = CreateTestProbe();
        var instanceActor = CreateTestProbe();

        var exec = ActorOf(Props.Create(() => new ScriptExecutionActor(
            "Answer", "Inst1", compiled, null, 0,
            instanceActor.Ref, _sharedLibrary, Options(),
            replyTo.Ref, "corr-1", NullLogger.Instance,
            ScriptScope.Root, null, null)));

        Watch(exec);

        var result = replyTo.ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
        Assert.True(result.Success);
        Assert.Equal("corr-1", result.CorrelationId);
        Assert.Equal(42, result.ReturnValue);

        // The actor must PoisonPill itself once execution completes.
        ExpectTerminated(exec, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public void ScriptExecutionActor_ScriptThrows_RepliesFailureAndStops()
    {
        var compiled = CompileScript("throw new InvalidOperationException(\"boom\");");
        var replyTo = CreateTestProbe();
        var instanceActor = CreateTestProbe();

        var exec = ActorOf(Props.Create(() => new ScriptExecutionActor(
            "Bad", "Inst1", compiled, null, 0,
            instanceActor.Ref, _sharedLibrary, Options(),
            replyTo.Ref, "corr-2", NullLogger.Instance,
            ScriptScope.Root, null, null)));

        Watch(exec);

        var result = replyTo.ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
        Assert.False(result.Success);
        Assert.Equal("corr-2", result.CorrelationId);
        Assert.Contains("boom", result.ErrorMessage);

        ExpectTerminated(exec, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public void ScriptExecutionActor_Timeout_RepliesFailureAndStops()
    {
        // A long busy loop that observes the cancellation token so the
        // 1-second timeout fires cooperatively.
        var compiled = CompileScript(
            "while (true) { await System.Threading.Tasks.Task.Delay(50, CancellationToken); }");
        var replyTo = CreateTestProbe();
        var instanceActor = CreateTestProbe();

        var exec = ActorOf(Props.Create(() => new ScriptExecutionActor(
            "Slow", "Inst1", compiled, null, 0,
            instanceActor.Ref, _sharedLibrary, Options(timeoutSeconds: 1),
            replyTo.Ref, "corr-3", NullLogger.Instance,
            ScriptScope.Root, null, null)));

        Watch(exec);

        var result = replyTo.ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
        Assert.False(result.Success);
        Assert.Contains("timed out", result.ErrorMessage);

        ExpectTerminated(exec, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public void ScriptExecutionActor_NoReplyTo_StillStopsAfterCompletion()
    {
        var compiled = CompileScript("return 1;");
        var instanceActor = CreateTestProbe();

        // ActorRefs.Nobody as replyTo — fire-and-forget execution.
        var exec = ActorOf(Props.Create(() => new ScriptExecutionActor(
            "FireForget", "Inst1", compiled, null, 0,
            instanceActor.Ref, _sharedLibrary, Options(),
            ActorRefs.Nobody, "corr-4", NullLogger.Instance,
            ScriptScope.Root, null, null)));

        Watch(exec);
        ExpectTerminated(exec, TimeSpan.FromSeconds(5));
    }

    // ── AlarmExecutionActor ──

    [Fact]
    public void AlarmExecutionActor_Success_StopsAfterCompletion()
    {
        var compiled = CompileScript("return 0;");
        var instanceActor = CreateTestProbe();

        var exec = ActorOf(Props.Create(() => new AlarmExecutionActor(
            "HiTemp", "Inst1", AlarmLevel.High, 5, "High temperature",
            compiled, instanceActor.Ref, _sharedLibrary, Options(),
            NullLogger.Instance)));

        Watch(exec);
        ExpectTerminated(exec, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public void AlarmExecutionActor_ScriptThrows_StillStops()
    {
        var compiled = CompileScript("throw new System.Exception(\"alarm-boom\");");
        var instanceActor = CreateTestProbe();

        var exec = ActorOf(Props.Create(() => new AlarmExecutionActor(
            "HiTemp", "Inst1", AlarmLevel.High, 5, "High temperature",
            compiled, instanceActor.Ref, _sharedLibrary, Options(),
            NullLogger.Instance)));

        Watch(exec);
        // Even on a throwing on-trigger body, the actor must self-stop.
        ExpectTerminated(exec, TimeSpan.FromSeconds(5));
    }
}
