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<ISharedScriptCatalog>();
    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<ScriptShape>());
        _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);
    }

    // ── Format ────────────────────────────────────────────────────────────

    [Fact]
    public void Format_ScrambledCode_ReturnsPrettyPrinted()
    {
        var resp = _svc.Format(new FormatRequest("if(x){return 1;}else{return 2;}"));
        // Roslyn's default formatter adds spaces around keywords/braces.
        Assert.Contains("if (x)", resp.Code);
        Assert.NotEqual("if(x){return 1;}else{return 2;}", resp.Code);
    }

    [Fact]
    public void Format_EmptyCode_ReturnsEmpty()
    {
        Assert.Equal("", _svc.Format(new FormatRequest("")).Code);
    }

    // ── Inlay hints ───────────────────────────────────────────────────────

    [Fact]
    public void InlayHints_OnCallScript_EmitsParameterLabels()
    {
        var siblings = new[] { Shape("Calc", Param("x"), Param("y")) };
        var resp = _svc.InlayHints(new InlayHintsRequest(
            Code: "var r = CallScript(\"Calc\", 1, 2);",
            SiblingScripts: siblings));
        Assert.Equal(2, resp.Hints.Count);
        Assert.Equal("x:", resp.Hints[0].Label);
        Assert.Equal("y:", resp.Hints[1].Label);
    }

    [Fact]
    public void InlayHints_OnUnknownSibling_Skipped()
    {
        var resp = _svc.InlayHints(new InlayHintsRequest(
            Code: "var r = CallScript(\"NotKnown\", 1, 2);",
            SiblingScripts: Array.Empty<ScriptShape>()));
        Assert.Empty(resp.Hints);
    }

    // ── Argument-type diagnostic (SCADA005) ───────────────────────────────

    [Fact]
    public void ArgumentTypeMismatch_StringExpectedIntegerGiven()
    {
        var siblings = new[] { Shape("Greet", Param("name", "String")) };
        var resp = _svc.Diagnose(new DiagnoseRequest(
            Code: "var r = CallScript(\"Greet\", 42);",
            SiblingScripts: siblings));
        Assert.Contains(resp.Markers, m => m.Code == "SCADA005" && m.Message.Contains("String"));
    }

    [Fact]
    public void ArgumentTypeMismatch_IntegerExpectedStringGiven()
    {
        var siblings = new[] { Shape("Calc", Param("n", "Integer")) };
        var resp = _svc.Diagnose(new DiagnoseRequest(
            Code: "var r = CallScript(\"Calc\", \"oops\");",
            SiblingScripts: siblings));
        Assert.Contains(resp.Markers, m => m.Code == "SCADA005");
    }

    [Fact]
    public void ArgumentType_FloatAcceptsInteger()
    {
        var siblings = new[] { Shape("Calc", Param("ratio", "Float")) };
        var resp = _svc.Diagnose(new DiagnoseRequest(
            Code: "var r = CallScript(\"Calc\", 1);",
            SiblingScripts: siblings));
        Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA005");
    }

    [Fact]
    public void ArgumentType_ObjectAcceptsAnyLiteral()
    {
        var siblings = new[] { Shape("Log", Param("v", "Object")) };
        var resp = _svc.Diagnose(new DiagnoseRequest(
            Code: "var r = CallScript(\"Log\", 1); CallScript(\"Log\", \"x\"); CallScript(\"Log\", true);",
            SiblingScripts: siblings));
        Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA005");
    }

    [Fact]
    public void ArgumentType_NonLiteralExpression_SkipsCheck()
    {
        var siblings = new[] { Shape("Calc", Param("n", "Integer")) };
        var resp = _svc.Diagnose(new DiagnoseRequest(
            Code: "var x = \"hi\"; var r = CallScript(\"Calc\", x);",
            SiblingScripts: siblings));
        Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA005");
    }

    // ── Self / Children / Parent attribute completions ────────────────────

    private static AttributeShape Attr(string name, string type = "String") => new(name, type);
    private static CompositionContext Comp(string name, AttributeShape[]? attrs = null, ScriptShape[]? scripts = null)
        => new(name, attrs ?? Array.Empty<AttributeShape>(), scripts ?? Array.Empty<ScriptShape>());

    [Fact]
    public async Task SelfAttribute_Literal_ReturnsSelfAttributeNames()
    {
        var req = new CompletionsRequest(
            CodeText: "var x = Attributes[\"",
            Line: 1,
            Column: 21,
            SelfAttributes: new[] { Attr("Temperature"), Attr("Setpoint", "Float") });
        var resp = await _svc.CompleteAsync(req);
        Assert.Contains(resp.Items, i => i.Label == "Temperature");
        Assert.Contains(resp.Items, i => i.Label == "Setpoint" && i.Detail.Contains("Float"));
    }

    [Fact]
    public async Task ChildAttribute_Literal_ReturnsChildAttributeNames()
    {
        var req = new CompletionsRequest(
            CodeText: "var x = Children[\"TempSensor\"].Attributes[\"",
            Line: 1,
            Column: 44,
            Children: new[] { Comp("TempSensor", attrs: new[] { Attr("Temperature"), Attr("Humidity") }) });
        var resp = await _svc.CompleteAsync(req);
        Assert.Contains(resp.Items, i => i.Label == "Temperature");
        Assert.Contains(resp.Items, i => i.Label == "Humidity");
    }

    [Fact]
    public async Task ParentAttribute_Literal_ReturnsParentAttributeNames()
    {
        var req = new CompletionsRequest(
            CodeText: "var x = Parent.Attributes[\"",
            Line: 1,
            Column: 28,
            Parent: Comp("Motor", attrs: new[] { Attr("SpeedRPM") }));
        var resp = await _svc.CompleteAsync(req);
        Assert.Contains(resp.Items, i => i.Label == "SpeedRPM");
    }

    [Fact]
    public async Task ChildrenLiteral_ReturnsCompositionNames()
    {
        var req = new CompletionsRequest(
            CodeText: "var x = Children[\"",
            Line: 1,
            Column: 19,
            Children: new[] { Comp("TempSensor"), Comp("PressureSensor") });
        var resp = await _svc.CompleteAsync(req);
        Assert.Contains(resp.Items, i => i.Label == "TempSensor" && i.Detail == "composition");
        Assert.Contains(resp.Items, i => i.Label == "PressureSensor");
    }

    [Fact]
    public void UnknownSelfAttribute_RaisesSCADA006()
    {
        var resp = _svc.Diagnose(new DiagnoseRequest(
            Code: "var x = Attributes[\"Typo\"];",
            SelfAttributes: new[] { Attr("Temperature") }));
        Assert.Contains(resp.Markers, m => m.Code == "SCADA006" && m.Message.Contains("Typo"));
    }

    [Fact]
    public void KnownSelfAttribute_NoMarker()
    {
        var resp = _svc.Diagnose(new DiagnoseRequest(
            Code: "var x = Attributes[\"Temperature\"];",
            SelfAttributes: new[] { Attr("Temperature") }));
        Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA006");
    }

    [Fact]
    public void UnknownChildAttribute_RaisesSCADA006()
    {
        var resp = _svc.Diagnose(new DiagnoseRequest(
            Code: "var x = Children[\"TempSensor\"].Attributes[\"Typo\"];",
            Children: new[] { Comp("TempSensor", attrs: new[] { Attr("Temperature") }) }));
        Assert.Contains(resp.Markers, m => m.Code == "SCADA006" && m.Message.Contains("TempSensor"));
    }

    [Fact]
    public void UnknownComposition_RaisesSCADA007()
    {
        var resp = _svc.Diagnose(new DiagnoseRequest(
            Code: "var x = Children[\"Unknown\"].Attributes[\"X\"];",
            Children: new[] { Comp("TempSensor") }));
        Assert.Contains(resp.Markers, m => m.Code == "SCADA007" && m.Message.Contains("Unknown"));
    }

    [Fact]
    public async Task ChildrenCallScript_ReturnsChildScripts()
    {
        var req = new CompletionsRequest(
            CodeText: "var x = Children[\"TempSensor\"].CallScript(\"",
            Line: 1,
            Column: 44,
            Children: new[]
            {
                Comp("TempSensor", scripts: new[] { Shape("Sample", Param("count", "Integer")) })
            });
        var resp = await _svc.CompleteAsync(req);
        var sample = Assert.Single(resp.Items, i => i.Label == "Sample");
        Assert.Contains("script on TempSensor", sample.Detail);
        Assert.Contains("${1:count}", sample.InsertText);
    }

    [Fact]
    public async Task ParentCallScript_ReturnsParentScripts()
    {
        var req = new CompletionsRequest(
            CodeText: "var x = Parent.CallScript(\"",
            Line: 1,
            Column: 28,
            Parent: Comp("Motor", scripts: new[] { Shape("Trip") }));
        var resp = await _svc.CompleteAsync(req);
        Assert.Contains(resp.Items, i => i.Label == "Trip" && i.Detail.Contains("parent script"));
    }

    // ── Hover on Parameters["name"] ───────────────────────────────────────

    [Fact]
    public void Hover_OnParametersKey_ShowsDeclaredType()
    {
        var resp = _svc.Hover(new HoverRequest(
            CodeText: "var x = Parameters[\"name\"];",
            Line: 1,
            Column: 22,
            DeclaredParameters: new[] { new ParameterShape("name", "String", true) }));
        Assert.NotNull(resp.Markdown);
        Assert.Contains("name", resp.Markdown);
        Assert.Contains("String", resp.Markdown);
    }
}
