using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;

namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests;

/// <summary>
/// Arch-review S1: the compiled-handler cache must be revision-checked against the
/// freshly-fetched ApiMethod.Script so a standby node (whose cache was populated at
/// its own startup) never serves a stale delegate after failover, and a method fixed
/// out-of-band is never stuck behind a stale known-bad record.
/// </summary>
public class InboundScriptExecutorStalenessTests
{
    private readonly InboundScriptExecutor _executor;
    private readonly RouteHelper _route;

    public InboundScriptExecutorStalenessTests()
    {
        _executor = new InboundScriptExecutor(
            NullLogger<InboundScriptExecutor>.Instance, Substitute.For<IServiceProvider>());
        _route = new RouteHelper(
            Substitute.For<IInstanceLocator>(), Substitute.For<IInstanceRouter>());
    }

    private Task<InboundScriptResult> Run(ApiMethod m) => _executor.ExecuteAsync(
        m, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));

    [Fact]
    public async Task UpdatedScript_RecompilesInsteadOfServingStaleHandler()
    {
        // Simulates the failover scenario: this node compiled v1 at startup...
        var v1 = new ApiMethod("stale", "return 1;") { Id = 1, TimeoutSeconds = 10 };
        Assert.True(_executor.CompileAndRegister(v1));
        var r1 = await Run(v1);
        Assert.Contains("1", r1.ResultJson);

        // ...the active node updated the method (this node never heard about it);
        // the per-request DB fetch hands ExecuteAsync the NEW row.
        var v2 = new ApiMethod("stale", "return 2;") { Id = 1, TimeoutSeconds = 10 };
        var r2 = await Run(v2);

        Assert.True(r2.Success);
        Assert.Contains("2", r2.ResultJson); // old cache would have returned 1
    }

    [Fact]
    public async Task KnownBadMethod_FixedScript_CompilesInsteadOfFastFailing()
    {
        // The mirror-image variant: broken at this node's startup...
        var broken = new ApiMethod("fixme", "%%% not C# %%%") { Id = 2, TimeoutSeconds = 10 };
        Assert.False(_executor.CompileAndRegister(broken));
        var r1 = await Run(broken);
        Assert.False(r1.Success);

        // ...fixed via the management path on the OTHER node; this node's fresh
        // DB fetch carries the fixed script.
        var fixedMethod = new ApiMethod("fixme", "return 42;") { Id = 2, TimeoutSeconds = 10 };
        var r2 = await Run(fixedMethod);

        Assert.True(r2.Success);
        Assert.Contains("42", r2.ResultJson);
    }

    [Fact]
    public async Task KnownBadMethod_SameScript_StillFastFails()
    {
        var broken = new ApiMethod("stillbad", "%%% not C# %%%") { Id = 3, TimeoutSeconds = 10 };
        Assert.False(_executor.CompileAndRegister(broken));

        // Same script text → the fast-fail record must hold (no per-request Roslyn).
        var again = new ApiMethod("stillbad", "%%% not C# %%%") { Id = 3, TimeoutSeconds = 10 };
        var r = await Run(again);
        Assert.False(r.Success);
        Assert.Contains("Script compilation failed", r.ErrorMessage);
    }

    [Fact]
    public async Task TestSeamRegisterHandler_WithoutScript_IsNotRevisionChecked()
    {
        // RegisterHandler (script-less test seam) pins the handler regardless of row content.
        var m = new ApiMethod("pinned", "return 0;") { Id = 4, TimeoutSeconds = 10 };
        _executor.RegisterHandler("pinned", async _ => { await Task.CompletedTask; return 99; });
        var r = await Run(m);
        Assert.True(r.Success);
        Assert.Contains("99", r.ResultJson);
    }

    [Fact]
    public async Task InvalidateMethod_DropsHandlerAndKnownBadRecord()
    {
        var good = new ApiMethod("inv", "return 5;") { Id = 5, TimeoutSeconds = 10 };
        Assert.True(_executor.CompileAndRegister(good));
        var broken = new ApiMethod("invbad", "%%%") { Id = 6, TimeoutSeconds = 10 };
        Assert.False(_executor.CompileAndRegister(broken));

        _executor.InvalidateMethod("inv");
        _executor.InvalidateMethod("invbad");

        Assert.Equal(0, _executor.KnownBadMethodCount);
        // After invalidation the method lazily recompiles from the row — still serves.
        var r = await Run(good);
        Assert.True(r.Success);
    }
}
