// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerShell.EditorServices.Handlers;
using Newtonsoft.Json.Linq;
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Client;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Workspace;
using Xunit;
using Xunit.Abstractions;
using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range;
using Microsoft.PowerShell.EditorServices.Logging;
using Microsoft.PowerShell.EditorServices.Services.Configuration;
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
using Microsoft.PowerShell.EditorServices.Services.Template;

namespace PowerShellEditorServices.Test.E2E
{
    [Trait("Category", "LSP")]
    public class LanguageServerProtocolMessageTests : IClassFixture<LSPTestsFixture>, IDisposable
    {
        // Borrowed from `VersionUtils` which can't be used here due to an initialization problem.
        private static bool IsLinux { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);

        private static readonly string s_binDir =
            Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

        private readonly ILanguageClient PsesLanguageClient;
        private readonly List<Diagnostic> Diagnostics;
        private readonly List<PsesTelemetryEvent> TelemetryEvents;
        private readonly string PwshExe;

        public LanguageServerProtocolMessageTests(ITestOutputHelper output, LSPTestsFixture data)
        {
            data.Output = output;
            PsesLanguageClient = data.PsesLanguageClient;
            Diagnostics = data.Diagnostics;
            Diagnostics.Clear();
            TelemetryEvents = data.TelemetryEvents;
            TelemetryEvents.Clear();
            PwshExe = PsesStdioProcess.PwshExe;
        }

        public void Dispose()
        {
            Diagnostics.Clear();
            TelemetryEvents.Clear();
            GC.SuppressFinalize(this);
        }

        private string NewTestFile(string script, bool isPester = false, string languageId = "powershell")
        {
            string fileExt = isPester ? ".Tests.ps1" : ".ps1";
            string filePath = Path.Combine(s_binDir, Path.GetRandomFileName() + fileExt);
            File.WriteAllText(filePath, script);

            PsesLanguageClient.SendNotification("textDocument/didOpen", new DidOpenTextDocumentParams
            {
                TextDocument = new TextDocumentItem
                {
                    LanguageId = languageId,
                    Version = 0,
                    Text = script,
                    Uri = new Uri(filePath)
                }
            });

            // Give PSES a chance to run what it needs to run.
            Thread.Sleep(2000);

            return filePath;
        }

        private async Task WaitForDiagnosticsAsync()
        {
            // Wait for PSSA to finish.
            for (int i = 0; Diagnostics.Count == 0; i++)
            {
                if (i >= 10)
                {
                    throw new InvalidDataException("No diagnostics showed up after 20s.");
                }

                await Task.Delay(2000).ConfigureAwait(true);
            }
        }

        private async Task WaitForTelemetryEventsAsync()
        {
            // Wait for PSSA to finish.
            for (int i = 0; TelemetryEvents.Count == 0; i++)
            {
                if (i >= 10)
                {
                    throw new InvalidDataException("No telemetry events showed up after 20s.");
                }

                await Task.Delay(2000).ConfigureAwait(true);
            }
        }

        [Fact]
        public async Task CanSendPowerShellGetVersionRequestAsync()
        {
            PowerShellVersion details
                = await PsesLanguageClient
                    .SendRequest("powerShell/getVersion", new GetVersionParams())
                    .Returning<PowerShellVersion>(CancellationToken.None).ConfigureAwait(true);

            if (PwshExe == "powershell")
            {
                Assert.Equal("Desktop", details.Edition);
            }
            else
            {
                Assert.Equal("Core", details.Edition);
            }
        }

        [Fact]
        public async Task CanSendWorkspaceSymbolRequestAsync()
        {
            NewTestFile(@"
function CanSendWorkspaceSymbolRequest {
    Write-Host 'hello'
}
");

            Container<SymbolInformation> symbols = await PsesLanguageClient
                .SendRequest(
                    "workspace/symbol",
                    new WorkspaceSymbolParams
                    {
                        Query = "CanSendWorkspaceSymbolRequest"
                    })
                .Returning<Container<SymbolInformation>>(CancellationToken.None).ConfigureAwait(true);

            SymbolInformation symbol = Assert.Single(symbols);
            Assert.Equal("CanSendWorkspaceSymbolRequest { }", symbol.Name);
        }

        [SkippableFact]
        public async Task CanReceiveDiagnosticsFromFileOpenAsync()
        {
            Skip.If(
                PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
                "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");

            NewTestFile("$a = 4");
            await WaitForDiagnosticsAsync().ConfigureAwait(true);

            Diagnostic diagnostic = Assert.Single(Diagnostics);
            Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code);
        }

        [Fact]
        public async Task WontReceiveDiagnosticsFromFileOpenThatIsNotPowerShellAsync()
        {
            NewTestFile("$a = 4", languageId: "plaintext");
            await Task.Delay(2000).ConfigureAwait(true);

            Assert.Empty(Diagnostics);
        }

        [SkippableFact]
        public async Task CanReceiveDiagnosticsFromFileChangedAsync()
        {
            Skip.If(
                PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
                "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");

            string filePath = NewTestFile("$a = 4");
            await WaitForDiagnosticsAsync().ConfigureAwait(true);
            Diagnostics.Clear();

            PsesLanguageClient.SendNotification("textDocument/didChange", new DidChangeTextDocumentParams
            {
                // Include several content changes to test against duplicate Diagnostics showing up.
                ContentChanges = new Container<TextDocumentContentChangeEvent>(new[]
                {
                    new TextDocumentContentChangeEvent
                    {
                        Text = "$a = 5"
                    },
                    new TextDocumentContentChangeEvent
                    {
                        Text = "$a = 6"
                    },
                    new TextDocumentContentChangeEvent
                    {
                        Text = "$a = 7"
                    }
                }),
                TextDocument = new OptionalVersionedTextDocumentIdentifier
                {
                    Version = 4,
                    Uri = new Uri(filePath)
                }
            });

            await WaitForDiagnosticsAsync().ConfigureAwait(true);
            if (Diagnostics.Count > 1)
            {
                StringBuilder errorBuilder = new StringBuilder().AppendLine("Multiple diagnostics found when there should be only 1:");
                foreach (Diagnostic diag in Diagnostics)
                {
                    errorBuilder.AppendLine(diag.Message);
                }

                Assert.True(Diagnostics.Count == 1, errorBuilder.ToString());
            }

            Diagnostic diagnostic = Assert.Single(Diagnostics);
            Assert.Equal("PSUseDeclaredVarsMoreThanAssignments", diagnostic.Code);
        }

        [SkippableFact]
        public async Task CanReceiveDiagnosticsFromConfigurationChangeAsync()
        {
            Skip.If(
                PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
                "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");

            NewTestFile("gci | % { $_ }");
            await WaitForDiagnosticsAsync().ConfigureAwait(true);

            // NewTestFile doesn't clear diagnostic notifications so we need to do that for this test.
            Diagnostics.Clear();

            PsesLanguageClient.SendNotification("workspace/didChangeConfiguration",
                new DidChangeConfigurationParams
                {
                    Settings = JToken.FromObject(new LanguageServerSettingsWrapper
                    {
                        Files = new EditorFileSettings(),
                        Search = new EditorSearchSettings(),
                        Powershell = new LanguageServerSettings
                        {
                            ScriptAnalysis = new ScriptAnalysisSettings
                            {
                                Enable = false
                            }
                        }
                    })
                });

            await WaitForTelemetryEventsAsync().ConfigureAwait(true);
            PsesTelemetryEvent telemetryEvent = Assert.Single(TelemetryEvents);
            Assert.Equal("NonDefaultPsesFeatureConfiguration", telemetryEvent.EventName);
            Assert.False((bool)telemetryEvent.Data.GetValue("ScriptAnalysis"));

            // We also shouldn't get any Diagnostics because ScriptAnalysis is disabled.
            Assert.Empty(Diagnostics);

            // Clear telemetry events so we can test to make sure telemetry doesn't
            // come through with default settings.
            TelemetryEvents.Clear();

            // Restore default configuration
            PsesLanguageClient.SendNotification("workspace/didChangeConfiguration",
                new DidChangeConfigurationParams
                {
                    Settings = JToken.FromObject(new LanguageServerSettingsWrapper
                    {
                        Files = new EditorFileSettings(),
                        Search = new EditorSearchSettings(),
                        Powershell = new LanguageServerSettings()
                    })
                });

            // Wait a bit to make sure no telemetry events came through
            await Task.Delay(2000).ConfigureAwait(true);
            // Since we have default settings we should not get any telemetry events about
            Assert.Empty(TelemetryEvents.Where(e => e.EventName == "NonDefaultPsesFeatureConfiguration"));
        }

        [Fact]
        public async Task CanSendFoldingRangeRequestAsync()
        {
            string scriptPath = NewTestFile(@"gci | % {
$_

@""
    $_
""@
}");

            Container<FoldingRange> foldingRanges =
                await PsesLanguageClient
                    .SendRequest(
                        "textDocument/foldingRange",
                        new FoldingRangeRequestParam
                        {
                            TextDocument = new TextDocumentIdentifier
                            {
                                Uri = new Uri(scriptPath)
                            }
                        })
                    .Returning<Container<FoldingRange>>(CancellationToken.None).ConfigureAwait(true);

            Assert.Collection(foldingRanges.OrderBy(f => f.StartLine),
                range1 =>
                {
                    Assert.Equal(0, range1.StartLine);
                    Assert.Equal(8, range1.StartCharacter);
                    Assert.Equal(5, range1.EndLine);
                    Assert.Equal(1, range1.EndCharacter);
                },
                range2 =>
                {
                    Assert.Equal(3, range2.StartLine);
                    Assert.Equal(0, range2.StartCharacter);
                    Assert.Equal(4, range2.EndLine);
                    Assert.Equal(2, range2.EndCharacter);
                });
        }

        [SkippableFact]
        public async Task CanSendFormattingRequestAsync()
        {
            Skip.If(
                PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
                "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");

            string scriptPath = NewTestFile(@"
gci | % {
Get-Process
}

");

            TextEditContainer textEdits = await PsesLanguageClient
                .SendRequest(
                    "textDocument/formatting",
                    new DocumentFormattingParams
                    {
                        TextDocument = new TextDocumentIdentifier
                        {
                            Uri = new Uri(scriptPath)
                        },
                        Options = new FormattingOptions
                        {
                            TabSize = 4,
                            InsertSpaces = false
                        }
                    })
                .Returning<TextEditContainer>(CancellationToken.None).ConfigureAwait(true);

            TextEdit textEdit = Assert.Single(textEdits);

            // If we have a tab, formatting ran.
            Assert.Contains("\t", textEdit.NewText);
        }

        [SkippableFact]
        public async Task CanSendRangeFormattingRequestAsync()
        {
            Skip.If(
                PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
                "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");

            string scriptPath = NewTestFile(@"
gci | % {
Get-Process
}

");

            TextEditContainer textEdits = await PsesLanguageClient
                .SendRequest(
                    "textDocument/formatting",
                    new DocumentRangeFormattingParams
                    {
                        Range = new Range
                        {
                            Start = new Position
                            {
                                Line = 2,
                                Character = 0
                            },
                            End = new Position
                            {
                                Line = 3,
                                Character = 0
                            }
                        },
                        TextDocument = new TextDocumentIdentifier
                        {
                            Uri = new Uri(scriptPath)
                        },
                        Options = new FormattingOptions
                        {
                            TabSize = 4,
                            InsertSpaces = false
                        }
                    })
                .Returning<TextEditContainer>(CancellationToken.None).ConfigureAwait(true);

            TextEdit textEdit = Assert.Single(textEdits);

            // If we have a tab, formatting ran.
            Assert.Contains("\t", textEdit.NewText);
        }

        [Fact]
        public async Task CanSendDocumentSymbolRequestAsync()
        {
            string scriptPath = NewTestFile(@"
function CanSendDocumentSymbolRequest {

}

CanSendDocumentSymbolRequest
");

            SymbolInformationOrDocumentSymbolContainer symbolInformationOrDocumentSymbols =
                await PsesLanguageClient
                    .SendRequest(
                        "textDocument/documentSymbol",
                        new DocumentSymbolParams
                        {
                            TextDocument = new TextDocumentIdentifier
                            {
                                Uri = new Uri(scriptPath)
                            }
                        })
                    .Returning<SymbolInformationOrDocumentSymbolContainer>(CancellationToken.None).ConfigureAwait(true);

            Assert.Collection(symbolInformationOrDocumentSymbols,
                symInfoOrDocSym =>
                {
                    Range range = symInfoOrDocSym.SymbolInformation.Location.Range;

                    Assert.Equal(1, range.Start.Line);
                    Assert.Equal(0, range.Start.Character);
                    Assert.Equal(3, range.End.Line);
                    Assert.Equal(1, range.End.Character);
                });
        }

        [Fact]
        public async Task CanSendReferencesRequestAsync()
        {
            string scriptPath = NewTestFile(@"
function CanSendReferencesRequest {

}

CanSendReferencesRequest
");

            LocationContainer locations = await PsesLanguageClient
                .SendRequest(
                    "textDocument/references",
                    new ReferenceParams
                    {
                        TextDocument = new TextDocumentIdentifier
                        {
                            Uri = new Uri(scriptPath)
                        },
                        Position = new Position
                        {
                            Line = 5,
                            Character = 0
                        },
                        Context = new ReferenceContext
                        {
                            IncludeDeclaration = false
                        }
                    })
                .Returning<LocationContainer>(CancellationToken.None).ConfigureAwait(true);

            Assert.Collection(locations,
                location1 =>
                {
                    Range range = location1.Range;
                    Assert.Equal(1, range.Start.Line);
                    Assert.Equal(9, range.Start.Character);
                    Assert.Equal(1, range.End.Line);
                    Assert.Equal(33, range.End.Character);
                },
                location2 =>
                {
                    Range range = location2.Range;
                    Assert.Equal(5, range.Start.Line);
                    Assert.Equal(0, range.Start.Character);
                    Assert.Equal(5, range.End.Line);
                    Assert.Equal(24, range.End.Character);
                });
        }

        [Fact]
        public async Task CanSendDocumentHighlightRequestAsync()
        {
            string scriptPath = NewTestFile(@"
Write-Host 'Hello!'

Write-Host 'Goodbye'
");

            DocumentHighlightContainer documentHighlights =
                await PsesLanguageClient
                    .SendRequest(
                        "textDocument/documentHighlight",
                        new DocumentHighlightParams
                        {
                            TextDocument = new TextDocumentIdentifier
                            {
                                Uri = new Uri(scriptPath)
                            },
                            Position = new Position
                            {
                                Line = 3,
                                Character = 1
                            }
                        })
                    .Returning<DocumentHighlightContainer>(CancellationToken.None).ConfigureAwait(true);

            Assert.Collection(documentHighlights,
                documentHighlight1 =>
                {
                    Range range = documentHighlight1.Range;
                    Assert.Equal(1, range.Start.Line);
                    Assert.Equal(0, range.Start.Character);
                    Assert.Equal(1, range.End.Line);
                    Assert.Equal(10, range.End.Character);
                },
                documentHighlight2 =>
                {
                    Range range = documentHighlight2.Range;
                    Assert.Equal(3, range.Start.Line);
                    Assert.Equal(0, range.Start.Character);
                    Assert.Equal(3, range.End.Line);
                    Assert.Equal(10, range.End.Character);
                });
        }

        [Fact]
        public async Task CanSendPowerShellGetPSHostProcessesRequestAsync()
        {
            Process process = new();
            process.StartInfo.FileName = PwshExe;
            process.StartInfo.ArgumentList.Add("-NoProfile");
            process.StartInfo.ArgumentList.Add("-NoLogo");
            process.StartInfo.ArgumentList.Add("-NoExit");

            process.StartInfo.CreateNoWindow = true;
            process.StartInfo.UseShellExecute = false;

            process.StartInfo.RedirectStandardInput = true;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.RedirectStandardError = true;

            process.Start();

            // Wait for the process to start.
            Thread.Sleep(1000);

            PSHostProcessResponse[] pSHostProcessResponses = null;

            try
            {
                pSHostProcessResponses =
                    await PsesLanguageClient
                        .SendRequest(
                            "powerShell/getPSHostProcesses",
                            new GetPSHostProcesssesParams())
                        .Returning<PSHostProcessResponse[]>(CancellationToken.None).ConfigureAwait(true);
            }
            finally
            {
                process.Kill();
                process.Dispose();
            }

            Assert.NotEmpty(pSHostProcessResponses);
        }

        [Fact]
        public async Task CanSendPowerShellGetRunspaceRequestAsync()
        {
            Process process = new();
            process.StartInfo.FileName = PwshExe;
            process.StartInfo.ArgumentList.Add("-NoProfile");
            process.StartInfo.ArgumentList.Add("-NoLogo");
            process.StartInfo.ArgumentList.Add("-NoExit");

            process.StartInfo.CreateNoWindow = true;
            process.StartInfo.UseShellExecute = false;

            process.StartInfo.RedirectStandardInput = true;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.RedirectStandardError = true;

            process.Start();

            // Wait for the process to start.
            Thread.Sleep(1000);

            RunspaceResponse[] runspaceResponses = null;
            try
            {
                runspaceResponses =
                    await PsesLanguageClient
                        .SendRequest(
                            "powerShell/getRunspace",
                            new GetRunspaceParams
                            {
                                ProcessId = $"{process.Id}"
                            })
                        .Returning<RunspaceResponse[]>(CancellationToken.None).ConfigureAwait(true);
            }
            finally
            {
                process.Kill();
                process.Dispose();
            }

            Assert.NotEmpty(runspaceResponses);
        }

        [Fact]
        public async Task CanSendPesterLegacyCodeLensRequestAsync()
        {
            // Make sure LegacyCodeLens is enabled because we'll need it in this test.
            PsesLanguageClient.Workspace.DidChangeConfiguration(
                new DidChangeConfigurationParams
                {
                    Settings = JObject.Parse(@"
{
    ""powershell"": {
        ""pester"": {
            ""useLegacyCodeLens"": true,
            ""codeLens"": true
        }
    }
}
")
                });

            string filePath = NewTestFile(@"
Describe 'DescribeName' {
    Context 'ContextName' {
        It 'ItName' {
            1 | Should - Be 1
        }
    }
}
", isPester: true);

            CodeLensContainer codeLenses = await PsesLanguageClient
                .SendRequest(
                    "textDocument/codeLens",
                    new CodeLensParams
                    {
                        TextDocument = new TextDocumentIdentifier
                        {
                            Uri = new Uri(filePath)
                        }
                    })
                .Returning<CodeLensContainer>(CancellationToken.None).ConfigureAwait(true);

            Assert.Collection(codeLenses,
                codeLens1 =>
                {
                    Range range = codeLens1.Range;

                    Assert.Equal(1, range.Start.Line);
                    Assert.Equal(0, range.Start.Character);
                    Assert.Equal(7, range.End.Line);
                    Assert.Equal(1, range.End.Character);

                    Assert.Equal("Run tests", codeLens1.Command.Title);
                },
                codeLens2 =>
                {
                    Range range = codeLens2.Range;

                    Assert.Equal(1, range.Start.Line);
                    Assert.Equal(0, range.Start.Character);
                    Assert.Equal(7, range.End.Line);
                    Assert.Equal(1, range.End.Character);

                    Assert.Equal("Debug tests", codeLens2.Command.Title);
                });
        }

        [Fact]
        public async Task CanSendPesterCodeLensRequestAsync()
        {
            // Make sure Pester legacy CodeLens is disabled because we'll need it in this test.
            PsesLanguageClient.Workspace.DidChangeConfiguration(
                new DidChangeConfigurationParams
                {
                    Settings = JObject.Parse(@"
{
    ""powershell"": {
        ""pester"": {
            ""useLegacyCodeLens"": false,
            ""codeLens"": true
        }
    }
}
")
                });

            string filePath = NewTestFile(@"
Describe 'DescribeName' {
    Context 'ContextName' {
        It 'ItName' {
            1 | Should - Be 1
        }
    }
}
", isPester: true);

            CodeLensContainer codeLenses = await PsesLanguageClient
                .SendRequest(
                    "textDocument/codeLens",
                    new CodeLensParams
                    {
                        TextDocument = new TextDocumentIdentifier
                        {
                            Uri = new Uri(filePath)
                        }
                    })
                .Returning<CodeLensContainer>(CancellationToken.None).ConfigureAwait(true);

            Assert.Collection(codeLenses,
                codeLens =>
                {
                    Range range = codeLens.Range;

                    Assert.Equal(1, range.Start.Line);
                    Assert.Equal(0, range.Start.Character);
                    Assert.Equal(7, range.End.Line);
                    Assert.Equal(1, range.End.Character);

                    Assert.Equal("Run tests", codeLens.Command.Title);
                },
                codeLens =>
                {
                    Range range = codeLens.Range;

                    Assert.Equal(1, range.Start.Line);
                    Assert.Equal(0, range.Start.Character);
                    Assert.Equal(7, range.End.Line);
                    Assert.Equal(1, range.End.Character);

                    Assert.Equal("Debug tests", codeLens.Command.Title);
                },
                codeLens =>
                {
                    Range range = codeLens.Range;

                    Assert.Equal(2, range.Start.Line);
                    Assert.Equal(4, range.Start.Character);
                    Assert.Equal(6, range.End.Line);
                    Assert.Equal(5, range.End.Character);

                    Assert.Equal("Run tests", codeLens.Command.Title);
                },
                codeLens =>
                {
                    Range range = codeLens.Range;

                    Assert.Equal(2, range.Start.Line);
                    Assert.Equal(4, range.Start.Character);
                    Assert.Equal(6, range.End.Line);
                    Assert.Equal(5, range.End.Character);

                    Assert.Equal("Debug tests", codeLens.Command.Title);
                },
                codeLens =>
                {
                    Range range = codeLens.Range;

                    Assert.Equal(3, range.Start.Line);
                    Assert.Equal(8, range.Start.Character);
                    Assert.Equal(5, range.End.Line);
                    Assert.Equal(9, range.End.Character);

                    Assert.Equal("Run test", codeLens.Command.Title);
                },
                codeLens =>
                {
                    Range range = codeLens.Range;

                    Assert.Equal(3, range.Start.Line);
                    Assert.Equal(8, range.Start.Character);
                    Assert.Equal(5, range.End.Line);
                    Assert.Equal(9, range.End.Character);

                    Assert.Equal("Debug test", codeLens.Command.Title);
                });
        }

        [Fact]
        public async Task NoMessageIfPesterCodeLensDisabled()
        {
            // Make sure Pester legacy CodeLens is disabled because we'll need it in this test.
            PsesLanguageClient.Workspace.DidChangeConfiguration(
                new DidChangeConfigurationParams
                {
                    Settings = JObject.Parse(@"
{
    ""powershell"": {
        ""pester"": {
            ""codeLens"": false
        }
    }
}
")
                });

            string filePath = NewTestFile(@"
Describe 'DescribeName' {
    Context 'ContextName' {
        It 'ItName' {
            1 | Should - Be 1
        }
    }
}
", isPester: true);

            CodeLensContainer codeLenses = await PsesLanguageClient
                .SendRequest(
                    "textDocument/codeLens",
                    new CodeLensParams
                    {
                        TextDocument = new TextDocumentIdentifier
                        {
                            Uri = new Uri(filePath)
                        }
                    })
                .Returning<CodeLensContainer>(CancellationToken.None).ConfigureAwait(true);

            Assert.Empty(codeLenses);
        }

        [Fact]
        public async Task CanSendReferencesCodeLensRequestAsync()
        {
            string filePath = NewTestFile(@"
function CanSendReferencesCodeLensRequest {

}

CanSendReferencesCodeLensRequest
");

            CodeLensContainer codeLenses = await PsesLanguageClient
                .SendRequest(
                    "textDocument/codeLens",
                    new CodeLensParams
                    {
                        TextDocument = new TextDocumentIdentifier
                        {
                            Uri = new Uri(filePath)
                        }
                    })
                .Returning<CodeLensContainer>(CancellationToken.None).ConfigureAwait(true);

            CodeLens codeLens = Assert.Single(codeLenses);

            Range range = codeLens.Range;
            Assert.Equal(1, range.Start.Line);
            Assert.Equal(0, range.Start.Character);
            Assert.Equal(3, range.End.Line);
            Assert.Equal(1, range.End.Character);

            CodeLens codeLensResolveResult = await PsesLanguageClient
                .SendRequest("codeLens/resolve", codeLens)
                .Returning<CodeLens>(CancellationToken.None).ConfigureAwait(true);

            Assert.Equal("1 reference", codeLensResolveResult.Command.Title);
        }

        [SkippableFact]
        public async Task CanSendCodeActionRequestAsync()
        {
            Skip.If(
                PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
                "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");

            string filePath = NewTestFile("gci");
            await WaitForDiagnosticsAsync().ConfigureAwait(true);

            CommandOrCodeActionContainer commandOrCodeActions =
                await PsesLanguageClient
                    .SendRequest(
                        "textDocument/codeAction",
                        new CodeActionParams
                        {
                            TextDocument = new TextDocumentIdentifier(
                                new Uri(filePath, UriKind.Absolute)),
                            Range = new Range
                            {
                                Start = new Position
                                {
                                    Line = 0,
                                    Character = 0
                                },
                                End = new Position
                                {
                                    Line = 0,
                                    Character = 3
                                }
                            },
                            Context = new CodeActionContext
                            {
                                Diagnostics = new Container<Diagnostic>(Diagnostics)
                            }
                        })
                    .Returning<CommandOrCodeActionContainer>(CancellationToken.None).ConfigureAwait(true);

            Assert.Collection(commandOrCodeActions,
                command =>
                {
                    Assert.Equal(
                        "Replace gci with Get-ChildItem",
                        command.CodeAction.Title);
                    Assert.Equal(
                        CodeActionKind.QuickFix,
                        command.CodeAction.Kind);
                    Assert.Single(command.CodeAction.Edit.DocumentChanges);
                },
                command =>
                {
                    Assert.Equal(
                        "PowerShell.ShowCodeActionDocumentation",
                        command.CodeAction.Command.Name);
                });
        }

        [SkippableFact]
        public async Task CanSendCompletionAndCompletionResolveRequestAsync()
        {
            Skip.If(IsLinux, "This depends on the help system, which is flaky on Linux.");
            string filePath = NewTestFile("Write-H");

            CompletionList completionItems = await PsesLanguageClient.TextDocument.RequestCompletion(
                new CompletionParams
                {
                    TextDocument = new TextDocumentIdentifier
                    {
                        Uri = DocumentUri.FromFileSystemPath(filePath)
                    },
                    Position = new Position(line: 0, character: 7)
                });

            CompletionItem completionItem = Assert.Single(completionItems,
                completionItem1 => completionItem1.FilterText == "Write-Host");

            CompletionItem updatedCompletionItem = await PsesLanguageClient
                .SendRequest("completionItem/resolve", completionItem)
                .Returning<CompletionItem>(CancellationToken.None).ConfigureAwait(true);

            Assert.Contains("Writes customized output to a host", updatedCompletionItem.Documentation.String);
        }

        [SkippableFact(Skip = "This test is too flaky right now.")]
        public async Task CanSendCompletionResolveWithModulePrefixRequestAsync()
        {
            await PsesLanguageClient
                .SendRequest(
                    "evaluate",
                    new EvaluateRequestArguments
                    {
                        Expression = "Import-Module Microsoft.PowerShell.Archive -Prefix Slow"
                    })
                .ReturningVoid(CancellationToken.None).ConfigureAwait(true);

            string filePath = NewTestFile("Expand-SlowArch");

            CompletionList completionItems = await PsesLanguageClient.TextDocument.RequestCompletion(
                new CompletionParams
                {
                    TextDocument = new TextDocumentIdentifier
                    {
                        Uri = DocumentUri.FromFileSystemPath(filePath)
                    },
                    Position = new Position(line: 0, character: 15)
                });

            CompletionItem completionItem = Assert.Single(completionItems,
                completionItem1 => completionItem1.Label == "Expand-SlowArchive");

            CompletionItem updatedCompletionItem = await PsesLanguageClient
                .SendRequest("completionItem/resolve", completionItem)
                .Returning<CompletionItem>(CancellationToken.None).ConfigureAwait(true);

            Assert.Contains("Extracts files from a specified archive", updatedCompletionItem.Documentation.String);
        }

        [SkippableFact]
        public async Task CanSendHoverRequestAsync()
        {
            Skip.If(IsLinux, "This depends on the help system, which is flaky on Linux.");
            string filePath = NewTestFile("Write-Host");

            Hover hover = await PsesLanguageClient.TextDocument.RequestHover(
                new HoverParams
                {
                    TextDocument = new TextDocumentIdentifier
                    {
                        Uri = DocumentUri.FromFileSystemPath(filePath)
                    },
                    Position = new Position(line: 0, character: 1)
                }).ConfigureAwait(true);

            Assert.True(hover.Contents.HasMarkedStrings);
            Assert.Collection(hover.Contents.MarkedStrings,
                str1 => Assert.Equal("function Write-Host", str1.Value),
                str2 =>
                {
                    Assert.Equal("markdown", str2.Language);
                    Assert.Equal("Writes customized output to a host.", str2.Value);
                });
        }

        [Fact]
        public async Task CanSendSignatureHelpRequestAsync()
        {
            string filePath = NewTestFile("Get-Date ");

            SignatureHelp signatureHelp = await PsesLanguageClient
                .SendRequest(
                    "textDocument/signatureHelp",
                    new SignatureHelpParams
                    {
                        TextDocument = new TextDocumentIdentifier
                        {
                            Uri = new Uri(filePath)
                        },
                        Position = new Position
                        {
                            Line = 0,
                            Character = 9
                        }
                    })
                .Returning<SignatureHelp>(CancellationToken.None).ConfigureAwait(true);

            Assert.Contains("Get-Date", signatureHelp.Signatures.First().Label);
        }

        [Fact]
        public async Task CanSendDefinitionRequestAsync()
        {
            string scriptPath = NewTestFile(@"
function CanSendDefinitionRequest {

}

CanSendDefinitionRequest
");

            LocationOrLocationLinks locationOrLocationLinks =
                await PsesLanguageClient
                    .SendRequest(
                        "textDocument/definition",
                        new DefinitionParams
                        {
                            TextDocument = new TextDocumentIdentifier { Uri = new Uri(scriptPath) },
                            Position = new Position { Line = 5, Character = 2 }
                        })
                    .Returning<LocationOrLocationLinks>(CancellationToken.None).ConfigureAwait(true);

            LocationOrLocationLink locationOrLocationLink =
                    Assert.Single(locationOrLocationLinks);

            Assert.Equal(1, locationOrLocationLink.Location.Range.Start.Line);
            Assert.Equal(9, locationOrLocationLink.Location.Range.Start.Character);
            Assert.Equal(1, locationOrLocationLink.Location.Range.End.Line);
            Assert.Equal(33, locationOrLocationLink.Location.Range.End.Character);
        }

        [SkippableFact]
        public async Task CanSendGetProjectTemplatesRequestAsync()
        {
            Skip.If(PsesStdioProcess.RunningInConstrainedLanguageMode, "Plaster doesn't work in ConstrainedLanguage mode.");

            GetProjectTemplatesResponse getProjectTemplatesResponse =
                await PsesLanguageClient
                    .SendRequest(
                        "powerShell/getProjectTemplates",
                        new GetProjectTemplatesRequest
                        {
                            IncludeInstalledModules = true
                        })
                    .Returning<GetProjectTemplatesResponse>(CancellationToken.None).ConfigureAwait(true);

            Assert.Contains(getProjectTemplatesResponse.Templates, t => t.Title is "AddPSScriptAnalyzerSettings");
            Assert.Contains(getProjectTemplatesResponse.Templates, t => t.Title is "New PowerShell Manifest Module");
        }

        [SkippableFact]
        public async Task CanSendGetCommentHelpRequestAsync()
        {
            Skip.If(
                PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
                "Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");

            string scriptPath = NewTestFile(@"
function CanSendGetCommentHelpRequest {
    param(
        $myParam,
        $myOtherParam,
        $yetAnotherParam
    )

    # Include other problematic code to make sure this still works
    gci
}
");

            CommentHelpRequestResult commentHelpRequestResult =
                await PsesLanguageClient
                    .SendRequest(
                        "powerShell/getCommentHelp",
                        new CommentHelpRequestParams
                        {
                            DocumentUri = new Uri(scriptPath).ToString(),
                            BlockComment = false,
                            TriggerPosition = new Position
                            {
                                Line = 0,
                                Character = 0
                            }
                        })
                    .Returning<CommentHelpRequestResult>(CancellationToken.None).ConfigureAwait(true);

            Assert.NotEmpty(commentHelpRequestResult.Content);
            Assert.Contains("myParam", commentHelpRequestResult.Content[7]);
        }

        [Fact]
        public async Task CanSendEvaluateRequestAsync()
        {
            using CancellationTokenSource cancellationSource = new(millisecondsDelay: 5000);

            EvaluateResponseBody evaluateResponseBody =
                await PsesLanguageClient
                    .SendRequest(
                        "evaluate",
                        new EvaluateRequestArguments
                        {
                            Expression = "Get-ChildItem"
                        })
                    .Returning<EvaluateResponseBody>(cancellationSource.Token).ConfigureAwait(true);

            // These always gets returned so this test really just makes sure we get _any_ response.
            Assert.Equal("", evaluateResponseBody.Result);
            Assert.Equal(0, evaluateResponseBody.VariablesReference);
        }

        [Fact]
        public async Task CanSendGetCommandRequestAsync()
        {
            List<object> pSCommandMessages =
                await PsesLanguageClient
                    .SendRequest("powerShell/getCommand", new GetCommandParams())
                    .Returning<List<object>>(CancellationToken.None).ConfigureAwait(true);

            Assert.NotEmpty(pSCommandMessages);
            // There should be at least 20 commands or so.
            Assert.True(pSCommandMessages.Count > 20);
        }

        [SkippableFact]
        public async Task CanSendExpandAliasRequestAsync()
        {
            Skip.If(
                PsesStdioProcess.RunningInConstrainedLanguageMode,
                "This feature currently doesn't support ConstrainedLanguage Mode.");

            ExpandAliasResult expandAliasResult =
                await PsesLanguageClient
                    .SendRequest(
                        "powerShell/expandAlias",
                        new ExpandAliasParams
                        {
                            Text = "gci"
                        })
                    .Returning<ExpandAliasResult>(CancellationToken.None).ConfigureAwait(true);

            Assert.Equal("Get-ChildItem", expandAliasResult.Text);
        }

        [Fact]
        public async Task CanSendSemanticTokenRequestAsync()
        {
            const string scriptContent = "function";
            string scriptPath = NewTestFile(scriptContent);

            SemanticTokens result =
                await PsesLanguageClient
                    .SendRequest(
                        "textDocument/semanticTokens/full",
                        new SemanticTokensParams
                        {
                            TextDocument = new TextDocumentIdentifier
                            {
                                Uri = new Uri(scriptPath)
                            }
                        })
                    .Returning<SemanticTokens>(CancellationToken.None).ConfigureAwait(true);

            // More information about how this data is generated can be found at
            // https://github.com/microsoft/vscode-extension-samples/blob/5ae1f7787122812dcc84e37427ca90af5ee09f14/semantic-tokens-sample/vscode.proposed.d.ts#L71
            int[] expectedArr = new int[5]
                {
                    // line, index, token length, token type, token modifiers
                    0, 0, scriptContent.Length, 1, 0 //function token: line 0, index 0, length of script, type 1 = keyword, no modifiers
                };

            Assert.Equal(expectedArr, result.Data.ToArray());
        }
    }
}