using Microsoft.Extensions.Caching.Memory;
using NSubstitute;
using ScadaLink.CentralUI.ScriptAnalysis;
using ScadaLink.TemplateEngine;

namespace ScadaLink.CentralUI.Tests.ScriptAnalysis;

/// <summary>
/// Regression tests for CentralUI-013. <c>ResolveCalledShape</c> resolved shared
/// script shapes with <c>_sharedScripts.GetShapesAsync().GetAwaiter().GetResult()</c>
/// — a sync-over-async block on a request thread that risks thread-pool
/// starvation and deadlock. <c>Hover</c> and <c>SignatureHelp</c> were synchronous
/// purely to accommodate that block. The fix makes both methods async and
/// <c>await</c>s the catalog.
/// </summary>
public class ScriptAnalysisAsyncResolveTests
{
    private readonly ISharedScriptCatalog _catalog = Substitute.For<ISharedScriptCatalog>();
    private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 });
    private readonly IServiceProvider _services = Substitute.For<IServiceProvider>();
    private readonly ScriptAnalysisService _svc;

    public ScriptAnalysisAsyncResolveTests()
    {
        _catalog.GetShapesAsync().Returns(Array.Empty<ScriptShape>());
        _svc = new ScriptAnalysisService(_catalog, _cache, _services);
    }

    private static ScriptShape Shape(string name, params ParameterShape[] ps) => new(name, ps, null);
    private static ParameterShape Param(string name, string type) => new(name, type, true);

    [Fact]
    public async Task HoverAsync_OnSharedCallName_AwaitsCatalog_AndResolvesShape()
    {
        // The catalog only completes after yielding — a truly asynchronous
        // source. The fixed Hover awaits it instead of blocking.
        _catalog.GetShapesAsync().Returns(async _ =>
        {
            await Task.Yield();
            return (IReadOnlyList<ScriptShape>)new[]
            {
                Shape("Aggregate", Param("window", "Integer")),
            };
        });

        var resp = await _svc.Hover(new HoverRequest(
            CodeText: "var r = Scripts.CallShared(\"Aggregate\");",
            Line: 1,
            Column: 30));

        Assert.NotNull(resp.Markdown);
        Assert.Contains("shared script", resp.Markdown);
        Assert.Contains("Aggregate", resp.Markdown);
    }

    [Fact]
    public async Task SignatureHelpAsync_InsideSharedCall_AwaitsCatalog_AndResolvesParameters()
    {
        _catalog.GetShapesAsync().Returns(async _ =>
        {
            await Task.Yield();
            return (IReadOnlyList<ScriptShape>)new[]
            {
                Shape("Aggregate", Param("window", "Integer"), Param("mode", "String")),
            };
        });

        var resp = await _svc.SignatureHelp(new SignatureHelpRequest(
            CodeText: "var r = Scripts.CallShared(\"Aggregate\", ",
            Line: 1,
            Column: 41));

        Assert.NotNull(resp.Label);
        Assert.Contains("Aggregate", resp.Label!);
        Assert.Equal(2, resp.Parameters!.Count);
    }

    [Fact]
    public void HoverAndSignatureHelp_AreAsync_NotSyncOverAsync()
    {
        // Structural guard: the methods must return Task so the catalog can be
        // awaited rather than blocked with .GetAwaiter().GetResult().
        var hover = typeof(ScriptAnalysisService).GetMethod(nameof(ScriptAnalysisService.Hover));
        var sigHelp = typeof(ScriptAnalysisService).GetMethod(nameof(ScriptAnalysisService.SignatureHelp));

        Assert.NotNull(hover);
        Assert.NotNull(sigHelp);
        Assert.Equal(typeof(Task<HoverResponse>), hover!.ReturnType);
        Assert.Equal(typeof(Task<SignatureHelpResponse>), sigHelp!.ReturnType);
    }
}
