using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;

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

/// <summary>
/// M3.1: adversarial + legitimate cases for the fused trust validator. Reject
/// cases exercise the semantic pass (alias / using static / global::), the
/// reflection-gateway hardening pass, and the namespace deny-list union; clean
/// cases pin the allowed exceptions (Tasks, CancellationToken, Diagnostics
/// other than Process).
/// </summary>
public class ScriptTrustValidatorTests
{
    // ---- Reject (non-empty violations) --------------------------------------

    [Fact]
    public void Rejects_SystemIo_Using()
    {
        var code = "using System.IO; var x = File.ReadAllText(\"/etc/passwd\");";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_SystemIo_GlobalQualified()
    {
        var code = "var x = global::System.IO.File.ReadAllText(\"x\");";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_SystemIo_Aliased()
    {
        var code = "using IO = System.IO; var f = IO.File.ReadAllText(\"x\");";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_SystemIo_UsingStatic()
    {
        var code = "using static System.IO.File; var s = ReadAllText(\"x\");";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_ReflectionGateway_OffPermittedType()
    {
        var code = "var t = typeof(string).Assembly.GetType(\"System.IO.File\");";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_Dynamic_Keyword()
    {
        var code = "dynamic d = 5; d.Foo();";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_Activator_CreateInstance()
    {
        var code = "var o = Activator.CreateInstance(typeof(string));";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_RuntimeInteropServices()
    {
        var code = "using System.Runtime.InteropServices; var h = Marshal.SizeOf<int>();";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_MicrosoftWin32()
    {
        var code = "using Microsoft.Win32; var k = Registry.LocalMachine;";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_Threading_Thread_Sleep()
    {
        var code = "System.Threading.Thread.Sleep(10);";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_All_SystemNet()
    {
        var code = "System.Net.WebClient w = null;";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_ReflectionGateway_OffAllowedTaskType()
    {
        // Guards the reflection-first ordering: a gateway member hung off an
        // allowed System.Threading.Tasks type must still be rejected even though
        // the chain's namespace prefix is an allowed exception.
        var code = "var a = System.Threading.Tasks.Task.CompletedTask.GetType().Assembly;";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_ForbiddenIo_NestedInAllowedTaskRunLambda()
    {
        // A forbidden System.IO reference buried inside an allowed Task.Run lambda.
        // The allowed-exception prefix on the outer member access must NOT shadow
        // the nested forbidden reference — Pass 2 must descend into the lambda.
        var code = "await System.Threading.Tasks.Task.Run(() => System.IO.File.ReadAllText(\"x\"));";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_ForbiddenMutex_AsGenericArg_UnderAllowedTasksPrefix()
    {
        // System.Threading.Mutex (forbidden) appears as a generic argument of an
        // allowed System.Threading.Tasks.TaskCompletionSource<T>. The allowed
        // outer name must not shadow the forbidden generic arg.
        var code = "System.Threading.Tasks.TaskCompletionSource<System.Threading.Mutex> tcs = null;";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_DirectThreadingMutex_NotThreadSleep()
    {
        // A direct forbidden System.Threading type (not Thread.Sleep) — pins that
        // the broad System.Threading deny-list catches more than the one cased test.
        var code = "var m = new System.Threading.Mutex();";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_ForbiddenFileInfo_AsGenericArg()
    {
        // System.IO.FileInfo (forbidden) as a generic argument of an allowed
        // System.Collections.Generic.List<T>.
        var code = "System.Collections.Generic.List<System.IO.FileInfo> x = null;";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_NameOf_ForbiddenType()
    {
        // Conservative fail-safe: naming a forbidden type inside nameof(...) is
        // deliberately flagged (a script has no business naming it even there).
        var code = "var s = nameof(System.IO.File);";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_Process_QualifiedType()
    {
        var code = "var p = System.Diagnostics.Process.Start(\"x\");";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_Process_BareIdentifier_ViaUsing()
    {
        // System.Diagnostics is an ALLOWED namespace (Stopwatch/Debug ok), so the
        // using directive is not flagged; Process is a forbidden TYPE reached as a
        // bare identifier. This pins whether FindViolations resolves it.
        var code = "using System.Diagnostics; var p = Process.Start(\"x\");";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    // ---- Clean (empty violations) -------------------------------------------

    [Fact]
    public void Allows_TasksDelay()
    {
        var code = "await System.Threading.Tasks.Task.Delay(1);";
        Assert.Empty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Allows_DiagnosticsStopwatch_NotProcess()
    {
        var code = "var sw = System.Diagnostics.Stopwatch.StartNew();";
        Assert.Empty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Allows_CancellationTokenSource()
    {
        var code = "var c = new System.Threading.CancellationTokenSource();";
        Assert.Empty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Allows_LinqAndMath()
    {
        var code = "var n = System.Linq.Enumerable.Range(0,3).Sum(); var m = System.Math.Max(1,2);";
        Assert.Empty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Allows_AlarmsCurrentAsync_AndTheScriptAlarmType()
    {
        // MES alarm-status API §5.2/task 4. The trust model is a DENY-list over API roots,
        // not an allow-list of context members, so `Alarms` needs no policy entry — but the
        // ScriptAlarm projection it returns lives in Commons, which must resolve as a
        // permitted namespace. This pins that: a script naming the type explicitly (not just
        // via `var`) produces no violation.
        const string code = """
            var alarms = await Alarms.CurrentAsync();
            System.Collections.Generic.IReadOnlyList<
                ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts.ScriptAlarm> typed = alarms;
            var acked = typed.Where(a => a.AckTime != null).Select(a => a.Name).ToList();
            """;

        Assert.Empty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void ForbiddenScopes_DoNotCover_TheCommonsScriptSurface()
    {
        // Guards the above from the other direction: if someone ever adds a deny-list root
        // that swallows the ZB.MOM.WW namespace, every script accessor's return type would
        // become a trust violation. Fail loudly here rather than at deploy time.
        Assert.DoesNotContain(
            ScriptTrustPolicy.ForbiddenScopes,
            scope => typeof(ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts.ScriptAlarm)
                .Namespace!.StartsWith(scope, StringComparison.Ordinal));
    }

    // ---- ScriptAnalysis-003: adversarial bypass-vector coverage --------------

    // (a) TPA-FALLBACK DEGRADATION (the SA-001 hole). Forces Pass 1 onto the
    // minimal fallback reference set (DefaultAssemblies + ForbiddenAnchorAssemblies)
    // — the set used on a single-file/AOT/trimmed host with no TPA list — and
    // proves a BARE forbidden type inside an ALLOWED namespace is STILL caught.
    // Before the fix, `Process` resolved to nothing against the minimal set, the
    // syntactic fallback ignored the dotless identifier, and Pass 2 never flags a
    // bare identifier — so `Process.Start` slipped the validator entirely. The
    // anchor assemblies folded into the fallback close that hole.

    [Fact]
    public void TpaFallback_StillRejects_BareProcess_ViaUsing()
    {
        // The documented forbidden-type-in-allowed-namespace case: System.Diagnostics
        // is allowed (Stopwatch/Debug), the `using` is not flagged, and `Process`
        // is a BARE identifier. Against the minimal fallback set this must still
        // be rejected — otherwise the SA-001 fallback hole is open.
        var minimal = ScriptTrustPolicy.BuildMinimalFallbackReferences();
        var code = "using System.Diagnostics; var p = Process.Start(\"x\");";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code, minimal));
    }

    [Fact]
    public void TpaFallback_StillRejects_BareSocket_ViaUsing()
    {
        // System.Net.Sockets.Socket lives in its own assembly (not CoreLib); the
        // anchor set must include it so the minimal fallback still resolves a bare
        // `Socket` reference.
        var minimal = ScriptTrustPolicy.BuildMinimalFallbackReferences();
        var code = "using System.Net.Sockets; Socket s = null;";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code, minimal));
    }

    [Fact]
    public void TpaFallback_StillAllows_DiagnosticsStopwatch()
    {
        // Control: the fallback must not over-block — Stopwatch (System.Diagnostics,
        // allowed) stays clean even against the minimal anchor-enriched set.
        var minimal = ScriptTrustPolicy.BuildMinimalFallbackReferences();
        var code = "var sw = System.Diagnostics.Stopwatch.StartNew();";
        Assert.Empty(ScriptTrustValidator.FindViolations(code, minimal));
    }

    [Fact]
    public void MinimalFallbackReferences_Resolve_Process_AsForbidden()
    {
        // Pins the resolution mechanism directly: against the minimal fallback set,
        // bare `Process` resolves to its true namespace and is reported by the
        // semantic pass (the message names the forbidden scope), not merely caught
        // by some incidental syntactic rule.
        var minimal = ScriptTrustPolicy.BuildMinimalFallbackReferences();
        var violations = ScriptTrustValidator.FindViolations(
            "using System.Diagnostics; var p = Process.Start(\"x\");", minimal);
        Assert.Contains(violations, v => v.Contains("System.Diagnostics.Process", StringComparison.Ordinal));
    }

    // (b) EXTENSION-METHOD invocation of a forbidden API. `asm.GetCustomAttribute<T>()`
    // resolves to the extension method on System.Reflection.CustomAttributeExtensions
    // (a forbidden namespace) even though it is invoked in receiver position — the
    // semantic pass resolves the reduced extension method's containing type, so the
    // forbidden namespace is caught through the invocation itself.
    [Fact]
    public void Rejects_ExtensionMethod_InForbiddenNamespace()
    {
        var code =
            "using System.Reflection; " +
            "Assembly asm = typeof(string).Assembly; " +
            "var a = asm.GetCustomAttribute<System.ObsoleteAttribute>();";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    // (c) VERBATIM (@) and UNICODE-ESCAPE spellings of a forbidden identifier.
    // VisitIdentifierName compares Identifier.ValueText, which decodes both the
    // verbatim '@' prefix and \uXXXX escapes — so neither spelling evades the
    // ForbiddenIdentifiers deny-list.
    [Fact]
    public void Rejects_VerbatimIdentifier_Activator()
    {
        var code = "var o = @Activator.CreateInstance(typeof(string));";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_UnicodeEscapedIdentifier_Activator()
    {
        // 'A' is 'A' — the token spells "Activator" but ValueText is "Activator".
        var code = "var o = \\u0041ctivator.CreateInstance(typeof(string));";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    // (d) UNSAFE block. The validator is a forbidden-API deny-list, not a
    // language-feature gate: a benign `unsafe` block reaches no forbidden API, so
    // it must NOT be a false positive — while a forbidden API used INSIDE an unsafe
    // block is still caught (the walker descends into the block).
    [Fact]
    public void Allows_BenignUnsafeBlock_NoForbiddenApi()
    {
        var code = "unsafe { int x = 1; int* p = &x; var y = *p; }";
        Assert.Empty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Rejects_ForbiddenApi_InsideUnsafeBlock()
    {
        var code = "unsafe { var t = typeof(string).Assembly; }";
        Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));
    }

    // (e) COMMENT / STRING-LITERAL must NOT cause a false positive. A forbidden
    // namespace mentioned only in trivia or a string literal reaches no API and
    // must stay clean (the walker inspects name/member nodes, never trivia or
    // literal text). Reconstructing a forbidden API from runtime strings is outside
    // the static validator's remit (documented sandbox caveat).
    [Fact]
    public void Allows_ForbiddenNamespace_InCommentOnly()
    {
        var code = "// using System.IO; File.ReadAllText(\"x\")\nvar y = 1;";
        Assert.Empty(ScriptTrustValidator.FindViolations(code));
    }

    [Fact]
    public void Allows_ForbiddenNamespace_InStringLiteralOnly()
    {
        var code = "var s = \"System.IO.File.ReadAllText\";";
        Assert.Empty(ScriptTrustValidator.FindViolations(code));
    }

    // ---- PLAN-05 Task 21: hardened deny-list additions -----------------------

    // Environment (whole type), GC, and the concrete ADO.NET provider namespaces
    // (Microsoft.Data / System.Data.SqlClient / .Odbc / .OleDb) are now forbidden.
    [Theory]
    [InlineData("Environment.Exit(0);")]
    [InlineData("Environment.FailFast(\"x\");")]
    [InlineData("var s = Environment.GetEnvironmentVariable(\"SCADABRIDGE_API_KEY\");")]
    [InlineData("var c = new Microsoft.Data.SqlClient.SqlConnection(\"Server=attacker\");")]
    public void FindViolations_Flags(string code) => Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));

    // Syntactic gateway closure (matters in TPA-degraded fallback mode): these
    // reflection-gateway members are rejected regardless of receiver.
    [Theory]
    [InlineData("typeof(string).Assembly.GetTypes()")]
    [InlineData("asm.EntryPoint")]
    public void SyntacticPass_Flags(string code) => Assert.NotEmpty(ScriptTrustValidator.FindViolations(code));

    // Must NOT regress legitimate scripts: Task.Delay stays allowed and
    // delegate.Invoke() is deliberately NOT on the reflection-gateway list.
    [Theory]
    [InlineData("await Task.Delay(1);")]
    [InlineData("Func<int,int> f = x => x; f.Invoke(3);")]
    public void FindViolations_Allows(string code) => Assert.Empty(ScriptTrustValidator.FindViolations(code));
}
