using Microsoft.Extensions.Caching.Memory; using NSubstitute; using ScadaLink.CentralUI.ScriptAnalysis; namespace ScadaLink.CentralUI.Tests.ScriptAnalysis; public class ScriptAnalysisServiceTests { private readonly ISharedScriptCatalog _catalog = Substitute.For(); private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 }); private readonly ScriptAnalysisService _svc; private static ScriptShape Shape(string name, params ParameterShape[] ps) => new(name, ps, null); private static ParameterShape Param(string name, string type = "String", bool required = true) => new(name, type, required); public ScriptAnalysisServiceTests() { _catalog.GetShapesAsync().Returns(Array.Empty()); _svc = new ScriptAnalysisService(_catalog, _cache); } // ── Diagnose ────────────────────────────────────────────────────────── [Fact] public void EmptyCode_NoMarkers() { var resp = _svc.Diagnose(new DiagnoseRequest("")); Assert.Empty(resp.Markers); } [Fact] public void CleanScript_NoMarkers() { var resp = _svc.Diagnose(new DiagnoseRequest("var x = 1 + 2; return x;")); Assert.Empty(resp.Markers); } [Fact] public void MissingSemicolon_ReportsRoslynDiagnostic() { var resp = _svc.Diagnose(new DiagnoseRequest("var x = 1\n")); Assert.Contains(resp.Markers, m => m.Code.StartsWith("CS")); } [Fact] public void ForbiddenUsingDirective_RaisesSCADA001() { var resp = _svc.Diagnose(new DiagnoseRequest("using System.IO;")); Assert.Contains(resp.Markers, m => m.Code == "SCADA001" && m.Message.Contains("System.IO")); } [Theory] [InlineData("System.Diagnostics")] [InlineData("System.Reflection")] [InlineData("System.Net")] public void ForbiddenUsing_AllBannedNamespaces(string ns) { var resp = _svc.Diagnose(new DiagnoseRequest($"using {ns};")); Assert.Contains(resp.Markers, m => m.Code == "SCADA001" && m.Message.Contains(ns)); } [Fact] public void ForbiddenTypeUsage_ResolvesViaSemanticModel() { var resp = _svc.Diagnose(new DiagnoseRequest( "using System.IO; var s = File.ReadAllText(\"x\");")); Assert.Contains(resp.Markers, m => m.Code == "SCADA002" && m.Message.Contains("File")); } [Fact] public void UserIdentifierNamedFile_DoesNotFalsePositive() { var resp = _svc.Diagnose(new DiagnoseRequest( "var File = \"hello\"; return File.Length;")); Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA002"); } [Fact] public void UserIdentifierNamedThread_DoesNotFalsePositive() { var resp = _svc.Diagnose(new DiagnoseRequest( "var Thread = 42; return Thread;")); Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA002"); } [Fact] public void DiagnosticsAreCached_SecondCallSkipsRecompile() { var req = new DiagnoseRequest("using System.IO;"); var first = _svc.Diagnose(req); var second = _svc.Diagnose(req); Assert.Same(first, second); } [Fact] public void DifferentCode_GetsDifferentCacheEntries() { var a = _svc.Diagnose(new DiagnoseRequest("var x = 1;")); var b = _svc.Diagnose(new DiagnoseRequest("var y = 2;")); Assert.NotSame(a, b); } [Fact] public void UnknownParameterKey_RaisesSCADA003() { var resp = _svc.Diagnose(new DiagnoseRequest( Code: "var x = Parameters[\"typo\"];", DeclaredParameters: new[] { "name", "temperature" })); Assert.Contains(resp.Markers, m => m.Code == "SCADA003" && m.Message.Contains("'typo'")); } [Fact] public void DeclaredParameterKey_NoMarker() { var resp = _svc.Diagnose(new DiagnoseRequest( Code: "var x = Parameters[\"name\"];", DeclaredParameters: new[] { "name", "temperature" })); Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA003"); } [Fact] public void ArgumentCountTooFew_RaisesSCADA004() { var siblings = new[] { Shape("Calc", Param("x"), Param("y")) }; var resp = _svc.Diagnose(new DiagnoseRequest( Code: "var r = CallScript(\"Calc\", 1);", SiblingScripts: siblings)); Assert.Contains(resp.Markers, m => m.Code == "SCADA004" && m.Message.Contains("expects 2")); } [Fact] public void ArgumentCountTooMany_RaisesSCADA004() { var siblings = new[] { Shape("Ping") }; var resp = _svc.Diagnose(new DiagnoseRequest( Code: "var r = CallScript(\"Ping\", 1, 2);", SiblingScripts: siblings)); Assert.Contains(resp.Markers, m => m.Code == "SCADA004" && m.Message.Contains("got 2")); } [Fact] public void ArgumentCountCorrect_NoMarker() { var siblings = new[] { Shape("Calc", Param("x"), Param("y")) }; var resp = _svc.Diagnose(new DiagnoseRequest( Code: "var r = CallScript(\"Calc\", 1, 2);", SiblingScripts: siblings)); Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA004"); } [Fact] public void OptionalParameter_AcceptsBothOmittedAndPresent() { var siblings = new[] { Shape("Calc", Param("x"), Param("y", required: false)) }; // Required only (1) — OK. var with1 = _svc.Diagnose(new DiagnoseRequest( Code: "var r = CallScript(\"Calc\", 1);", SiblingScripts: siblings)); Assert.DoesNotContain(with1.Markers, m => m.Code == "SCADA004"); // Both passed (2) — OK. var with2 = _svc.Diagnose(new DiagnoseRequest( Code: "var r = CallScript(\"Calc\", 1, 2);", SiblingScripts: siblings)); Assert.DoesNotContain(with2.Markers, m => m.Code == "SCADA004"); } // ── Completions ─────────────────────────────────────────────────────── [Fact] public async Task ParametersStringLiteral_ReturnsDeclaredParameterNames() { var req = new CompletionsRequest( CodeText: "var x = Parameters[\"", Line: 1, Column: 21, DeclaredParameters: new[] { "name", "temperature" }); var resp = await _svc.CompleteAsync(req); Assert.Contains(resp.Items, i => i.Label == "name" && i.Detail == "declared parameter"); Assert.Contains(resp.Items, i => i.Label == "temperature"); } [Fact] public async Task CallScriptStringLiteral_ReturnsSiblingNamesWithSnippet() { var siblings = new[] { Shape("SiblingA", Param("x")) }; var req = new CompletionsRequest( CodeText: "var x = CallScript(\"", Line: 1, Column: 21, SiblingScripts: siblings); var resp = await _svc.CompleteAsync(req); var item = Assert.Single(resp.Items, i => i.Label == "SiblingA"); Assert.Equal(4, item.InsertTextRules); Assert.Contains("${1:x}", item.InsertText); Assert.Contains("sibling script", item.Detail); } [Fact] public async Task CallSharedStringLiteral_ResolvesViaCatalogWithShapes() { _catalog.GetShapesAsync().Returns(new[] { Shape("GetWeather"), Shape("Greet", Param("name")) }); var req = new CompletionsRequest( CodeText: "var x = CallShared(\"", Line: 1, Column: 21); var resp = await _svc.CompleteAsync(req); Assert.Contains(resp.Items, i => i.Label == "GetWeather"); var greet = Assert.Single(resp.Items, i => i.Label == "Greet"); Assert.Contains("${1:name}", greet.InsertText); } [Fact] public async Task GeneralCompletion_ReturnsInScopeSymbols() { var req = new CompletionsRequest("var x = ", 1, 9); var resp = await _svc.CompleteAsync(req); Assert.Contains(resp.Items, i => i.Label == "Parameters"); Assert.Contains(resp.Items, i => i.Label == "CallShared"); } // ── Hover ───────────────────────────────────────────────────────────── [Fact] public void Hover_OnSiblingName_ReturnsSignature() { var siblings = new[] { Shape("Calc", Param("x", "Integer"), Param("y", "Float")) }; var resp = _svc.Hover(new HoverRequest( CodeText: "var r = CallScript(\"Calc\", 1, 2);", Line: 1, Column: 23, SiblingScripts: siblings)); Assert.NotNull(resp.Markdown); Assert.Contains("Calc", resp.Markdown); Assert.Contains("x: Integer", resp.Markdown); Assert.Contains("y: Float", resp.Markdown); } [Fact] public void Hover_OnUnrelatedToken_ReturnsNull() { var resp = _svc.Hover(new HoverRequest( CodeText: "var r = 1 + 2;", Line: 1, Column: 5)); Assert.Null(resp.Markdown); } // ── Signature help ──────────────────────────────────────────────────── [Fact] public void SignatureHelp_InsideCallScript_ReturnsParameterStrip() { var siblings = new[] { Shape("Calc", Param("x", "Integer"), Param("y", "Float")) }; var resp = _svc.SignatureHelp(new SignatureHelpRequest( CodeText: "var r = CallScript(\"Calc\", 1, ", Line: 1, Column: 31, SiblingScripts: siblings)); Assert.NotNull(resp.Label); Assert.Equal(2, resp.Parameters!.Count); Assert.Equal("x: Integer", resp.Parameters[0].Label); Assert.Equal("y: Float", resp.Parameters[1].Label); Assert.Equal(1, resp.ActiveParameter); } [Fact] public void SignatureHelp_OutsideCall_ReturnsNull() { var resp = _svc.SignatureHelp(new SignatureHelpRequest( CodeText: "var r = 1 + 2;", Line: 1, Column: 5)); Assert.Null(resp.Label); } }