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.Messages.Streaming;
using ScadaLink.Commons.Types.Flattening;
using ScadaLink.SiteRuntime.Actors;
using ScadaLink.SiteRuntime.Scripts;

namespace ScadaLink.SiteRuntime.Tests.Actors;

/// <summary>
/// WP-15: Script Actor and Script Execution Actor tests.
/// WP-20: Recursion limit tests.
/// WP-22: Tell vs Ask convention tests.
/// WP-32: Script error handling tests.
/// </summary>
public class ScriptActorTests : TestKit, IDisposable
{
    private readonly SharedScriptLibrary _sharedLibrary;
    private readonly SiteRuntimeOptions _options;
    private readonly ScriptCompilationService _compilationService;

    public ScriptActorTests()
    {
        _compilationService = new ScriptCompilationService(
            NullLogger<ScriptCompilationService>.Instance);
        _sharedLibrary = new SharedScriptLibrary(
            _compilationService, NullLogger<SharedScriptLibrary>.Instance);
        _options = new SiteRuntimeOptions
        {
            MaxScriptCallDepth = 10,
            ScriptExecutionTimeoutSeconds = 30
        };
    }

    void IDisposable.Dispose()
    {
        Shutdown();
    }

    private 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;
    }

    [Fact]
    public void ScriptActor_CallScript_ReturnsResult()
    {
        var compiled = CompileScript("42");
        var scriptConfig = new ResolvedScript
        {
            CanonicalName = "GetAnswer",
            Code = "42"
        };

        var instanceActor = CreateTestProbe();
        var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
            "GetAnswer",
            "TestInstance",
            instanceActor.Ref,
            compiled,
            scriptConfig,
            _sharedLibrary,
            _options,
            NullLogger<ScriptActor>.Instance)));

        // Ask pattern (WP-22) for CallScript
        scriptActor.Tell(new ScriptCallRequest("GetAnswer", null, 0, "corr-1"));

        var result = ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
        Assert.True(result.Success, $"Script call failed: {result.ErrorMessage}");
        Assert.Equal(42, result.ReturnValue);
    }

    [Fact]
    public void ScriptActor_CallScript_WithParameters_Works()
    {
        var compiled = CompileScript("(int)Parameters[\"x\"] + (int)Parameters[\"y\"]");
        var scriptConfig = new ResolvedScript
        {
            CanonicalName = "Add",
            Code = "(int)Parameters[\"x\"] + (int)Parameters[\"y\"]"
        };

        var instanceActor = CreateTestProbe();
        var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
            "Add",
            "TestInstance",
            instanceActor.Ref,
            compiled,
            scriptConfig,
            _sharedLibrary,
            _options,
            NullLogger<ScriptActor>.Instance)));

        var parameters = new Dictionary<string, object?> { ["x"] = 3, ["y"] = 4 };
        scriptActor.Tell(new ScriptCallRequest("Add", parameters, 0, "corr-2"));

        var result = ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
        Assert.True(result.Success);
        Assert.Equal(7, result.ReturnValue);
    }

    [Fact]
    public void ScriptActor_NullCompiledScript_ReturnsError()
    {
        var scriptConfig = new ResolvedScript
        {
            CanonicalName = "Broken",
            Code = ""
        };

        var instanceActor = CreateTestProbe();
        var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
            "Broken",
            "TestInstance",
            instanceActor.Ref,
            null, // no compiled script
            scriptConfig,
            _sharedLibrary,
            _options,
            NullLogger<ScriptActor>.Instance)));

        scriptActor.Tell(new ScriptCallRequest("Broken", null, 0, "corr-3"));

        var result = ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(5));
        Assert.False(result.Success);
        Assert.Contains("not compiled", result.ErrorMessage);
    }

    [Fact]
    public void ScriptActor_ValueChangeTrigger_SpawnsExecution()
    {
        var compiled = CompileScript("\"triggered\"");
        var scriptConfig = new ResolvedScript
        {
            CanonicalName = "OnChange",
            Code = "\"triggered\"",
            TriggerType = "ValueChange",
            TriggerConfiguration = "{\"attributeName\":\"Temperature\"}"
        };

        var instanceActor = CreateTestProbe();
        var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
            "OnChange",
            "TestInstance",
            instanceActor.Ref,
            compiled,
            scriptConfig,
            _sharedLibrary,
            _options,
            NullLogger<ScriptActor>.Instance)));

        // Send an attribute change that matches the trigger
        scriptActor.Tell(new AttributeValueChanged(
            "TestInstance", "Temperature", "Temperature", "100.0", "Good", DateTimeOffset.UtcNow));

        // The script should execute (we can't easily verify the output since it's fire-and-forget)
        // But we can verify the actor doesn't crash
        ExpectNoMsg(TimeSpan.FromSeconds(1));
    }

    [Fact]
    public void ScriptActor_MinTimeBetweenRuns_SkipsIfTooSoon()
    {
        var compiled = CompileScript("\"ok\"");
        var scriptConfig = new ResolvedScript
        {
            CanonicalName = "Throttled",
            Code = "\"ok\"",
            TriggerType = "ValueChange",
            TriggerConfiguration = "{\"attributeName\":\"Temp\"}",
            MinTimeBetweenRuns = TimeSpan.FromMinutes(10) // long minimum
        };

        var instanceActor = CreateTestProbe();
        var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
            "Throttled",
            "TestInstance",
            instanceActor.Ref,
            compiled,
            scriptConfig,
            _sharedLibrary,
            _options,
            NullLogger<ScriptActor>.Instance)));

        // First trigger -- should execute
        scriptActor.Tell(new AttributeValueChanged(
            "TestInstance", "Temp", "Temp", "1", "Good", DateTimeOffset.UtcNow));

        // Second trigger immediately -- should be skipped due to min time
        scriptActor.Tell(new AttributeValueChanged(
            "TestInstance", "Temp", "Temp", "2", "Good", DateTimeOffset.UtcNow));

        // No crash expected
        ExpectNoMsg(TimeSpan.FromSeconds(1));
    }

    [Fact]
    public void ScriptActor_WP32_ScriptFailure_DoesNotDisable()
    {
        // Script that throws an exception
        var compiled = CompileScript("throw new System.Exception(\"boom\")");
        var scriptConfig = new ResolvedScript
        {
            CanonicalName = "Failing",
            Code = "throw new System.Exception(\"boom\")"
        };

        var instanceActor = CreateTestProbe();
        var scriptActor = ActorOf(Props.Create(() => new ScriptActor(
            "Failing",
            "TestInstance",
            instanceActor.Ref,
            compiled,
            scriptConfig,
            _sharedLibrary,
            _options,
            NullLogger<ScriptActor>.Instance)));

        // First call -- fails
        scriptActor.Tell(new ScriptCallRequest("Failing", null, 0, "corr-fail-1"));
        var result1 = ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
        Assert.False(result1.Success);

        // Second call -- should still work (script not disabled after failure)
        scriptActor.Tell(new ScriptCallRequest("Failing", null, 0, "corr-fail-2"));
        var result2 = ExpectMsg<ScriptCallResult>(TimeSpan.FromSeconds(10));
        Assert.False(result2.Success); // Still fails, but the actor is still alive
    }
}
