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

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

/// <summary>
/// SiteRuntime-011: regression tests for the semantic-analysis trust-model validation.
/// The previous implementation was a raw substring scan of the source text — it both
/// missed forbidden APIs (no literal namespace string) and raised false positives on
/// the namespace string appearing in comments, string literals or unrelated identifiers.
///
/// As of M3.3, <c>ValidateTrustModel</c> delegates to the shared authoritative
/// <c>ScriptAnalysis.ScriptTrustValidator</c>, which retains this same Roslyn
/// semantic-symbol analysis (plus reflection-gateway hardening), so these bypass /
/// false-positive / allowed-exception regressions continue to hold through the
/// delegating service.
/// </summary>
public class TrustModelSemanticTests
{
    private readonly ScriptCompilationService _service =
        new(NullLogger<ScriptCompilationService>.Instance);

    // ── Bypass cases (under-inclusive substring scan would MISS these) ──

    [Fact]
    public void TrustModel_GlobalQualifiedForbiddenType_IsDetected()
    {
        // `global::`-prefixed name — the literal "System.IO" substring is still present
        // here, but the resolved-symbol approach catches it regardless of spelling.
        var violations = _service.ValidateTrustModel(
            "global::System.IO.File.ReadAllText(\"/etc/passwd\")");
        Assert.NotEmpty(violations);
    }

    [Fact]
    public void TrustModel_ForbiddenTypeViaUsingAlias_IsDetected()
    {
        // A using-alias hides the forbidden namespace from a substring scan entirely:
        // the script body never writes "System.IO". Semantic resolution still sees that
        // the alias resolves to System.IO.File.
        var code = """
            using F = System.IO.File;
            F.ReadAllText("/etc/passwd");
            """;
        var violations = _service.ValidateTrustModel(code);
        Assert.NotEmpty(violations);
        Assert.Contains(violations, v => v.Contains("System.IO"));
    }

    // ── False-positive cases (over-inclusive substring scan would WRONGLY flag these) ──

    [Fact]
    public void TrustModel_ForbiddenNamespaceInStringLiteral_IsNotFlagged()
    {
        // "System.IO" appears only inside a string literal — not an API reference.
        var violations = _service.ValidateTrustModel(
            "var label = \"System.IO is blocked\"; return label;");
        Assert.Empty(violations);
    }

    [Fact]
    public void TrustModel_ForbiddenNamespaceInComment_IsNotFlagged()
    {
        var code = """
            // This script does not use System.IO or System.Reflection at all.
            var x = 1 + 2;
            return x;
            """;
        var violations = _service.ValidateTrustModel(code);
        Assert.Empty(violations);
    }

    [Fact]
    public void TrustModel_UnrelatedIdentifierContainingForbiddenSubstring_IsNotFlagged()
    {
        // A local variable whose name merely contains "Threading" is harmless.
        var code = """
            var ProcessThreadingCount = 5;
            return ProcessThreadingCount + 1;
            """;
        var violations = _service.ValidateTrustModel(code);
        Assert.Empty(violations);
    }

    // ── Allowed exceptions still resolve correctly ──

    [Fact]
    public void TrustModel_TaskAndCancellationToken_RemainAllowed()
    {
        var code = """
            var cts = new System.Threading.CancellationTokenSource();
            await System.Threading.Tasks.Task.Delay(1, cts.Token);
            return 0;
            """;
        var violations = _service.ValidateTrustModel(code);
        Assert.Empty(violations);
    }
}
