using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

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

/// <summary>
/// WP-6 (Phase 8): Script sandboxing verification.
/// Adversarial tests that verify forbidden APIs are blocked at compilation time.
/// </summary>
public class SandboxTests
{
    private readonly ScriptCompilationService _service;

    public SandboxTests()
    {
        _service = new ScriptCompilationService(NullLogger<ScriptCompilationService>.Instance);
    }

    // ── System.IO forbidden ──

    [Fact]
    public void Sandbox_FileRead_Blocked()
    {
        var result = _service.Compile("evil", """System.IO.File.ReadAllText("/etc/passwd")""");
        Assert.False(result.IsSuccess);
        Assert.Contains(result.Errors, e => e.Contains("System.IO"));
    }

    [Fact]
    public void Sandbox_FileWrite_Blocked()
    {
        var result = _service.Compile("evil", """System.IO.File.WriteAllText("/tmp/hack.txt", "pwned")""");
        Assert.False(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_DirectoryCreate_Blocked()
    {
        var result = _service.Compile("evil", """System.IO.Directory.CreateDirectory("/tmp/evil")""");
        Assert.False(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_FileStream_Blocked()
    {
        var result = _service.Compile("evil", """new System.IO.FileStream("/tmp/x", System.IO.FileMode.Create)""");
        Assert.False(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_StreamReader_Blocked()
    {
        var result = _service.Compile("evil", """new System.IO.StreamReader("/tmp/x")""");
        Assert.False(result.IsSuccess);
    }

    // ── Process forbidden ──

    [Fact]
    public void Sandbox_ProcessStart_Blocked()
    {
        var result = _service.Compile("evil", """System.Diagnostics.Process.Start("cmd.exe", "/c dir")""");
        Assert.False(result.IsSuccess);
        Assert.Contains(result.Errors, e => e.Contains("Process"));
    }

    [Fact]
    public void Sandbox_ProcessStartInfo_Blocked()
    {
        var code = """
            var psi = new System.Diagnostics.Process();
            psi.StartInfo.FileName = "bash";
            """;
        var result = _service.Compile("evil", code);
        Assert.False(result.IsSuccess);
    }

    // ── Threading forbidden (except Tasks/CancellationToken) ──

    [Fact]
    public void Sandbox_ThreadCreate_Blocked()
    {
        var result = _service.Compile("evil", """new System.Threading.Thread(() => {}).Start()""");
        Assert.False(result.IsSuccess);
        Assert.Contains(result.Errors, e => e.Contains("System.Threading"));
    }

    [Fact]
    public void Sandbox_Mutex_Blocked()
    {
        var result = _service.Compile("evil", """new System.Threading.Mutex()""");
        Assert.False(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_Semaphore_Blocked()
    {
        var result = _service.Compile("evil", """new System.Threading.Semaphore(1, 1)""");
        Assert.False(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_TaskDelay_Allowed()
    {
        // async/await and Tasks are explicitly allowed
        var violations = _service.ValidateTrustModel("await System.Threading.Tasks.Task.Delay(100)");
        Assert.Empty(violations);
    }

    [Fact]
    public void Sandbox_CancellationToken_Allowed()
    {
        var violations = _service.ValidateTrustModel(
            "var ct = System.Threading.CancellationToken.None;");
        Assert.Empty(violations);
    }

    [Fact]
    public void Sandbox_CancellationTokenSource_Allowed()
    {
        var violations = _service.ValidateTrustModel(
            "var cts = new System.Threading.CancellationTokenSource();");
        Assert.Empty(violations);
    }

    // ── Reflection forbidden ──

    [Fact]
    public void Sandbox_GetType_Reflection_Blocked()
    {
        var result = _service.Compile("evil",
            """typeof(string).GetMethods(System.Reflection.BindingFlags.NonPublic)""");
        Assert.False(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_AssemblyLoad_Blocked()
    {
        var result = _service.Compile("evil",
            """System.Reflection.Assembly.Load("System.Runtime")""");
        Assert.False(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_ActivatorCreateInstance_ViaReflection_Blocked()
    {
        var result = _service.Compile("evil",
            """System.Reflection.Assembly.GetExecutingAssembly()""");
        Assert.False(result.IsSuccess);
    }

    // ── Raw network forbidden ──

    [Fact]
    public void Sandbox_TcpClient_Blocked()
    {
        var result = _service.Compile("evil", """new System.Net.Sockets.TcpClient("evil.com", 80)""");
        Assert.False(result.IsSuccess);
        Assert.Contains(result.Errors, e => e.Contains("System.Net.Sockets"));
    }

    [Fact]
    public void Sandbox_UdpClient_Blocked()
    {
        var result = _service.Compile("evil", """new System.Net.Sockets.UdpClient(1234)""");
        Assert.False(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_HttpClient_Blocked()
    {
        var result = _service.Compile("evil", """new System.Net.Http.HttpClient()""");
        Assert.False(result.IsSuccess);
        Assert.Contains(result.Errors, e => e.Contains("System.Net.Http"));
    }

    [Fact]
    public void Sandbox_HttpRequestMessage_Blocked()
    {
        var result = _service.Compile("evil",
            """new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, "https://evil.com")""");
        Assert.False(result.IsSuccess);
    }

    // ── Allowed operations ──

    [Fact]
    public void Sandbox_BasicMath_Allowed()
    {
        var result = _service.Compile("safe", "Math.Max(1, 2)");
        Assert.True(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_LinqOperations_Allowed()
    {
        var result = _service.Compile("safe",
            "new List<int> { 1, 2, 3 }.Where(x => x > 1).Sum()");
        Assert.True(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_StringOperations_Allowed()
    {
        var result = _service.Compile("safe",
            """string.Join(", ", new[] { "a", "b", "c" })""");
        Assert.True(result.IsSuccess);
    }

    [Fact]
    public void Sandbox_DateTimeOperations_Allowed()
    {
        var result = _service.Compile("safe",
            "DateTime.UtcNow.AddHours(1).ToString(\"o\")");
        Assert.True(result.IsSuccess);
    }

    // ── Execution cancellation ──
    //
    // These two tests used to arm a wall-clock CancellationTokenSource
    // (500 ms / 100 ms) and hope it expired while the script body was still
    // running. That is a race between a fixed timer and a fixed amount of
    // work, not a synchronization, and it is wrong in both directions:
    //
    //   * Too slow a timer relative to the work and the script simply
    //     FINISHES first, so nothing is cancelled and ThrowsAnyAsync fails
    //     with "No exception was thrown". The bounded loop below was measured
    //     at 298 ms against a 100 ms pin on this machine — a 3x margin, which
    //     a faster host (or any change to the scripting OptimizationLevel,
    //     which is Debug today) closes. Reproduced deterministically by
    //     shrinking the loop to 1M iterations: it then ran in 76 ms and the
    //     test failed with exactly that message while all 27 siblings passed.
    //   * Too fast a timer and the token is ALREADY cancelled before the body
    //     is entered — Roslyn's runner throws OperationCanceledException up
    //     front (verified), so the test goes GREEN without the script's own
    //     in-loop ThrowIfCancellationRequested ever being reached. A vacuous
    //     pass is worse than a flake: it asserts nothing.
    //
    // Both are replaced with a deterministic edge. The script triggers the
    // cancellation itself, synchronously, from inside its own loop via an
    // Action handed in through Parameters. When that call returns, the token
    // is already cancelled ON THE SAME THREAD, so the next in-loop check is
    // guaranteed to observe it — with iterations to spare and no reliance on
    // wall-clock time, host speed or scheduler behaviour. Sandbox_UncancelledScript_RunsToCompletion
    // is the negative control for the bounded case.

    /// <summary>Builds globals whose "cancelNow" parameter cancels <paramref name="cts"/> when the script invokes it.</summary>
    private static ScriptGlobals GlobalsWithCancelHook(CancellationTokenSource cts, Action? onSignal = null)
        => new()
        {
            Instance = null!,
            Parameters = new ScriptParameters(new Dictionary<string, object?>
            {
                ["cancelNow"] = onSignal ?? (() => cts.Cancel())
            }),
            CancellationToken = cts.Token
        };

    [Fact]
    public async Task Sandbox_InfiniteLoop_CancelledByToken()
    {
        // An unbounded loop that cancels itself on the first pass, so the
        // check on that same pass is the one that throws.
        var code = """
            var spins = 0;
            while (true) {
                spins++;
                if (spins == 1) ((Action)Parameters["cancelNow"]!)();
                CancellationToken.ThrowIfCancellationRequested();
            }
            return null;
            """;

        var result = _service.Compile("infinite", code);
        Assert.True(result.IsSuccess, "Infinite loop compiles but should be cancelled at runtime");

        using var cts = new CancellationTokenSource();
        var globals = GlobalsWithCancelHook(cts);

        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
        {
            await result.CompiledScript!.RunAsync(globals, cts.Token);
        });
    }

    [Fact]
    public async Task Sandbox_LongRunningScript_TimesOut()
    {
        // Heavy bounded computation with periodic cancellation checks. It
        // cancels itself at i == 1, leaving ~100M iterations and ~10,000
        // cancellation checks still ahead of it, so interruption is certain
        // however fast the host runs — the point being that a long script
        // does not get to run to completion once its deadline has fired.
        var code = """
            var sum = 0L;
            for (var i = 0; i < 100_000_000; i++) {
                sum += i;
                if (i == 1) ((Action)Parameters["cancelNow"]!)();
                if (i % 10000 == 0) CancellationToken.ThrowIfCancellationRequested();
            }
            return sum;
            """;

        var result = _service.Compile("heavy", code);
        Assert.True(result.IsSuccess);

        using var cts = new CancellationTokenSource();
        var globals = GlobalsWithCancelHook(cts);

        await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
        {
            await result.CompiledScript!.RunAsync(globals, cts.Token);
        });
    }

    [Fact]
    public async Task Sandbox_UncancelledScript_RunsToCompletion()
    {
        // Negative control for Sandbox_LongRunningScript_TimesOut: the SAME
        // script shape, with the signal wired to a no-op instead of Cancel().
        // It must run every iteration and return the closed-form sum. This is
        // what proves the sibling's OperationCanceledException is caused by
        // the cancellation and not by the loop being unreachable, the trust
        // model rejecting the body, or the runner failing for its own reasons.
        var code = """
            var sum = 0L;
            for (var i = 0; i < 100_000_000; i++) {
                sum += i;
                if (i == 1) ((Action)Parameters["cancelNow"]!)();
                if (i % 10000 == 0) CancellationToken.ThrowIfCancellationRequested();
            }
            return sum;
            """;

        var result = _service.Compile("heavy-control", code);
        Assert.True(result.IsSuccess);

        using var cts = new CancellationTokenSource();
        var signalled = false;
        var globals = GlobalsWithCancelHook(cts, onSignal: () => signalled = true);

        var value = await result.CompiledScript!.RunAsync(globals, cts.Token);

        Assert.True(signalled, "The script's in-loop signal must actually fire.");
        Assert.Equal(4999999950000000L, value.ReturnValue);
    }

    // ── Combined adversarial attempts ──

    [Fact]
    public void Sandbox_MultipleViolationsInOneScript_AllDetected()
    {
        var code = """
            System.IO.File.ReadAllText("/etc/passwd");
            System.Diagnostics.Process.Start("cmd");
            new System.Net.Sockets.TcpClient();
            new System.Net.Http.HttpClient();
            """;

        var violations = _service.ValidateTrustModel(code);
        Assert.True(violations.Count >= 4,
            $"Expected at least 4 violations but got {violations.Count}: {string.Join("; ", violations)}");
    }

    [Fact]
    public void Sandbox_UsingDirective_StillDetected()
    {
        var code = """
            // Even with using aliases, the namespace string is still detected
            var x = System.IO.Path.GetTempPath();
            """;

        var violations = _service.ValidateTrustModel(code);
        Assert.NotEmpty(violations);
    }
}
