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

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.PowerShell.EditorServices.Services;
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
using Microsoft.PowerShell.EditorServices.Services.PowerShellContext;
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
using Microsoft.PowerShell.EditorServices.Test.Shared;
using Microsoft.PowerShell.EditorServices.Utility;
using Xunit;

namespace Microsoft.PowerShell.EditorServices.Test.Debugging
{
    public class DebugServiceTests : IDisposable
    {
        private WorkspaceService workspace;
        private DebugService debugService;
        private ScriptFile debugScriptFile;
        private ScriptFile variableScriptFile;
        private PowerShellContextService powerShellContext;

        private AsyncQueue<DebuggerStoppedEventArgs> debuggerStoppedQueue =
            new AsyncQueue<DebuggerStoppedEventArgs>();
        private AsyncQueue<SessionStateChangedEventArgs> sessionStateQueue =
            new AsyncQueue<SessionStateChangedEventArgs>();

        private ScriptFile GetDebugScript(string fileName)
        {
            return this.workspace.GetFile(
                TestUtilities.NormalizePath(Path.Combine(
                    Path.GetDirectoryName(typeof(DebugServiceTests).Assembly.Location),
                    "../../../../PowerShellEditorServices.Test.Shared/Debugging",
                    fileName
                )));
        }

        public DebugServiceTests()
        {
            var logger = NullLogger.Instance;

            this.powerShellContext = PowerShellContextFactory.Create(logger);
            this.powerShellContext.SessionStateChanged += powerShellContext_SessionStateChanged;

            this.workspace = new WorkspaceService(NullLoggerFactory.Instance);

            // Load the test debug files
            this.debugScriptFile = GetDebugScript("DebugTest.ps1");
            this.variableScriptFile = GetDebugScript("VariableTest.ps1");

            this.debugService = new DebugService(
                this.powerShellContext,
                null,
                new BreakpointService(
                    NullLoggerFactory.Instance,
                    powerShellContext,
                    new DebugStateService()),
                NullLoggerFactory.Instance);

            this.debugService.DebuggerStopped += debugService_DebuggerStopped;
            this.debugService.BreakpointUpdated += debugService_BreakpointUpdated;
        }

        async void powerShellContext_SessionStateChanged(object sender, SessionStateChangedEventArgs e)
        {
            // Skip all transitions except those back to 'Ready'
            if (e.NewSessionState == PowerShellContextState.Ready)
            {
                await this.sessionStateQueue.EnqueueAsync(e).ConfigureAwait(false);
            }
        }

        void debugService_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e)
        {
            // TODO: Needed?
        }

        void debugService_DebuggerStopped(object sender, DebuggerStoppedEventArgs e)
        {
            // We need to ensure this is run on a different thread than the one it's
            // called on because it can cause PowerShellContext.OnDebuggerStopped to
            // never hit the while loop.
            Task.Run(() => this.debuggerStoppedQueue.Enqueue(e));
        }

        public void Dispose()
        {
            this.powerShellContext.Close();
        }

        [Trait("Category", "DebugService")]
        [Fact]
        // This regression test asserts that `ExecuteScriptWithArgsAsync` works for both script
        // files and, in this case, in-line scripts (commands). The bug was that the cwd was
        // erroneously prepended when the script argument was a command.
        public async Task DebuggerAcceptsInlineScript()
        {
            await this.debugService.SetCommandBreakpointsAsync(
                new[] { CommandBreakpointDetails.Create("Get-Random") }).ConfigureAwait(false);

            Task executeTask =
                this.powerShellContext.ExecuteScriptWithArgsAsync(
                    "Get-Random", string.Join(" ", "-Maximum", "100"));

            await this.AssertDebuggerStopped("", 1).ConfigureAwait(false);
            this.debugService.Continue();
            await executeTask.ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();
            Assert.Equal(StackFrameDetails.NoFileScriptPath, stackFrames[0].ScriptPath);

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            var var = variables.FirstOrDefault(v => v.Name == "$Error");
            Assert.NotNull(var);
            Assert.True(var.IsExpandable);
            Assert.Equal("[ArrayList: 0]", var.ValueString);
        }

        public static IEnumerable<object[]> DebuggerAcceptsScriptArgsTestData
        {
            get
            {
                var data = new[]
                {
                    new[] { new []{ "Foo -Param2 @('Bar','Baz') -Force Extra1" }},
                    new[] { new []{ "Foo", "-Param2", "@('Bar','Baz')", "-Force", "Extra1" }},
                };

                return data;
            }
        }

        [Trait("Category", "DebugService")]
        [Theory]
        [MemberData(nameof(DebuggerAcceptsScriptArgsTestData))]
        public async Task DebuggerAcceptsScriptArgs(string[] args)
        {
            // The path is intentionally odd (some escaped chars but not all) because we are testing
            // the internal path escaping mechanism - it should escape certains chars ([, ] and space) but
            // it should not escape already escaped chars.
            ScriptFile debugWithParamsFile = GetDebugScript("Debug W&ith Params [Test].ps1");

            await this.debugService.SetLineBreakpointsAsync(
                debugWithParamsFile,
                new[] { BreakpointDetails.Create(debugWithParamsFile.FilePath, 3) }).ConfigureAwait(false);

            string arguments = string.Join(" ", args);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptWithArgsAsync(
                    debugWithParamsFile.FilePath, arguments);

            await this.AssertDebuggerStopped(debugWithParamsFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            var var = variables.FirstOrDefault(v => v.Name == "$Param1");
            Assert.NotNull(var);
            Assert.Equal("\"Foo\"", var.ValueString);
            Assert.False(var.IsExpandable);

            var = variables.FirstOrDefault(v => v.Name == "$Param2");
            Assert.NotNull(var);
            Assert.True(var.IsExpandable);

            var childVars = debugService.GetVariables(var.Id);
            Assert.Equal(9, childVars.Length);
            Assert.Equal("\"Bar\"", childVars[0].ValueString);
            Assert.Equal("\"Baz\"", childVars[1].ValueString);

            var = variables.FirstOrDefault(v => v.Name == "$Force");
            Assert.NotNull(var);
            Assert.Equal("True", var.ValueString);
            Assert.True(var.IsExpandable);

            var = variables.FirstOrDefault(v => v.Name == "$args");
            Assert.NotNull(var);
            Assert.True(var.IsExpandable);

            childVars = debugService.GetVariables(var.Id);
            Assert.Equal(8, childVars.Length);
            Assert.Equal("\"Extra1\"", childVars[0].ValueString);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerSetsAndClearsFunctionBreakpoints()
        {
            CommandBreakpointDetails[] breakpoints =
                await this.debugService.SetCommandBreakpointsAsync(
                    new[] {
                        CommandBreakpointDetails.Create("Write-Host"),
                        CommandBreakpointDetails.Create("Get-Date")
                    }).ConfigureAwait(false);

            Assert.Equal(2, breakpoints.Length);
            Assert.Equal("Write-Host", breakpoints[0].Name);
            Assert.Equal("Get-Date", breakpoints[1].Name);

            breakpoints =
                await this.debugService.SetCommandBreakpointsAsync(
                    new[] { CommandBreakpointDetails.Create("Get-Host") }).ConfigureAwait(false);

            Assert.Single(breakpoints);
            Assert.Equal("Get-Host", breakpoints[0].Name);

            breakpoints =
                await this.debugService.SetCommandBreakpointsAsync(
                    Array.Empty<CommandBreakpointDetails>()).ConfigureAwait(false);

            Assert.Empty(breakpoints);

            // Abort debugger
            this.debugService.Abort();
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerStopsOnFunctionBreakpoints()
        {
            CommandBreakpointDetails[] breakpoints =
                await this.debugService.SetCommandBreakpointsAsync(
                    new[] {
                        CommandBreakpointDetails.Create("Write-Host")
                    }).ConfigureAwait(false);

            Task executeTask =
                this.powerShellContext.ExecuteScriptWithArgsAsync(
                    this.debugScriptFile.FilePath);

            // Wait for function breakpoint to hit
            await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();
            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            // Verify the function breakpoint broke at Write-Host and $i is 1
            var i = variables.FirstOrDefault(v => v.Name == "$i");
            Assert.NotNull(i);
            Assert.False(i.IsExpandable);
            Assert.Equal("1", i.ValueString);

            // The function breakpoint should fire the next time through the loop.
            this.debugService.Continue();
            await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6).ConfigureAwait(false);

            stackFrames = debugService.GetStackFrames();
            variables = debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            // Verify the function breakpoint broke at Write-Host and $i is 1
            i = variables.FirstOrDefault(v => v.Name == "$i");
            Assert.NotNull(i);
            Assert.False(i.IsExpandable);
            Assert.Equal("2", i.ValueString);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerSetsAndClearsLineBreakpoints()
        {
            BreakpointDetails[] breakpoints =
                await this.debugService.SetLineBreakpointsAsync(
                    this.debugScriptFile,
                    new[] {
                        BreakpointDetails.Create(this.debugScriptFile.FilePath, 5),
                        BreakpointDetails.Create(this.debugScriptFile.FilePath, 10)
                    }).ConfigureAwait(false);

            var confirmedBreakpoints = await this.GetConfirmedBreakpoints(this.debugScriptFile).ConfigureAwait(false);

            Assert.Equal(2, confirmedBreakpoints.Count());
            Assert.Equal(5, breakpoints[0].LineNumber);
            Assert.Equal(10, breakpoints[1].LineNumber);

            breakpoints =
                await this.debugService.SetLineBreakpointsAsync(
                    this.debugScriptFile,
                    new[] { BreakpointDetails.Create(this.debugScriptFile.FilePath, 2) }).ConfigureAwait(false);

            confirmedBreakpoints = await this.GetConfirmedBreakpoints(this.debugScriptFile).ConfigureAwait(false);

            Assert.Single(confirmedBreakpoints);
            Assert.Equal(2, breakpoints[0].LineNumber);

            await this.debugService.SetLineBreakpointsAsync(
                this.debugScriptFile,
                Array.Empty<BreakpointDetails>()).ConfigureAwait(false);

            var remainingBreakpoints = await this.GetConfirmedBreakpoints(this.debugScriptFile).ConfigureAwait(false);

            Assert.Empty(remainingBreakpoints);

            // Abort debugger
            this.debugService.Abort();
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerStopsOnLineBreakpoints()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.debugScriptFile,
                new[] {
                    BreakpointDetails.Create(this.debugScriptFile.FilePath, 5),
                    BreakpointDetails.Create(this.debugScriptFile.FilePath, 7)
                }).ConfigureAwait(false);

            Task executeTask =
                this.powerShellContext.ExecuteScriptWithArgsAsync(
                    this.debugScriptFile.FilePath);

            // Wait for a couple breakpoints
            await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 5).ConfigureAwait(false);
            this.debugService.Continue();

            await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 7).ConfigureAwait(false);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerStopsOnConditionalBreakpoints()
        {
            const int breakpointValue1 = 10;
            const int breakpointValue2 = 20;

            await this.debugService.SetLineBreakpointsAsync(
                this.debugScriptFile,
                new[] {
                    BreakpointDetails.Create(this.debugScriptFile.FilePath, 7, null, $"$i -eq {breakpointValue1} -or $i -eq {breakpointValue2}"),
                }).ConfigureAwait(false);

            Task executeTask =
                this.powerShellContext.ExecuteScriptWithArgsAsync(
                    this.debugScriptFile.FilePath);

            // Wait for conditional breakpoint to hit
            await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 7).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();
            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            // Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
            var i = variables.FirstOrDefault(v => v.Name == "$i");
            Assert.NotNull(i);
            Assert.False(i.IsExpandable);
            Assert.Equal($"{breakpointValue1}", i.ValueString);

            // The conditional breakpoint should not fire again, until the value of
            // i reaches breakpointValue2.
            this.debugService.Continue();
            await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 7).ConfigureAwait(false);

            stackFrames = debugService.GetStackFrames();
            variables = debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            // Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
            i = variables.FirstOrDefault(v => v.Name == "$i");
            Assert.NotNull(i);
            Assert.False(i.IsExpandable);
            Assert.Equal($"{breakpointValue2}", i.ValueString);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerStopsOnHitConditionBreakpoint()
        {
            const int hitCount = 5;

            await this.debugService.SetLineBreakpointsAsync(
                this.debugScriptFile,
                new[] {
                    BreakpointDetails.Create(this.debugScriptFile.FilePath, 6, null, null, $"{hitCount}"),
                }).ConfigureAwait(false);

            Task executeTask =
                this.powerShellContext.ExecuteScriptWithArgsAsync(
                    this.debugScriptFile.FilePath);

            // Wait for conditional breakpoint to hit
            await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();
            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            // Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
            var i = variables.FirstOrDefault(v => v.Name == "$i");
            Assert.NotNull(i);
            Assert.False(i.IsExpandable);
            Assert.Equal($"{hitCount}", i.ValueString);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerStopsOnConditionalAndHitConditionBreakpoint()
        {
            const int hitCount = 5;

            await this.debugService.SetLineBreakpointsAsync(
                this.debugScriptFile,
                new[] {
                    BreakpointDetails.Create(this.debugScriptFile.FilePath, 6, null, $"$i % 2 -eq 0", $"{hitCount}"),
                }).ConfigureAwait(false);

            Task executeTask =
                this.powerShellContext.ExecuteScriptWithArgsAsync(
                    this.debugScriptFile.FilePath);

            // Wait for conditional breakpoint to hit
            await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 6).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();
            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            // Verify the breakpoint only broke at the condition ie. $i -eq breakpointValue1
            var i = variables.FirstOrDefault(v => v.Name == "$i");
            Assert.NotNull(i);
            Assert.False(i.IsExpandable);
            // Condition is even numbers ($i starting at 1) should end up on 10 with a hit count of 5.
            Assert.Equal("10", i.ValueString);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerProvidesMessageForInvalidConditionalBreakpoint()
        {
            BreakpointDetails[] breakpoints =
                await this.debugService.SetLineBreakpointsAsync(
                    this.debugScriptFile,
                    new[] {
                        // TODO: Add this breakpoint back when it stops moving around?! The ordering
                        // of these two breakpoints seems to do with which framework executes the
                        // code. Best guess is that `IEnumerable` is not stably sorted so `ToArray`
                        // returns different orderings. However, that doesn't explain why this is
                        // the only affected test.

                        // BreakpointDetails.Create(this.debugScriptFile.FilePath, 5),
                        BreakpointDetails.Create(this.debugScriptFile.FilePath, 10, column: null, condition: "$i -ez 100")
                    }).ConfigureAwait(false);

            Assert.Equal(1, breakpoints.Length);
            // Assert.Equal(5, breakpoints[0].LineNumber);
            // Assert.True(breakpoints[0].Verified);
            // Assert.Null(breakpoints[0].Message);

            Assert.Equal(10, breakpoints[0].LineNumber);
            Assert.False(breakpoints[0].Verified);
            Assert.NotNull(breakpoints[0].Message);
            Assert.Contains("Unexpected token '-ez'", breakpoints[0].Message);

            // Abort debugger
            this.debugService.Abort();
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerFindsParseableButInvalidSimpleBreakpointConditions()
        {
            BreakpointDetails[] breakpoints =
                await this.debugService.SetLineBreakpointsAsync(
                    this.debugScriptFile,
                    new[] {
                        BreakpointDetails.Create(this.debugScriptFile.FilePath, 5, column: null, condition: "$i == 100"),
                        BreakpointDetails.Create(this.debugScriptFile.FilePath, 7, column: null, condition: "$i > 100")
                    }).ConfigureAwait(false);

            Assert.Equal(2, breakpoints.Length);
            Assert.Equal(5, breakpoints[0].LineNumber);
            Assert.False(breakpoints[0].Verified);
            Assert.Contains("Use '-eq' instead of '=='", breakpoints[0].Message);

            Assert.Equal(7, breakpoints[1].LineNumber);
            Assert.False(breakpoints[1].Verified);
            Assert.NotNull(breakpoints[1].Message);
            Assert.Contains("Use '-gt' instead of '>'", breakpoints[1].Message);

            // Abort debugger
            this.debugService.Abort();
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerBreaksWhenRequested()
        {
            var confirmedBreakpoints = await this.GetConfirmedBreakpoints(this.debugScriptFile).ConfigureAwait(false);

            await this.AssertStateChange(
                PowerShellContextState.Ready,
                PowerShellExecutionResult.Completed).ConfigureAwait(false);

            Assert.False(
                confirmedBreakpoints.Any(),
                "Unexpected breakpoint found in script file");

            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.debugScriptFile.FilePath);

            // Break execution and wait for the debugger to stop
            this.debugService.Break();

            await this.AssertDebuggerPaused().ConfigureAwait(false);
            await this.AssertStateChange(
                PowerShellContextState.Ready,
                PowerShellExecutionResult.Stopped).ConfigureAwait(false);

            // Abort execution and wait for the debugger to exit
            this.debugService.Abort();

            await this.AssertStateChange(
                PowerShellContextState.Ready,
                PowerShellExecutionResult.Stopped).ConfigureAwait(false);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerRunsCommandsWhileStopped()
        {
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.debugScriptFile.FilePath);

            // Break execution and wait for the debugger to stop
            this.debugService.Break();
            await this.AssertStateChange(
                PowerShellContextState.Ready,
                PowerShellExecutionResult.Stopped).ConfigureAwait(false);

            // Try running a command from outside the pipeline thread
            await this.powerShellContext.ExecuteScriptStringAsync("Get-Command Get-Process").ConfigureAwait(false);

            // Abort execution and wait for the debugger to exit
            this.debugService.Abort();

            await this.AssertStateChange(
                PowerShellContextState.Ready,
                PowerShellExecutionResult.Stopped).ConfigureAwait(false);

            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerVariableStringDisplaysCorrectly()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.variableScriptFile,
                new[] { BreakpointDetails.Create(this.variableScriptFile.FilePath, 8) }).ConfigureAwait(false);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.variableScriptFile.FilePath);

            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            var var = variables.FirstOrDefault(v => v.Name == "$strVar");
            Assert.NotNull(var);
            Assert.Equal("\"Hello\"", var.ValueString);
            Assert.False(var.IsExpandable);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerGetsVariables()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.variableScriptFile,
                new[] { BreakpointDetails.Create(this.variableScriptFile.FilePath, 14) }).ConfigureAwait(false);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.variableScriptFile.FilePath);

            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            // TODO: Add checks for correct value strings as well

            var strVar = variables.FirstOrDefault(v => v.Name == "$strVar");
            Assert.NotNull(strVar);
            Assert.False(strVar.IsExpandable);

            var objVar = variables.FirstOrDefault(v => v.Name == "$assocArrVar");
            Assert.NotNull(objVar);
            Assert.True(objVar.IsExpandable);

            var objChildren = debugService.GetVariables(objVar.Id);
            Assert.Equal(9, objChildren.Length);

            var arrVar = variables.FirstOrDefault(v => v.Name == "$arrVar");
            Assert.NotNull(arrVar);
            Assert.True(arrVar.IsExpandable);

            var arrChildren = debugService.GetVariables(arrVar.Id);
            Assert.Equal(11, arrChildren.Length);

            var classVar = variables.FirstOrDefault(v => v.Name == "$classVar");
            Assert.NotNull(classVar);
            Assert.True(classVar.IsExpandable);

            var classChildren = debugService.GetVariables(classVar.Id);
            Assert.Equal(2, classChildren.Length);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerSetsVariablesNoConversion()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.variableScriptFile,
                new[] { BreakpointDetails.Create(this.variableScriptFile.FilePath, 14) }).ConfigureAwait(false);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.variableScriptFile.FilePath);

            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            // Test set of a local string variable (not strongly typed)
            string newStrValue = "\"Goodbye\"";
            string setStrValue = await debugService.SetVariableAsync(stackFrames[0].LocalVariables.Id, "$strVar", newStrValue).ConfigureAwait(false);
            Assert.Equal(newStrValue, setStrValue);

            VariableScope[] scopes = this.debugService.GetVariableScopes(0);

            // Test set of script scope int variable (not strongly typed)
            VariableScope scriptScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.ScriptScopeName);
            string newIntValue = "49";
            string newIntExpr = "7 * 7";
            string setIntValue = await debugService.SetVariableAsync(scriptScope.Id, "$scriptInt", newIntExpr).ConfigureAwait(false);
            Assert.Equal(newIntValue, setIntValue);

            // Test set of global scope int variable (not strongly typed)
            VariableScope globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName);
            string newGlobalIntValue = "4242";
            string setGlobalIntValue = await debugService.SetVariableAsync(globalScope.Id, "$MaximumHistoryCount", newGlobalIntValue).ConfigureAwait(false);
            Assert.Equal(newGlobalIntValue, setGlobalIntValue);

            // The above just tests that the debug service returns the correct new value string.
            // Let's step the debugger and make sure the values got set to the new values.
            this.debugService.StepOver();
            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            stackFrames = debugService.GetStackFrames();

            // Test set of a local string variable (not strongly typed)
            variables = debugService.GetVariables(stackFrames[0].LocalVariables.Id);
            var strVar = variables.FirstOrDefault(v => v.Name == "$strVar");
            Assert.Equal(newStrValue, strVar.ValueString);

            scopes = this.debugService.GetVariableScopes(0);

            // Test set of script scope int variable (not strongly typed)
            scriptScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.ScriptScopeName);
            variables = debugService.GetVariables(scriptScope.Id);
            var intVar = variables.FirstOrDefault(v => v.Name == "$scriptInt");
            Assert.Equal(newIntValue, intVar.ValueString);

            // Test set of global scope int variable (not strongly typed)
            globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName);
            variables = debugService.GetVariables(globalScope.Id);
            var intGlobalVar = variables.FirstOrDefault(v => v.Name == "$MaximumHistoryCount");
            Assert.Equal(newGlobalIntValue, intGlobalVar.ValueString);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerSetsVariablesWithConversion()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.variableScriptFile,
                new[] { BreakpointDetails.Create(this.variableScriptFile.FilePath, 14) }).ConfigureAwait(false);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.variableScriptFile.FilePath);

            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            // Test set of a local string variable (not strongly typed but force conversion)
            string newStrValue = "\"False\"";
            string newStrExpr = "$false";
            string setStrValue = await debugService.SetVariableAsync(stackFrames[0].LocalVariables.Id, "$strVar2", newStrExpr).ConfigureAwait(false);
            Assert.Equal(newStrValue, setStrValue);

            VariableScope[] scopes = this.debugService.GetVariableScopes(0);

            // Test set of script scope bool variable (strongly typed)
            VariableScope scriptScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.ScriptScopeName);
            string newBoolValue = "$true";
            string newBoolExpr = "1";
            string setBoolValue = await debugService.SetVariableAsync(scriptScope.Id, "$scriptBool", newBoolExpr).ConfigureAwait(false);
            Assert.Equal(newBoolValue, setBoolValue);

            // Test set of global scope ActionPreference variable (strongly typed)
            VariableScope globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName);
            string newGlobalValue = "Continue";
            string newGlobalExpr = "'Continue'";
            string setGlobalValue = await debugService.SetVariableAsync(globalScope.Id, "$VerbosePreference", newGlobalExpr).ConfigureAwait(false);
            Assert.Equal(newGlobalValue, setGlobalValue);

            // The above just tests that the debug service returns the correct new value string.
            // Let's step the debugger and make sure the values got set to the new values.
            this.debugService.StepOver();
            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            stackFrames = debugService.GetStackFrames();

            // Test set of a local string variable (not strongly typed but force conversion)
            variables = debugService.GetVariables(stackFrames[0].LocalVariables.Id);
            var strVar = variables.FirstOrDefault(v => v.Name == "$strVar2");
            Assert.Equal(newStrValue, strVar.ValueString);

            scopes = this.debugService.GetVariableScopes(0);

            // Test set of script scope bool variable (strongly typed)
            scriptScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.ScriptScopeName);
            variables = debugService.GetVariables(scriptScope.Id);
            var boolVar = variables.FirstOrDefault(v => v.Name == "$scriptBool");
            Assert.Equal(newBoolValue, boolVar.ValueString);

            // Test set of global scope ActionPreference variable (strongly typed)
            globalScope = scopes.FirstOrDefault(s => s.Name == VariableContainerDetails.GlobalScopeName);
            variables = debugService.GetVariables(globalScope.Id);
            var globalVar = variables.FirstOrDefault(v => v.Name == "$VerbosePreference");
            Assert.Equal(newGlobalValue, globalVar.ValueString);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerVariableEnumDisplaysCorrectly()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.variableScriptFile,
                new[] { BreakpointDetails.Create(this.variableScriptFile.FilePath, 15) }).ConfigureAwait(false);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.variableScriptFile.FilePath);

            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            var var = variables.FirstOrDefault(v => v.Name == "$enumVar");
            Assert.NotNull(var);
            Assert.Equal("Continue", var.ValueString);
            Assert.False(var.IsExpandable);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerVariableHashtableDisplaysCorrectly()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.variableScriptFile,
                new[] { BreakpointDetails.Create(this.variableScriptFile.FilePath, 11) }).ConfigureAwait(false);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.variableScriptFile.FilePath);

            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            VariableDetailsBase var = variables.FirstOrDefault(v => v.Name == "$assocArrVar");
            Assert.NotNull(var);
            Assert.Equal("[Hashtable: 2]", var.ValueString);
            Assert.True(var.IsExpandable);

            VariableDetailsBase[] childVars = debugService.GetVariables(var.Id);
            Assert.Equal(9, childVars.Length);
            Assert.Equal("[0]", childVars[0].Name);
            Assert.Equal("[1]", childVars[1].Name);

            var childVarStrs = new HashSet<string>(childVars.Select(v => v.ValueString));
            var expectedVars = new[] {
                "[firstChild, \"Child\"]",
                "[secondChild, 42]"
            };

            foreach (string expectedVar in expectedVars)
            {
                Assert.Contains(expectedVar, childVarStrs);
            }

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerVariableNullStringDisplaysCorrectly()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.variableScriptFile,
                new[] { BreakpointDetails.Create(this.variableScriptFile.FilePath, 16) }).ConfigureAwait(false);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.variableScriptFile.FilePath);

            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            var nullStringVar = variables.FirstOrDefault(v => v.Name == "$nullString");
            Assert.NotNull(nullStringVar);
            Assert.Equal("[NullString]", nullStringVar.ValueString);
            Assert.True(nullStringVar.IsExpandable);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerVariablePSObjectDisplaysCorrectly()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.variableScriptFile,
                new[] { BreakpointDetails.Create(this.variableScriptFile.FilePath, 17) }).ConfigureAwait(false);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.variableScriptFile.FilePath);

            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            var psObjVar = variables.FirstOrDefault(v => v.Name == "$psObjVar");
            Assert.NotNull(psObjVar);
            Assert.True("@{Age=75; Name=John}".Equals(psObjVar.ValueString) || "@{Name=John; Age=75}".Equals(psObjVar.ValueString));
            Assert.True(psObjVar.IsExpandable);

            IDictionary<string, string> childVars = debugService.GetVariables(psObjVar.Id).ToDictionary(v => v.Name, v => v.ValueString);
            Assert.Equal(2, childVars.Count);
            Assert.Contains("Age", childVars.Keys);
            Assert.Contains("Name", childVars.Keys);
            Assert.Equal("75", childVars["Age"]);
            Assert.Equal("\"John\"", childVars["Name"]);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerVariablePSCustomObjectDisplaysCorrectly()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.variableScriptFile,
                new[] { BreakpointDetails.Create(this.variableScriptFile.FilePath, 18) }).ConfigureAwait(false);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.variableScriptFile.FilePath);

            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            var var = variables.FirstOrDefault(v => v.Name == "$psCustomObjVar");
            Assert.NotNull(var);
            Assert.Equal("@{Name=Paul; Age=73}", var.ValueString);
            Assert.True(var.IsExpandable);

            var childVars = debugService.GetVariables(var.Id);
            Assert.Equal(2, childVars.Length);
            Assert.Equal("Name", childVars[0].Name);
            Assert.Equal("\"Paul\"", childVars[0].ValueString);
            Assert.Equal("Age", childVars[1].Name);
            Assert.Equal("73", childVars[1].ValueString);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        // Verifies fix for issue #86, $proc = Get-Process foo displays just the ETS property set
        // and not all process properties.
        [Trait("Category", "DebugService")]
        [Fact]
        public async Task DebuggerVariableProcessObjDisplaysCorrectly()
        {
            await this.debugService.SetLineBreakpointsAsync(
                this.variableScriptFile,
                new[] { BreakpointDetails.Create(this.variableScriptFile.FilePath, 19) }).ConfigureAwait(false);

            // Execute the script and wait for the breakpoint to be hit
            Task executeTask =
                this.powerShellContext.ExecuteScriptStringAsync(
                    this.variableScriptFile.FilePath);

            await this.AssertDebuggerStopped(this.variableScriptFile.FilePath).ConfigureAwait(false);

            StackFrameDetails[] stackFrames = debugService.GetStackFrames();

            VariableDetailsBase[] variables =
                debugService.GetVariables(stackFrames[0].LocalVariables.Id);

            var var = variables.FirstOrDefault(v => v.Name == "$procVar");
            Assert.NotNull(var);
            Assert.StartsWith("System.Diagnostics.Process", var.ValueString);
            Assert.True(var.IsExpandable);

            var childVars = debugService.GetVariables(var.Id);
            Assert.Equal(53, childVars.Length);

            // Abort script execution early and wait for completion
            this.debugService.Abort();
            await executeTask.ConfigureAwait(false);
        }

        private async Task AssertDebuggerPaused()
        {
            DebuggerStoppedEventArgs eventArgs =
                await this.debuggerStoppedQueue.DequeueAsync(new CancellationTokenSource(10000).Token).ConfigureAwait(false);

            Assert.Empty(eventArgs.OriginalEvent.Breakpoints);
        }

        private async Task AssertDebuggerStopped(
            string scriptPath,
            int lineNumber = -1)
        {
            DebuggerStoppedEventArgs eventArgs =
                await this.debuggerStoppedQueue.DequeueAsync(new CancellationTokenSource(10000).Token).ConfigureAwait(false);

            // TODO: Why does the casing of the path change? Specifically the Drive letter on Windows.
            Assert.Equal(scriptPath.ToLower(), eventArgs.ScriptPath.ToLower());
            if (lineNumber > -1)
            {
                Assert.Equal(lineNumber, eventArgs.LineNumber);
            }
        }

        private async Task AssertStateChange(
            PowerShellContextState expectedState,
            PowerShellExecutionResult expectedResult = PowerShellExecutionResult.Completed)
        {
            SessionStateChangedEventArgs newState =
                await this.sessionStateQueue.DequeueAsync(new CancellationTokenSource(10000).Token).ConfigureAwait(false);

            Assert.Equal(expectedState, newState.NewSessionState);
            Assert.Equal(expectedResult, newState.ExecutionResult);
        }

        private async Task<IEnumerable<LineBreakpoint>> GetConfirmedBreakpoints(ScriptFile scriptFile)
        {
            return
                await this.powerShellContext.ExecuteCommandAsync<LineBreakpoint>(
                    new PSCommand()
                        .AddCommand("Get-PSBreakpoint")
                        .AddParameter("Script", scriptFile.FilePath)).ConfigureAwait(false);
        }
    }
}