# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

$FeatureEnabled = $EnabledExperimentalFeatures.Contains('Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace')

Describe 'Runspace Breakpoint Unit Tests - Feature-Enabled' -Tags 'CI' {

    BeforeAll {
        if (!$FeatureEnabled) {
            Write-Verbose 'Test series skipped. This series of tests requires the experimental feature ''Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace'' to be enabled.' -Verbose
            $originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
            $PSDefaultParameterValues['it:skip'] = $true
            return
        }

        # Start a job; this will create a runspace in which we can manage breakpoints
        $job = Start-Job -ScriptBlock {
            Set-PSBreakpoint -Command Start-Sleep
            1..240 | ForEach-Object {
                Start-Sleep -Milliseconds 250
                $_
                Write-Error 'boo'
                Write-Verbose 'Verbose' -Verbose
                $DebugPreference = 'Continue'
                Write-Debug 'Debug'
                Write-Warning 'Warning'
            }
        }

        # Wait for the child job that gets created to hit the breakpoint. This is the
        # only safe way to know that the job has actually entered a running state and
        # that the remote runspace is listening for requests.
        Wait-UntilTrue { $job.ChildJobs.Count -gt 0 -and $job.ChildJobs[0].State -eq 'AtBreakpoint' } -TimeoutInMilliseconds 10000 -IntervalInMilliseconds 250

        # Get the runspace for the running job
        $jobRunspace = $job.ChildJobs[0].Runspace
    }

    AfterAll {
        if (!$FeatureEnabled) {
            $global:PSDefaultParameterValues = $originalDefaultParameterValues
            return
        }

        # Remove the running job forcibly (whether it has finished or not)
        Remove-Job -Job $job -Force
    }

    # Test the transformation attribute independently of PSBreakpoint cmdlet tests so that
    # those tests can focus on scenarios expected to work.
    Context 'Can transform a runspace name, id, or instanceid into a runspace' {

        function Test-RunspaceTransform {
            param(
                [ValidateNotNull()]
                [Runspace]
                [System.Management.Automation.Runspaces.Runspace()]
                $Runspace
            )
            $Runspace
        }

        It 'Transforms a valid runspace name into a runspace' {
            Test-RunspaceTransform -Runspace $host.Runspace.Name | Should -Be $host.Runspace
        }

        It 'Transforms a valid runspace ID into a runspace' {
            Test-RunspaceTransform -Runspace $host.Runspace.Id | Should -Be $host.Runspace
        }

        It 'Transforms a valid runspace instance ID into a runspace' {
            Test-RunspaceTransform -Runspace $host.Runspace.InstanceId | Should -Be $host.Runspace
        }

        It 'Reports an argument transformation error when given invalid input' {
            $e = { Test-RunspaceTransform -Runspace 'This is not a runspace name' } | Should -Throw -PassThru
            $e.Exception.GetType().Name | Should -Be 'ParameterBindingArgumentTransformationException'
        }

        It 'Passes through $null without transforming it' {
            $e = { Test-RunspaceTransform -Runspace $null } | Should -Throw -PassThru
            $e.Exception.GetType().Name | Should -Be 'ParameterBindingValidationException'
        }
    }

    Context 'Managing breakpoints in the host runspace' {

        It 'Can set breakpoints' {
            Set-PSBreakpoint -Command Test-ThisCommandDoesNotExist -Runspace $host.Runspace | Should -BeOfType [System.Management.Automation.CommandBreakpoint]
        }

        It 'Can get breakpoints, and the result breakpoints do not show the runspace id because they are local' {
            foreach ($bp in Get-PSBreakpoint -Runspace $host.Runspace) {
                Get-Member -InputObject $bp -Name RunspaceId -ErrorAction Ignore | Should -Be $null
            }
        }

        It 'Can disable breakpoints in a pipeline' {
            foreach ($bp in Get-PSBreakpoint -Runspace $host.Runspace | Disable-PSBreakpoint -PassThru) {
                $bp.Enabled | Should -BeFalse
                # This ensures we're working in the right runspace
                Get-Member -InputObject $bp -Name RunspaceId -ErrorAction Ignore | Should -Be $null
            }
        }

        It 'Can enable breakpoints in a pipeline' {
            foreach ($bp in Get-PSBreakpoint -Runspace $host.Runspace | Enable-PSBreakpoint -PassThru) {
                $bp.Enabled | Should -BeTrue
                # This ensures we're working in the right runspace
                Get-Member -InputObject $bp -Name RunspaceId -ErrorAction Ignore | Should -Be $null
            }
        }

        It 'Can disable breakpoints by id' {
            foreach ($bp in Get-PSBreakpoint -Runspace $host.Runspace) {
                $bp = Disable-PSBreakpoint -Id $bp.Id -Runspace $host.Runspace -PassThru
                $bp.Enabled | Should -BeFalse
                # This ensures we're working in the right runspace
                Get-Member -InputObject $bp -Name RunspaceId -ErrorAction Ignore | Should -Be $null
            }
        }

        It 'Can enable breakpoints by id' {
            foreach ($bp in Get-PSBreakpoint -Runspace $host.Runspace) {
                $bp = Enable-PSBreakpoint -Id $bp.Id -Runspace $host.Runspace -PassThru
                $bp.Enabled | Should -BeTrue
                # This ensures we're working in the right runspace
                Get-Member -InputObject $bp -Name RunspaceId -ErrorAction Ignore | Should -Be $null
            }
        }

        It 'Can remove breakpoints' {
            Get-PSBreakpoint -Runspace $host.Runspace | Remove-PSBreakpoint
            Get-PSBreakpoint -Runspace $host.Runspace | Should -BeNull
        }
    }

    Context 'Managing breakpoints in a remote runspace' {

        AfterAll {
            # Get rid of any breakpoints that were created in the default runspace.
            # This is necessary due to a known bug that causes breakpoints with the
            # same id to be created or updated in the default runspace.
            Get-PSBreakpoint | Remove-PSBreakpoint
        }

        It 'Can set breakpoints' {
            Set-PSBreakpoint -Command Write-Verbose -Action { break } -Runspace $jobRunspace | Should -BeOfType [System.Management.Automation.CommandBreakpoint]
            Set-PSBreakpoint -Variable DebugPreference -Mode ReadWrite -Action { break } -Runspace $jobRunspace | Should -BeOfType [System.Management.Automation.VariableBreakpoint]
            Set-PSBreakpoint -Script $PSCommandPath -Line 1 -Column 1 -Action { break } -Runspace $jobRunspace | Should -BeOfType [System.Management.Automation.LineBreakpoint]
        }

        It 'Can get breakpoints, and the result breakpoints show the remote runspace id' {
            foreach ($bp in Get-PSBreakpoint -Runspace $jobRunspace) {
                $bp.RunspaceId | Should -Be $jobRunspace.InstanceId
            }
        }

        It 'Breakpoints are triggered by the remote debugger' {
            $startTime = [DateTime]::UtcNow
            $maxTimeToWait = [TimeSpan]'00:00:20'
            while ($job.State -ne 'AtBreakpoint' -and ([DateTime]::UtcNow - $startTime) -lt $maxTimeToWait) {
                Start-Sleep -Milliseconds 100 # Give the job a bit of time to hit a breakpoint
            }
            $job.State | Should -Be 'AtBreakpoint'
        }

        It 'Can disable breakpoints in a pipeline' {
            foreach ($bp in Get-PSBreakpoint -Runspace $jobRunspace | Disable-PSBreakpoint -PassThru) {
                $bp.Enabled | Should -BeFalse
                # This ensures we're working in the right runspace
                $bp.RunspaceId | Should -Be $jobRunspace.InstanceId
            }
        }

        It 'Can enable breakpoints in a pipeline' {
            foreach ($bp in Get-PSBreakpoint -Runspace $jobRunspace | Enable-PSBreakpoint -PassThru) {
                $bp.Enabled | Should -BeTrue
                # This ensures we're working in the right runspace
                $bp.RunspaceId | Should -Be $jobRunspace.InstanceId
            }
        }

        It 'Can disable breakpoints by id' {
            foreach ($bp in Get-PSBreakpoint -Runspace $jobRunspace) {
                $bp = Disable-PSBreakpoint -Id $bp.Id -Runspace $jobRunspace -PassThru
                $bp.Enabled | Should -BeFalse
                # This ensures we're working in the right runspace
                $bp.RunspaceId | Should -Be $jobRunspace.InstanceId
            }
        }

        It 'Can enable breakpoints in a pipeline' {
            foreach ($bp in Get-PSBreakpoint -Runspace $jobRunspace) {
                $bp = Enable-PSBreakpoint -Id $bp.Id -Runspace $jobRunspace -PassThru
                $bp.Enabled | Should -BeTrue
                # This ensures we're working in the right runspace
                $bp.RunspaceId | Should -Be $jobRunspace.InstanceId
            }
        }

        It 'Can remove breakpoints' {
            Get-PSBreakpoint -Runspace $jobRunspace | Remove-PSBreakpoint
            Get-PSBreakpoint -Runspace $jobRunspace | Should -BeNull
        }
    }
}
