using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;

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

/// <summary>
/// M3.1: parse + compile gate tests. The "representative real script" corpus is
/// the PRIMARY guard that <see cref="ScriptCompileSurface"/> faithfully mirrors
/// the runtime <c>ScriptGlobals</c> surface — if a member or signature drifts,
/// the corpus stops binding and this test fails.
/// </summary>
public class RoslynScriptCompilerTests
{
    [Fact]
    public void ParseDiagnostics_NonEmpty_ForSyntaxError()
    {
        Assert.NotEmpty(RoslynScriptCompiler.ParseDiagnostics("var x = ;"));
    }

    [Fact]
    public void ParseDiagnostics_Empty_ForValidSyntax()
    {
        Assert.Empty(RoslynScriptCompiler.ParseDiagnostics("var x = 1;"));
    }

    [Fact]
    public void Compile_NonEmpty_ForUndefinedSymbol()
    {
        var code = "var x = NoSuchThing.Foo();";
        Assert.NotEmpty(RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)));
    }

    [Fact]
    public void Compile_Empty_ForRepresentativeRealScript()
    {
        const string code = """
            var temp = Attributes["Temperature"];
            Attributes["Setpoint"] = 42;
            var r = await ExternalSystem.Call("erp", "sync");
            var op = await Database.CachedWrite("hist", "INSERT ...");
            await Notify.To("ops").Send("subj", "msg");
            var shared = await Scripts.CallShared("Helper");
            var child = Children["Pump"].Attributes["Speed"];

            // Widen coverage across the rest of the surface.
            var attr = await Instance.GetAttribute("Temperature");
            await Instance.SetAttribute("Setpoint", "43");
            var track = await Instance.Tracking.Status(op);
            var parentSpeed = Parent?.Attributes["Speed"];
            var alarmName = Alarm?.Name;
            var p = Parameters;
            var ct = CancellationToken;
            var status = await Notify.Status("notif-id");
            var cachedCall = await ExternalSystem.CachedCall("erp", "ping");
            var resolved = Attributes.Resolve("Temperature");
            var conn = await Database.Connection("hist");
            var scope = Scope;
            var currentAlarms = await Alarms.CurrentAsync();
            var viaInstance = await Instance.Alarms.CurrentAsync();
            """;

        var diagnostics = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface));
        Assert.Empty(diagnostics);
    }

    [Fact]
    public void Compile_Empty_ForTheMesAlarmStatusScriptShape()
    {
        // MES alarm-status API §5.3: the CvdReactor.SimpleAlarmStatus body must bind at
        // design time. Every ScriptAlarm field the endpoint projects is read here, so a
        // rename on the record breaks this test rather than the deployed script.
        const string code = """
            const int MesBandMin = 900;
            const int MesBandMax = 999;

            var raw  = (Parameters["SAPID"] as string) ?? "";
            var code = Parameters["MachineCode"]?.ToString() ?? "";

            var core = raw.EndsWith("_LT") ? raw.Substring(0, raw.Length - 3) : raw;
            var side = core.EndsWith("_A") ? "Left" : core.EndsWith("_B") ? "Right" : null;

            System.Func<string, bool> InScope = src =>
                string.IsNullOrEmpty(src)
                || side == null
                || src.StartsWith(side, System.StringComparison.OrdinalIgnoreCase)
                || src.StartsWith("Reactor", System.StringComparison.OrdinalIgnoreCase);

            var alarms = await Alarms.CurrentAsync();
            var infos = alarms
                .Where(a => a.Active && !a.IsConfiguredPlaceholder)
                .Where(a => InScope(a.NativeSourceCanonicalName))
                .Where(a => a.Severity >= MesBandMin && a.Severity <= MesBandMax)
                .Select(a => new {
                    Name = a.Name,
                    HierarchicalName = code + "." + a.Name,
                    Description = string.IsNullOrEmpty(a.Message) ? a.AlarmTypeName : a.Message,
                    IsFlaggedForMES = a.Severity >= MesBandMin && a.Severity <= MesBandMax,
                    Severity = a.Severity,
                    StatusCode = a.Acknowledged ? "Triggered.Acked" : "Triggered",
                    TriggeredDT = (a.OriginalRaiseTime ?? a.Timestamp),
                    AckDT = a.AckTime,
                    AckComment = a.OperatorComment,
                }).ToList();

            return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos };
            """;

        Assert.Empty(RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)));
    }

    [Fact]
    public void Compile_Empty_ForTriggerExpression()
    {
        const string expr =
            "Attributes[\"Temp\"] != null && (int)(Children[\"P\"].Attributes[\"S\"] ?? 0) > 5";

        var diagnostics = RoslynScriptCompiler.Compile(expr, typeof(TriggerCompileSurface));
        Assert.Empty(diagnostics);
    }

    [Fact]
    public void Compile_Empty_ForWaitAsyncAndWaitForAsync()
    {
        // Covers all four overloads: value + predicate for both WaitAsync and
        // WaitForAsync, on both root scope and composed/child scope.
        const string code = """
            // Root scope — value overload
            var matched = await Attributes.WaitAsync("Flag", true, System.TimeSpan.FromSeconds(5));
            // Root scope — predicate overload with requireGoodQuality
            var matched2 = await Attributes.WaitAsync("Flag", v => v != null, System.TimeSpan.FromSeconds(5), true);
            // Root scope — WaitForAsync value overload
            var r = await Attributes.WaitForAsync("Flag", true, System.TimeSpan.FromSeconds(5));
            // Root scope — WaitForAsync predicate overload with requireGoodQuality
            var r2 = await Attributes.WaitForAsync("Flag", v => v != null, System.TimeSpan.FromSeconds(5), true);
            // Composed/child scope — value overload
            var childMatched = await Children["LeftMESReceiver"].Attributes.WaitAsync("MoveInCompleteFlag", true, System.TimeSpan.FromSeconds(5));
            """;

        var diagnostics = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface));
        Assert.Empty(diagnostics);
    }

    [Fact]
    public void Compile_TwiceWithSharedResolver_StillCompilesCleanly()
    {
        Assert.Empty(RoslynScriptCompiler.Compile("return 1 + 1;"));
        Assert.Empty(RoslynScriptCompiler.Compile("return 2 + 2;"));
    }
}
