using Akka.Actor;
using Akka.TestKit;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

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

/// <summary>
/// Phase 1 of the script-scope rollout: verify path arithmetic for the new
/// Attributes / Children / Parent accessors. The actor-mediated reads/writes
/// are exercised end-to-end in Phase 2 once flattening carries scope info.
/// </summary>
public class ScopeAccessorTests
{
    [Fact]
    public void Root_SelfPath_Empty()
    {
        Assert.Equal("", ScriptScope.Root.SelfPath);
        Assert.Null(ScriptScope.Root.ParentPath);
        Assert.False(ScriptScope.Root.HasParent);
    }

    [Fact]
    public void CompositionScope_HasParent()
    {
        var scope = new ScriptScope("TempSensor", "");
        Assert.True(scope.HasParent);
        Assert.Equal("", scope.ParentPath);
    }

    [Fact]
    public void AttributeAccessor_RootScope_ResolvesBareKey()
    {
        var acc = new AttributeAccessor(null!, "");
        Assert.Equal("Temperature", acc.Resolve("Temperature"));
    }

    [Fact]
    public void AttributeAccessor_ComposedScope_PrependsPath()
    {
        var acc = new AttributeAccessor(null!, "TempSensor");
        Assert.Equal("TempSensor.Temperature", acc.Resolve("Temperature"));
    }

    [Fact]
    public void AttributeAccessor_NestedScope_ChainsPath()
    {
        var acc = new AttributeAccessor(null!, "Motor.TempSensor");
        Assert.Equal("Motor.TempSensor.Temperature", acc.Resolve("Temperature"));
    }

    [Fact]
    public void CompositionAccessor_AttributesShareScope()
    {
        var comp = new CompositionAccessor(null!, "TempSensor");
        Assert.Equal("TempSensor", comp.Path);
        Assert.Equal("TempSensor", comp.Attributes.ScopePrefix);
    }

    [Fact]
    public void CompositionAccessor_ResolveScript_PrependsPath()
    {
        var comp = new CompositionAccessor(null!, "TempSensor");
        Assert.Equal("TempSensor.Sample", comp.ResolveScript("Sample"));
    }

    [Fact]
    public void CompositionAccessor_EmptyPath_LeavesScriptNameBare()
    {
        var comp = new CompositionAccessor(null!, "");
        Assert.Equal("Sample", comp.ResolveScript("Sample"));
    }

    [Fact]
    public void ChildrenAccessor_FromRoot_GivesUnpathedChild()
    {
        var children = new ChildrenAccessor(null!, "");
        var temp = children["TempSensor"];
        Assert.Equal("TempSensor", temp.Path);
    }

    [Fact]
    public void ChildrenAccessor_FromComposition_PrefixesChild()
    {
        var children = new ChildrenAccessor(null!, "Motor");
        var temp = children["TempSensor"];
        Assert.Equal("Motor.TempSensor", temp.Path);
    }

    // --- AttributeAccessor encoding contract ----------------------------------
    //
    // AttributeAccessor.this[key].set and SetAsync both route through
    // ScriptRuntimeContext.SetAttribute(name, encodedString), which requires
    // a live Akka IActorRef; ScriptRuntimeContext has no virtual members and
    // its constructor cannot be satisfied without a real ActorSystem, so a
    // full-round-trip unit test through the accessor+context is not viable
    // without a heavy Akka harness.
    //
    // Instead we test the encoding decision directly: AttributeAccessor is now
    // documented to delegate value serialisation to AttributeValueCodec.Encode.
    // These tests verify that contract at the codec level, which is exactly what
    // the fix makes the accessor invoke.

    [Fact]
    public void AttributeValueCodec_Encode_List_ProducesJsonArray()
    {
        // A List<string> must encode to a JSON array, not the garbage
        // "System.Collections.Generic.List`1[System.String]" that .ToString() produced.
        var list = new List<string> { "a", "b" };
        var encoded = AttributeValueCodec.Encode(list);
        Assert.Equal("[\"a\",\"b\"]", encoded);
    }

    [Fact]
    public void AttributeValueCodec_Encode_Scalar_PassesThrough()
    {
        // A plain string scalar must be returned unchanged (byte-identical to
        // the historical value?.ToString() path for strings).
        var encoded = AttributeValueCodec.Encode("x");
        Assert.Equal("x", encoded);
    }

    [Fact]
    public void AttributeValueCodec_Encode_Null_ReturnsNull()
    {
        // AttributeAccessor coalesces null → "" at the call site,
        // but the codec itself must return null for null input.
        Assert.Null(AttributeValueCodec.Encode(null));
    }

    [Fact]
    public void AttributeValueCodec_Encode_IntList_ProducesJsonArray()
    {
        // Integer list elements encode as native-typed JSON numbers (NJ-1):
        // [1,2,3], not the old quoted-element form ["1","2","3"].
        var list = new List<int> { 1, 2, 3 };
        var encoded = AttributeValueCodec.Encode(list);
        Assert.Equal("[1,2,3]", encoded);
    }
}

/// <summary>
/// WaitAsync (spec §3-§5, acceptance §7.6) scope-resolution tests. Unlike the
/// path-arithmetic tests above, these route a real <see cref="ScriptRuntimeContext"/>
/// against a TestProbe standing in for the Instance Actor, so they need a live
/// ActorSystem — hence a TestKit-derived class. They assert that
/// <c>Attributes.WaitAsync</c> applies <see cref="AttributeAccessor.Resolve"/>
/// (the composition prefix) to the key BEFORE the request is sent to the actor —
/// the same contract Get/Set obey.
/// </summary>
public class AttributeAccessorWaitAsyncTests : TestKit, IDisposable
{
    private ScriptRuntimeContext MakeContext(IActorRef instanceActor) =>
        new(
            instanceActor,
            sharedScriptLibrary: null!,
            currentCallDepth: 0,
            maxCallDepth: 10,
            askTimeout: TimeSpan.FromSeconds(2),
            instanceName: "Pump1",
            logger: NullLogger<ScriptRuntimeContext>.Instance);

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

    [Fact]
    public void WaitAsync_Value_AppliesScopeResolution_BeforeSendingRequest()
    {
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        // Composed scope "TempSensor" — Resolve("Flag") => "TempSensor.Flag".
        var acc = new AttributeAccessor(ctx, "TempSensor");

        // Fire-and-forget; the assertion is on the message the actor receives.
        _ = acc.WaitAsync("Flag", true, TimeSpan.FromSeconds(30));

        var req = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
        Assert.Equal("TempSensor.Flag", req.AttributeName);
        // The value overload encodes the target via AttributeValueCodec.Encode and
        // sends a null predicate. bool true encodes to "True" (capital T).
        Assert.Equal(AttributeValueCodec.Encode(true), req.TargetValueEncoded);
        Assert.Equal("True", req.TargetValueEncoded);
        Assert.Null(req.Predicate);
        Assert.Equal("Pump1", req.InstanceName);
    }

    [Fact]
    public void WaitAsync_Predicate_AppliesScopeResolution_AndSendsPredicate()
    {
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        var acc = new AttributeAccessor(ctx, "Motor.TempSensor");

        Func<object?, bool> predicate = _ => true;
        _ = acc.WaitAsync("Level", predicate, TimeSpan.FromSeconds(30));

        var req = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
        Assert.Equal("Motor.TempSensor.Level", req.AttributeName);
        // The predicate overload sends the delegate and a null encoded target.
        Assert.Null(req.TargetValueEncoded);
        Assert.NotNull(req.Predicate);
    }

    [Fact]
    public void WaitAsync_RootScope_LeavesKeyBare()
    {
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        var acc = new AttributeAccessor(ctx, "");
        _ = acc.WaitAsync("Flag", true, TimeSpan.FromSeconds(30));

        var req = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
        Assert.Equal("Flag", req.AttributeName);
    }

    // ── WaitForAsync (spec §3): scope resolution + populated WaitResult ───────

    [Fact]
    public async Task WaitForAsync_Value_AppliesScopeResolution_AndSurfacesPopulatedWaitResult()
    {
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        // Composed scope "TempSensor" — Resolve("Flag") => "TempSensor.Flag".
        var acc = new AttributeAccessor(ctx, "TempSensor");

        var task = acc.WaitForAsync("Flag", true, TimeSpan.FromSeconds(30));

        // The actor receives the scope-resolved, codec-encoded request.
        var req = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
        Assert.Equal("TempSensor.Flag", req.AttributeName);
        Assert.Equal(AttributeValueCodec.Encode(true), req.TargetValueEncoded);
        Assert.Null(req.Predicate);
        Assert.False(req.RequireGoodQuality);

        // Reply with a matched response — the accessor must surface the full WaitResult.
        probe.Reply(new WaitForAttributeResponse(
            req.CorrelationId, Matched: true, Value: true, Quality: "Good", TimedOut: false));

        var result = await task;
        Assert.True(result.Matched);
        Assert.Equal(true, result.Value);
        Assert.Equal("Good", result.Quality);
        Assert.False(result.TimedOut);
    }

    [Fact]
    public async Task WaitForAsync_Predicate_AppliesScopeResolution_AndSurfacesWaitResult()
    {
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        var acc = new AttributeAccessor(ctx, "Motor.TempSensor");

        Func<object?, bool> predicate = _ => true;
        var task = acc.WaitForAsync("Level", predicate, TimeSpan.FromSeconds(30));

        var req = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
        Assert.Equal("Motor.TempSensor.Level", req.AttributeName);
        Assert.Null(req.TargetValueEncoded);
        Assert.NotNull(req.Predicate);

        probe.Reply(new WaitForAttributeResponse(
            req.CorrelationId, Matched: true, Value: 42, Quality: "Good", TimedOut: false));

        var result = await task;
        Assert.True(result.Matched);
        Assert.Equal(42, result.Value);
    }

    [Fact]
    public async Task WaitForAsync_RequireGoodQuality_ThreadsFlagIntoRequest()
    {
        var probe = CreateTestProbe();
        var ctx = MakeContext(probe.Ref);

        var acc = new AttributeAccessor(ctx, "");
        var task = acc.WaitForAsync("Flag", true, TimeSpan.FromSeconds(30), requireGoodQuality: true);

        var req = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
        Assert.True(req.RequireGoodQuality);

        probe.Reply(new WaitForAttributeResponse(
            req.CorrelationId, Matched: false, Value: null, Quality: null, TimedOut: true));

        var result = await task;
        Assert.False(result.Matched);
        Assert.True(result.TimedOut);
        Assert.Null(result.Value);
    }
}
