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

/// <summary>
/// InboundAPI-005 / InboundAPI-015: tests for the script-trust-model checker.
///
/// Since M3.4 <see cref="ForbiddenApiChecker"/> delegates to the shared
/// <c>ScriptTrustValidator</c> in <c>ZB.MOM.WW.ScadaBridge.ScriptAnalysis</c>.
/// The unified policy differs from the old local deny-list in one notable way:
/// <c>System.Diagnostics</c> is loosened to <c>System.Diagnostics.Process</c>
/// only — <c>Stopwatch</c>, <c>Debug</c>, and other non-Process Diagnostics
/// types are now ALLOWED.
///
/// InboundAPI-015 hardening (reflection-gateway / dynamic / Activator) is
/// preserved unchanged via the shared validator.
///
/// Violation message wording is intentionally not asserted — callers should
/// use <c>.Count</c> / <c>.Any()</c> rather than exact strings.
/// </summary>
public class ForbiddenApiCheckerTests
{
    private static bool IsRejected(string script) =>
        ForbiddenApiChecker.FindViolations(script).Count > 0;

    // --- Baseline: legitimate scripts must still pass ---

    [Theory]
    [InlineData("return 1 + 1;")]
    [InlineData("var list = new List<int> { 1, 2, 3 }; return list.Sum();")]
    [InlineData("return Parameters.Get<int>(\"x\") * 2;")]
    [InlineData("await Task.Delay(1); return null;")]
    [InlineData("var r = await Route.To(\"inst\").Call(\"s\"); return r;")]
    [InlineData("Action a = () => {}; a.Invoke(); return null;")]
    public void PermittedScript_NotRejected(string script)
    {
        Assert.False(IsRejected(script), script);
    }

    // --- System.Diagnostics loosening: Process is still forbidden; non-Process types
    //     (Stopwatch, Debug) are now ALLOWED by the unified policy (M3.4). The old
    //     InboundAPI checker blocked the entire System.Diagnostics namespace. ---

    [Fact]
    public void Diagnostics_Process_StillForbidden()
    {
        // System.Diagnostics.Process is an explicit forbidden scope in ScriptTrustPolicy.
        Assert.True(IsRejected("System.Diagnostics.Process.Start(\"/bin/sh\"); return null;"));
    }

    [Fact]
    public void Diagnostics_Stopwatch_NowAllowed()
    {
        // System.Diagnostics.Stopwatch is NOT under the forbidden scope
        // "System.Diagnostics.Process" — the unified policy only forbids
        // System.Diagnostics.Process, not the whole System.Diagnostics namespace.
        Assert.False(IsRejected("var sw = new System.Diagnostics.Stopwatch(); sw.Start(); return null;"));
    }

    [Fact]
    public void Diagnostics_Debug_NowAllowed()
    {
        // System.Diagnostics.Debug is also not covered by the Process-only scope.
        Assert.False(IsRejected("System.Diagnostics.Debug.WriteLine(\"hi\"); return null;"));
    }

    // --- Baseline: forbidden namespaces (textual) must still be rejected ---

    [Theory]
    [InlineData("System.IO.File.Delete(\"/tmp/x\"); return null;")]
    [InlineData("using System.Reflection; return null;")]
    [InlineData("var s = new System.Net.Sockets.Socket(default, default, default); return null;")]
    public void ForbiddenNamespace_Rejected(string script)
    {
        Assert.True(IsRejected(script), script);
    }

    // --- InboundAPI-015: reflection reachable without a forbidden namespace token ---

    [Fact]
    public void Reflection_AssemblyPropertyAccess_Rejected()
    {
        // typeof(string).Assembly — .Assembly is a reflection gateway off a permitted type.
        Assert.True(IsRejected("var a = typeof(string).Assembly; return null;"));
    }

    [Fact]
    public void Reflection_AssemblyGetType_Rejected()
    {
        // The classic bypass: obtain System.IO.File as a Type via a string literal.
        Assert.True(IsRejected(
            "var t = typeof(string).Assembly.GetType(\"System.IO.File\"); return null;"));
    }

    [Fact]
    public void Reflection_ObjectGetType_Rejected()
    {
        // x.GetType() returns a System.Type — a reflection gateway.
        Assert.True(IsRejected("var t = \"\".GetType(); return null;"));
    }

    [Fact]
    public void Reflection_TypeGetTypeStatic_Rejected()
    {
        Assert.True(IsRejected("var t = Type.GetType(\"System.IO.File\"); return null;"));
    }

    [Fact]
    public void Reflection_ActivatorCreateInstance_Rejected()
    {
        Assert.True(IsRejected(
            "var o = Activator.CreateInstance(Type.GetType(\"System.IO.File\")); return null;"));
    }

    [Fact]
    public void Reflection_InvokeMember_Rejected()
    {
        Assert.True(IsRejected(
            "typeof(object).InvokeMember(\"x\", default, null, null, null); return null;"));
    }

    [Fact]
    public void Reflection_GetMethodInvoke_Rejected()
    {
        Assert.True(IsRejected(
            "var m = typeof(object).GetMethod(\"ToString\"); return null;"));
    }

    [Fact]
    public void Reflection_GetTypeInfo_Rejected()
    {
        Assert.True(IsRejected("var ti = \"\".GetType().GetTypeInfo(); return null;"));
    }

    [Fact]
    public void DynamicKeyword_Rejected()
    {
        // dynamic widens late-bound member access the static walker cannot see through.
        Assert.True(IsRejected("dynamic d = Parameters; return null;"));
    }

    // --- FindViolations returns a non-null, non-throwing result for edge inputs ---

    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData(null!)]
    public void EmptyOrWhitespace_ReturnsEmptyList(string? script)
    {
        var result = ForbiddenApiChecker.FindViolations(script!);
        Assert.NotNull(result);
        Assert.Empty(result);
    }
}
