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

Describe "Error handling within a single 'Clean' block" -Tag 'CI' {

    BeforeAll {
        function ErrorInClean {
            [CmdletBinding()]
            param(
                [switch] $ThrowTerminatingError,
                [switch] $ErrorActionStop,
                [switch] $ThrowException,
                [switch] $WriteErrorAPI,
                [switch] $WriteErrorCmdlet,
                [switch] $MethodInvocationThrowException,
                [switch] $ExpressionThrowException
            )

            End { <# use an empty end block to allow the clean block to actually run #> }

            clean {
                if ($ThrowTerminatingError) {
                    $ex = [System.ArgumentException]::new('terminating-exception')
                    $er = [System.Management.Automation.ErrorRecord]::new($ex, 'ThrowTerminatingError:error', 'InvalidArgument', $null)
                    $PSCmdlet.ThrowTerminatingError($er)
                    Write-Verbose -Verbose "verbose-message"
                }
                elseif ($ErrorActionStop) {
                    Get-Command NonExist -ErrorAction Stop
                    Write-Verbose -Verbose "verbose-message"
                }
                elseif ($ThrowException) {
                    throw 'throw-exception'
                    Write-Verbose -Verbose "verbose-message"
                }
                elseif ($WriteErrorAPI) {
                    $ex = [System.ArgumentException]::new('arg-exception')
                    $er = [System.Management.Automation.ErrorRecord]::new($ex, 'WriteErrorAPI:error', 'InvalidArgument', $null)
                    $PSCmdlet.WriteError($er)
                    Write-Verbose -Verbose "verbose-message"
                }
                elseif ($WriteErrorCmdlet) {
                    Write-Error 'write-error-cmdlet'
                    Write-Verbose -Verbose "verbose-message"
                }
                elseif ($MethodInvocationThrowException) {
                    ## This method call throws exception.
                    $iss = [initialsessionstate]::Create()
                    $iss.ImportPSModule($null)
                    Write-Verbose -Verbose "verbose-message"
                }
                elseif ($ExpressionThrowException) {
                    1/0 ## throw exception.
                    Write-Verbose -Verbose "verbose-message"
                }
            }
        }

        function ErrorInEnd {
            [CmdletBinding()]
            param(
                [switch] $ThrowTerminatingError,
                [switch] $ErrorActionStop,
                [switch] $ThrowException,
                [switch] $WriteErrorAPI,
                [switch] $WriteErrorCmdlet,
                [switch] $MethodInvocationThrowException,
                [switch] $ExpressionThrowException
            )

            if ($ThrowTerminatingError) {
                $ex = [System.ArgumentException]::new('terminating-exception')
                $er = [System.Management.Automation.ErrorRecord]::new($ex, 'ThrowTerminatingError:error', 'InvalidArgument', $null)
                $PSCmdlet.ThrowTerminatingError($er)
                Write-Verbose -Verbose "verbose-message"
            }
            elseif ($ErrorActionStop) {
                Get-Command NonExist -ErrorAction Stop
                Write-Verbose -Verbose "verbose-message"
            }
            elseif ($ThrowException) {
                throw 'throw-exception'
                Write-Verbose -Verbose "verbose-message"
            }
            elseif ($WriteErrorAPI) {
                $ex = [System.ArgumentException]::new('arg-exception')
                $er = [System.Management.Automation.ErrorRecord]::new($ex, 'WriteErrorAPI:error', 'InvalidArgument', $null)
                $PSCmdlet.WriteError($er)
                Write-Verbose -Verbose "verbose-message"
            }
            elseif ($WriteErrorCmdlet) {
                Write-Error 'write-error-cmdlet'
                Write-Verbose -Verbose "verbose-message"
            }
            elseif ($MethodInvocationThrowException) {
                ## This method call throws exception.
                $iss = [initialsessionstate]::Create()
                $iss.ImportPSModule($null)
                Write-Verbose -Verbose "verbose-message"
            }
            elseif ($ExpressionThrowException) {
                1/0 ## throw exception.
                Write-Verbose -Verbose "verbose-message"
            }
        }

        function DivideByZeroWrappedInTry {
            [CmdletBinding()]
            param()

            end {}
            clean {
                try {
                    1/0
                    Write-Verbose -Verbose 'clean'
                }
                catch { Write-Verbose -Verbose $_.Exception.InnerException.GetType().FullName }
            }
        }

        function ArgumentNullWrappedInTry {
            [CmdletBinding()]
            param()

            end {}
            clean {
                try {
                    $iss = [initialsessionstate]::Create()
                    $iss.ImportPSModule($null)
                    Write-Verbose -Verbose 'clean'
                }
                catch { Write-Verbose -Verbose $_.Exception.InnerException.GetType().FullName }
            }
        }

        function DivideByZeroWithTrap {
            [CmdletBinding()]
            param()

            end {}
            clean {
                trap {
                    Write-Verbose -Verbose $_.Exception.GetType().FullName
                    continue
                }

                1/0
                Write-Verbose -Verbose 'clean'
            }
        }

        function ArgumentNullWithTrap {
            [CmdletBinding()]
            param()

            end {}
            clean {
                trap {
                    Write-Verbose -Verbose $_.Exception.GetType().FullName
                    continue
                }

                $iss = [initialsessionstate]::Create()
                $iss.ImportPSModule($null)
                Write-Verbose -Verbose 'clean'
            }
        }

        #region Helper

        $pwsh = [PowerShell]::Create()
        $text = (Get-Command ErrorInClean).ScriptBlock.Ast.Extent.Text
        $pwsh.AddScript($text).Invoke()

        $pwsh.Commands.Clear()
        $text = (Get-Command ErrorInEnd).ScriptBlock.Ast.Extent.Text
        $pwsh.AddScript($text).Invoke()

        function RunCommand {
            param(
                [ValidateSet('ErrorInClean', 'ErrorInEnd')]
                [string] $Command,

                [ValidateSet('ThrowTerminatingError', 'ErrorActionStop', 'ThrowException', 'WriteErrorAPI', 
                             'WriteErrorCmdlet', 'MethodInvocationThrowException', 'ExpressionThrowException')]
                [string] $ParamNameToUse,

                [ValidateSet('Continue', 'Ignore', 'SilentlyContinue', 'Stop')]
                [string] $ErrorAction
            )

            $pwsh.Commands.Clear()
            $pwsh.Streams.ClearStreams()
            $pwsh.AddCommand($Command).AddParameter($ParamNameToUse, $true) > $null
            if ($ErrorAction) { $pwsh.AddParameter('ErrorAction', $ErrorAction) > $null }
            $pwsh.Invoke()
        }

        function RunScript {
            param([string] $Script)

            $pwsh.Commands.Clear()
            $pwsh.Streams.ClearStreams()
            $pwsh.AddScript($Script).Invoke()
        }

        function GetLastError {
            $pwsh.Commands.Clear()
            $pwsh.AddCommand('Get-Error').Invoke()
        }

        function ClearDollarError {
            $pwsh.Commands.Clear()
            $pwsh.AddScript('$Error.Clear()').Invoke()
        }

        #endregion
    }

    AfterAll {
        $pwsh.Dispose()
    }

    It "Terminating error should stop the 'Clean' block execution but should not be propagated up" {
        ## 'ThrowTerminatingException' stops the execution within the 'Clean' block, but the error doesn't get
        ## propagated out of the 'Clean' block. Instead, the error is written to the 'ErrorOutput' pipe.
        RunCommand -Command 'ErrorInClean' -ParamNameToUse 'ThrowTerminatingError'
        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'terminating-exception'

        ## 'throw' statement stops the execution within the 'Clean' block by default, but the error doesn't get
        ## propagated out of the 'Clean' block. Instead, the error is written to the 'ErrorOutput' pipe.
        RunCommand -Command 'ErrorInClean' -ParamNameToUse 'ThrowException'
        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'throw-exception'

        ## '-ErrorAction Stop' stops the execution within the 'Clean' block, but the error doesn't get propagated
        ## out of the 'Clean' block. Instead, the error is written to the 'ErrorOutput' pipe.
        RunCommand -Command 'ErrorInClean' -ParamNameToUse 'ErrorActionStop'
        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].FullyQualifiedErrorId | Should -BeExactly 'CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand'

        ## Turn non-terminating errors into terminating by '-ErrorAction Stop' explicitly.
        ## Execution within the 'Clean' block should be stopped. The resulted terminating error should not get
        ## propagated, but instead should be written to 'ErrorOutput' pipe.
        RunCommand -Command 'ErrorInClean' -ParamNameToUse 'WriteErrorAPI' -ErrorAction Stop
        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'arg-exception'

        RunCommand -Command 'ErrorInClean' -ParamNameToUse 'WriteErrorCmdlet' -ErrorAction Stop
        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'write-error-cmdlet'

        RunCommand -Command 'ErrorInClean' -ParamNameToUse 'MethodInvocationThrowException' -ErrorAction Stop
        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'

        RunCommand -Command 'ErrorInClean' -ParamNameToUse 'ExpressionThrowException' -ErrorAction Stop
        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.DivideByZeroException'
    }

    It "Terminating error should set `$? correctly" {
        RunScript -Script 'ErrorInClean -ThrowTerminatingError; $?' | Should -BeFalse
        RunScript -Script 'ErrorInClean -ThrowException; $?' | Should -BeFalse
        RunScript -Script 'ErrorInClean -ErrorActionStop; $?' | Should -BeFalse
        RunScript -Script 'ErrorInClean -WriteErrorAPI -ErrorAction Stop; $?' | Should -BeFalse
        RunScript -Script 'ErrorInClean -WriteErrorCmdlet -ErrorAction Stop; $?' | Should -BeFalse
        RunScript -Script 'ErrorInClean -MethodInvocationThrowException -ErrorAction Stop; $?' | Should -BeFalse
        RunScript -Script 'ErrorInClean -ExpressionThrowException -ErrorAction Stop; $?' | Should -BeFalse
    }

    It "Track the `$? behavior for non-terminating errors within 'Clean' and 'End' blocks" {
        RunScript -Script 'ErrorInClean -WriteErrorAPI; $?' | Should -BeFalse
        RunScript -Script 'ErrorInEnd -WriteErrorAPI; $?' | Should -BeFalse

        ## The 'Write-Error' is specially weird, in that when a command emits error because of 'Write-Error' within it,
        ## the following '$?' won't reflect '$false', but will be '$true'.
        ## Frankly, this is counter-intuitive, but it's the existing behavior. The tests below just keeps track of this
        ## behavior. Feel free to change this test if someone is fixing this seemingly wrong behavior.
        RunScript -Script 'ErrorInClean -WriteErrorCmdlet; $?' | Should -BeTrue
        RunScript -Script 'ErrorInEnd -WriteErrorCmdlet; $?' | Should -BeTrue

        ## Similarly, when a command emits error because of a method invocation within it throws an exception,
        ## the following '$?' won't reflect '$false', but will be '$true'.
        ## Again, this seems wrong, but it's the existing behavior. The tests below just keeps track of this
        ## behavior. Feel free to change this test if someone is fixing this seemingly wrong behavior.
        RunScript -Script 'ErrorInClean -MethodInvocationThrowException; $?' | Should -BeTrue
        RunScript -Script 'ErrorInEnd -MethodInvocationThrowException; $?' | Should -BeTrue

        ## Again, when a command emits error because of an expression within it throws an exception,
        ## the following '$?' won't reflect '$false', but will be '$true'.
        ## This seems wrong, but it's the existing behavior. The tests below just keeps track of this
        ## behavior. Feel free to change this test if someone is fixing this seemingly wrong behavior.
        RunScript -Script 'ErrorInClean -ExpressionThrowException; $?' | Should -BeTrue
        RunScript -Script 'ErrorInEnd -ExpressionThrowException; $?' | Should -BeTrue
    }

    It "Non-terminating error within 'Clean' block should act based on ErrorActionPreference: <ParamName> - Continue" -TestCases @(
        @{ ParamName = 'WriteErrorAPI';    AssertScript = { param($err) $err.Exception.Message | Should -BeExactly 'arg-exception' } }
        @{ ParamName = 'WriteErrorCmdlet'; AssertScript = { param($err) $err.Exception.Message | Should -BeExactly 'write-error-cmdlet' } }
        @{ ParamName = 'MethodInvocationThrowException'; AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.ArgumentNullException' } }
        @{ ParamName = 'ExpressionThrowException';       AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.DivideByZeroException' } }
    ) {
        param($ParamName, $AssertScript)

        RunCommand -Command 'ErrorInClean' -ParamNameToUse $ParamName
        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'verbose-message'
        $pwsh.Streams.Error.Count | Should -Be 1
        & $AssertScript $pwsh.Streams.Error[0]
    }

    It "Non-terminating error within 'End' block should act based on ErrorActionPreference: <ParamName> - Continue" -TestCases @(
        @{ ParamName = 'WriteErrorAPI';    AssertScript = { param($err) $err.Exception.Message | Should -BeExactly 'arg-exception' } }
        @{ ParamName = 'WriteErrorCmdlet'; AssertScript = { param($err) $err.Exception.Message | Should -BeExactly 'write-error-cmdlet' } }
        @{ ParamName = 'MethodInvocationThrowException'; AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.ArgumentNullException' } }
        @{ ParamName = 'ExpressionThrowException';       AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.DivideByZeroException' } }
    ) {
        param($ParamName, $AssertScript)

        RunCommand -Command 'ErrorInEnd' -ParamNameToUse $ParamName
        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'verbose-message'
        $pwsh.Streams.Error.Count | Should -Be 1
        & $AssertScript $pwsh.Streams.Error[0]
    }

    It "Non-terminating error within 'Clean' block should act based on ErrorActionPreference: <ParamName> - <Action>" -TestCases @(
        ### When error action is 'Ignore', non-terminating errors emitted by 'WriteErrorAPI' and 'WriteErrorCmdlet' are not captured in $Error,
        ### but non-terminating errors emitted by method exception or expression exception are captured in $Error.
        ### This inconsistency is surprising, but it's the existing behavior -- same in other named blocks.
        @{ ParamName = 'WriteErrorAPI'; Action = 'Ignore'; AssertScript = $null }
        @{ ParamName = 'WriteErrorCmdlet'; Action = 'Ignore'; AssertScript = $null }
        @{ ParamName = 'MethodInvocationThrowException'; Action = 'Ignore'; AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.ArgumentNullException' } }
        @{ ParamName = 'ExpressionThrowException'; Action = 'Ignore'; AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.DivideByZeroException' } }

        @{ ParamName = 'WriteErrorAPI'; Action = 'SilentlyContinue'; AssertScript = { param($err) $err.Exception.Message | Should -BeExactly 'arg-exception' } }
        @{ ParamName = 'WriteErrorCmdlet'; Action = 'SilentlyContinue'; AssertScript = { param($err) $err.Exception.Message | Should -BeExactly 'write-error-cmdlet' } }
        @{ ParamName = 'MethodInvocationThrowException'; Action = 'SilentlyContinue'; AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.ArgumentNullException' } }
        @{ ParamName = 'ExpressionThrowException'; Action = 'SilentlyContinue'; AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.DivideByZeroException' } }
    ) {
        param($ParamName, $Action, $AssertScript)

        ClearDollarError
        RunCommand -Command 'ErrorInClean' -ParamNameToUse $ParamName -ErrorAction $Action

        $pwsh.Streams.Error.Count | Should -Be 0
        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'verbose-message'

        $lastErr = GetLastError
        if ($null -eq $AssertScript) {
            $lastErr | Should -BeNullOrEmpty
        } else {
            $lastErr | Should -Not -BeNullOrEmpty
            & $AssertScript $lastErr
        }
    }

    ### These tests are targeting 'End' block but with the same settings as the ones right above.
    ### They are used as a comparison to prove the consistent behavior in 'End' and 'Clean'.
    It "Non-terminating error within 'End' block should act based on ErrorActionPreference: <ParamName> - <Action>" -TestCases @(
        @{ ParamName = 'WriteErrorAPI'; Action = 'Ignore'; AssertScript = $null }
        @{ ParamName = 'WriteErrorCmdlet'; Action = 'Ignore'; AssertScript = $null }
        @{ ParamName = 'MethodInvocationThrowException'; Action = 'Ignore'; AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.ArgumentNullException' } }
        @{ ParamName = 'ExpressionThrowException'; Action = 'Ignore'; AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.DivideByZeroException' } }

        @{ ParamName = 'WriteErrorAPI'; Action = 'SilentlyContinue'; AssertScript = { param($err) $err.Exception.Message | Should -BeExactly 'arg-exception' } }
        @{ ParamName = 'WriteErrorCmdlet'; Action = 'SilentlyContinue'; AssertScript = { param($err) $err.Exception.Message | Should -BeExactly 'write-error-cmdlet' } }
        @{ ParamName = 'MethodInvocationThrowException'; Action = 'SilentlyContinue'; AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.ArgumentNullException' } }
        @{ ParamName = 'ExpressionThrowException'; Action = 'SilentlyContinue'; AssertScript = { param($err) $err.Exception.InnerException | Should -BeOfType 'System.DivideByZeroException' } }
    ) {
        param($ParamName, $Action, $AssertScript)

        ClearDollarError
        RunCommand -Command 'ErrorInEnd' -ParamNameToUse $ParamName -ErrorAction $Action

        $pwsh.Streams.Error.Count | Should -Be 0
        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'verbose-message'

        $lastErr = GetLastError
        if ($null -eq $AssertScript) {
            $lastErr | Should -BeNullOrEmpty
        } else {
            $lastErr | Should -Not -BeNullOrEmpty
            & $AssertScript $lastErr
        }
    }

    It "'try/catch' and 'trap' should turn general exception thrown from method/expression into terminating error within 'Clean' block. ErrorAction: <ErrorAction>" -TestCases @(
        @{ ErrorAction = 'Continue' }
        @{ ErrorAction = 'Ignore' }
        @{ ErrorAction = 'SilentlyContinue' }
    ) {
        param($ErrorAction)

        $verbose = DivideByZeroWrappedInTry -ErrorAction $ErrorAction 4>&1
        $verbose.Count | Should -Be 1
        $verbose.Message | Should -BeExactly 'System.DivideByZeroException'

        $verbose = ArgumentNullWrappedInTry -ErrorAction $ErrorAction 4>&1
        $verbose.Count | Should -Be 1
        $verbose.Message | Should -BeExactly 'System.ArgumentNullException'

        $verbose = DivideByZeroWithTrap -ErrorAction $ErrorAction 4>&1
        $verbose.Count | Should -Be 2
        $verbose[0].Message | Should -BeExactly 'System.DivideByZeroException'
        $verbose[1].Message | Should -BeExactly 'clean'

        $verbose = ArgumentNullWithTrap -ErrorAction $ErrorAction 4>&1
        $verbose.Count | Should -Be 2
        $verbose[0].Message | Should -BeExactly 'System.ArgumentNullException'
        $verbose[1].Message | Should -BeExactly 'clean'
    }

    It "'try/catch' and 'trap' outside the command should NOT affect general exception thrown from method/expression in the 'Clean' block" {
        ## The catch block should not run
        RunScript -Script "try { ErrorInClean -MethodInvocationThrowException } catch { Write-Debug -Debug 'caught-something' }"

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'verbose-message'
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'
        $pwsh.Streams.Debug.Count | Should -Be 0

        ## The catch block should not run
        RunScript -Script "try { ErrorInClean -ExpressionThrowException } catch { Write-Debug -Debug 'caught-something' }"

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'verbose-message'
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.DivideByZeroException'
        $pwsh.Streams.Debug.Count | Should -Be 0

        ## The trap block should not run
        RunScript -Script "trap { Write-Debug -Debug 'caught-something'; continue } ErrorInClean -MethodInvocationThrowException"

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'verbose-message'
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'
        $pwsh.Streams.Debug.Count | Should -Be 0

        ## The trap block should not run
        RunScript -Script "trap { Write-Debug -Debug 'caught-something'; continue } ErrorInClean -ExpressionThrowException"

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'verbose-message'
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.DivideByZeroException'
        $pwsh.Streams.Debug.Count | Should -Be 0
    }

    It "'try/catch' and 'trap' outside the command should NOT affect 'throw' statement in the 'Clean' block. ErrorAction: <ErrorAction>" -TestCases @(
        @{ ErrorAction = 'Ignore' }
        @{ ErrorAction = 'SilentlyContinue' }
    ) {
        param ($ErrorAction)

        ## 'throw' statement should be suppressed by 'Ignore' or 'SilentlyContinue' within a 'Clean' block,
        ## even if the command is wrapped in try/catch.
        RunScript -Script "try { ErrorInClean -ThrowException -ErrorAction $ErrorAction } catch { Write-Debug -Debug 'caught-something' }"

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'verbose-message'

        ## Nothing written to error stream
        $pwsh.Streams.Error.Count | Should -Be 0
        ## Nothing written to debug stream
        $pwsh.Streams.Debug.Count | Should -Be 0

        ## The suppressed 'throw' exception is kept in '$Error'
        $err = GetLastError
        $err | Should -Not -BeNullOrEmpty
        $err.FullyQualifiedErrorId | Should -BeExactly 'throw-exception'


        ## 'throw' statement should be suppressed by 'Ignore' or 'SilentlyContinue' within a 'Clean' block,
        ## even if the command is accompanied by 'trap'.
        RunScript -Script "trap { Write-Debug -Debug 'caught-something'; continue } ErrorInClean -ThrowException -ErrorAction $ErrorAction"

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'verbose-message'

        ## Nothing written to error stream
        $pwsh.Streams.Error.Count | Should -Be 0
        ## Nothing written to debug stream
        $pwsh.Streams.Debug.Count | Should -Be 0

        ## The suppressed 'throw' exception is kept in '$Error'
        $err = GetLastError
        $err | Should -Not -BeNullOrEmpty
        $err.FullyQualifiedErrorId | Should -BeExactly 'throw-exception'
    }

    It "Error out-variable should work for the 'Clean' block" {
        ## Terminating errors thrown from 'Clean' block are captured and written to the error pipe.
        ## Here we redirect the error pipe to discard the error stream, so the error doesn't pollute
        ## the test output.
        ErrorInClean -ThrowTerminatingError -ErrorVariable err 2>&1 > $null
        $err.Count | Should -Be 1
        $err[0].Message | Should -BeExactly 'terminating-exception'

        ## $err.Count is 3 in this case. It's the same for other named blocks too.
        ## This looks like an existing bug because $err.Count should be 1 since only 1 error happened.
        ## Opened issue https://github.com/PowerShell/PowerShell/issues/15739
        ErrorInClean -ErrorActionStop -ErrorVariable err 2>&1 > $null
        $err[0] | Should -BeOfType 'System.Management.Automation.ActionPreferenceStopException'

        ## $err.Count is 2 in this case. It's the same for other named blocks too.
        ## Similarly, this looks like an existing bug and $err.Count should be 1.
        ## This is tracked by the same issue above.
        ErrorInClean -ThrowException -ErrorVariable err 2>&1 > $null
        $err[0].Exception.Message | Should -BeExactly 'throw-exception'

        ErrorInClean -WriteErrorAPI -ErrorVariable err *>&1 > $null
        $err.Count | Should -Be 1
        $err[0].Exception.Message | Should -BeExactly 'arg-exception'

        ErrorInClean -WriteErrorCmdlet -ErrorVariable err *>&1 > $null
        $err.Count | Should -Be 1
        $err[0].Exception.Message | Should -BeExactly 'write-error-cmdlet'

        ErrorInClean -MethodInvocationThrowException -ErrorVariable err *>&1 > $null
        $err.Count | Should -Be 1
        $err[0].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'

        ErrorInClean -ExpressionThrowException -ErrorVariable err *>&1 > $null
        $err.Count | Should -Be 1
        $err[0].Exception.InnerException | Should -BeOfType 'System.DivideByZeroException'
    }
}

Describe "Multiple errors from 'Clean' and another named block" {

    BeforeAll {
        function MultipleErrors {
            [CmdletBinding()]
            param(
                [ValidateSet('ThrowTerminatingError', 'ErrorActionStop', 'ThrowException', 'WriteErrorAPI',
                             'WriteErrorCmdlet', 'MethodInvocationThrowException', 'ExpressionThrowException')]
                [Parameter(Mandatory)]
                [string] $ErrorFromEndBlock,

                [ValidateSet('ThrowTerminatingError', 'ErrorActionStop', 'ThrowException', 'WriteErrorAPI',
                             'WriteErrorCmdlet', 'MethodInvocationThrowException', 'ExpressionThrowException')]
                [Parameter(Mandatory)]
                [string] $ErrorFromCleanBlock
            )

            End {
                switch ($ErrorFromEndBlock) {
                    'ThrowTerminatingError' {
                        $ex = [System.ArgumentException]::new('end-terminating-exception')
                        $er = [System.Management.Automation.ErrorRecord]::new($ex, 'ThrowTerminatingError:end-error', 'InvalidArgument', $null)
                        $PSCmdlet.ThrowTerminatingError($er)
                        Write-Verbose -Verbose "end-verbose-message"
                    }

                    'ErrorActionStop' {
                        Get-Command NonExistEnd -ErrorAction Stop
                        Write-Verbose -Verbose "end-verbose-message"
                    }

                    'ThrowException' {
                        throw 'end-throw-exception'
                        Write-Verbose -Verbose "end-verbose-message"
                    }

                    'WriteErrorAPI' {
                        $ex = [System.ArgumentException]::new('end-arg-exception')
                        $er = [System.Management.Automation.ErrorRecord]::new($ex, 'WriteErrorAPI:end-error', 'InvalidArgument', $null)
                        $PSCmdlet.WriteError($er)
                        Write-Verbose -Verbose "end-verbose-message"
                    }

                    'WriteErrorCmdlet' {
                        Write-Error 'end-write-error-cmdlet'
                        Write-Verbose -Verbose "end-verbose-message"
                    }

                    'MethodInvocationThrowException' {
                        ## This method call throws exception.
                        $iss = [initialsessionstate]::Create()
                        $iss.ImportPSModule($null)
                        Write-Verbose -Verbose "end-verbose-message"
                    }

                    'ExpressionThrowException' {
                        1/0 ## throw exception.
                        Write-Verbose -Verbose "end-verbose-message"
                    }
                }
            }

            clean {
                switch ($ErrorFromCleanBlock) {
                    'ThrowTerminatingError' {
                        $ex = [System.ArgumentException]::new('clean-terminating-exception')
                        $er = [System.Management.Automation.ErrorRecord]::new($ex, 'ThrowTerminatingError:clean-error', 'InvalidArgument', $null)
                        $PSCmdlet.ThrowTerminatingError($er)
                        Write-Verbose -Verbose "clean-verbose-message"
                    }

                    'ErrorActionStop' {
                        Get-Command NonExistClean -ErrorAction Stop
                        Write-Verbose -Verbose "clean-verbose-message"
                    }

                    'ThrowException' {
                        throw 'clean-throw-exception'
                        Write-Verbose -Verbose "clean-verbose-message"
                    }

                    'WriteErrorAPI' {
                        $ex = [System.ArgumentException]::new('clean-arg-exception')
                        $er = [System.Management.Automation.ErrorRecord]::new($ex, 'WriteErrorAPI:clean-error', 'InvalidArgument', $null)
                        $PSCmdlet.WriteError($er)
                        Write-Verbose -Verbose "clean-verbose-message"
                    }

                    'WriteErrorCmdlet' {
                        Write-Error 'clean-write-error-cmdlet'
                        Write-Verbose -Verbose "clean-verbose-message"
                    }

                    'MethodInvocationThrowException' {
                        ## This method call throws exception.
                        $iss = [initialsessionstate]::Create()
                        $iss.ImportPSModule($null)
                        Write-Verbose -Verbose "clean-verbose-message"
                    }

                    'ExpressionThrowException' {
                        1/0 ## throw exception.
                        Write-Verbose -Verbose "clean-verbose-message"
                    }
                }
            }
        }

        #region Helper

        $pwsh = [PowerShell]::Create()
        $text = (Get-Command MultipleErrors).ScriptBlock.Ast.Extent.Text
        $pwsh.AddScript($text).Invoke()

        function RunCommand {
            param(
                [ValidateSet('MultipleErrors')]
                [string] $Command,

                [ValidateSet('ThrowTerminatingError', 'ErrorActionStop', 'ThrowException', 'WriteErrorAPI', 
                             'WriteErrorCmdlet', 'MethodInvocationThrowException', 'ExpressionThrowException')]
                [string] $ErrorFromEndBlock,

                [ValidateSet('ThrowTerminatingError', 'ErrorActionStop', 'ThrowException', 'WriteErrorAPI', 
                             'WriteErrorCmdlet', 'MethodInvocationThrowException', 'ExpressionThrowException')]
                [string] $ErrorFromCleanBlock,

                [ValidateSet('Continue', 'Ignore', 'SilentlyContinue', 'Stop')]
                [string] $ErrorAction
            )

            $pwsh.Commands.Clear()
            $pwsh.Streams.ClearStreams()
            $pwsh.AddCommand($Command) > $null
            $pwsh.AddParameter('ErrorFromEndBlock', $ErrorFromEndBlock) > $null
            $pwsh.AddParameter('ErrorFromCleanBlock', $ErrorFromCleanBlock) > $null
            if ($ErrorAction) { $pwsh.AddParameter('ErrorAction', $ErrorAction) > $null }
            $pwsh.Invoke()
        }

        function RunScript {
            param([string] $Script)

            $pwsh.Commands.Clear()
            $pwsh.Streams.ClearStreams()
            $pwsh.AddScript($Script).Invoke()
        }

        function GetLastError {
            $pwsh.Commands.Clear()
            $pwsh.AddCommand('Get-Error').Invoke()
        }

        function GetAllErrors {
            $pwsh.Commands.Clear()
            $pwsh.AddScript('$Error').Invoke()
        }

        function ClearDollarError {
            $pwsh.Commands.Clear()
            $pwsh.AddScript('$Error.Clear()').Invoke()
        }

        #endregion
    }

    AfterAll {
        $pwsh.Dispose()
    }

    It "Terminating errors from both 'End' (ThrowTerminatingError) and 'Clean' (ThrowTerminatingError) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowTerminatingError' -ErrorFromCleanBlock 'ThrowTerminatingError'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException | Should -BeOfType 'System.Management.Automation.CmdletInvocationException'
        $failure.Exception.InnerException.InnerException | Should -BeOfType 'System.ArgumentException'
        $failure.Exception.InnerException.InnerException.Message | Should -BeExactly 'end-terminating-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception | Should -BeOfType 'System.ArgumentException'
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'clean-terminating-exception'
    }

    It "Terminating errors from both 'End' (ErrorActionStop) and 'Clean' (ThrowTerminatingError) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ErrorActionStop' -ErrorFromCleanBlock 'ThrowTerminatingError'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException | Should -BeOfType 'System.Management.Automation.ActionPreferenceStopException'
        $failure.Exception.InnerException.Message | should -BeLike "*'NonExistEnd'*"

        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception | Should -BeOfType 'System.ArgumentException'
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'clean-terminating-exception'
    }

    It "Terminating errors from both 'End' (ThrowException) and 'Clean' (ThrowTerminatingError) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowException' -ErrorFromCleanBlock 'ThrowTerminatingError'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException.Message | should -BeExactly 'end-throw-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception | Should -BeOfType 'System.ArgumentException'
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'clean-terminating-exception'
    }

    It "Terminating errors from both 'End' (ThrowTerminatingError) and 'Clean' (ErrorActionStop) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowTerminatingError' -ErrorFromCleanBlock 'ErrorActionStop'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException | Should -BeOfType 'System.Management.Automation.CmdletInvocationException'
        $failure.Exception.InnerException.InnerException | Should -BeOfType 'System.ArgumentException'
        $failure.Exception.InnerException.InnerException.Message | Should -BeExactly 'end-terminating-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].FullyQualifiedErrorId | Should -BeExactly 'CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand'
        $pwsh.Streams.Error[0].Exception.Message | Should -BeLike "*'NonExistClean'*"
    }

    It "Terminating errors from both 'End' (ErrorActionStop) and 'Clean' (ErrorActionStop) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ErrorActionStop' -ErrorFromCleanBlock 'ErrorActionStop'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException | Should -BeOfType 'System.Management.Automation.ActionPreferenceStopException'
        $failure.Exception.InnerException.Message | should -BeLike "*'NonExistEnd'*"

        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].FullyQualifiedErrorId | Should -BeExactly 'CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand'
        $pwsh.Streams.Error[0].Exception.Message | Should -BeLike "*'NonExistClean'*"
    }

    It "Terminating errors from both 'End' (ThrowException) and 'Clean' (ErrorActionStop) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowException' -ErrorFromCleanBlock 'ErrorActionStop'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException.Message | should -BeExactly 'end-throw-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].FullyQualifiedErrorId | Should -BeExactly 'CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand'
        $pwsh.Streams.Error[0].Exception.Message | Should -BeLike "*'NonExistClean'*"
    }

    It "Terminating errors from both 'End' (ThrowTerminatingError) and 'Clean' (ThrowException) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowTerminatingError' -ErrorFromCleanBlock 'ThrowException'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException | Should -BeOfType 'System.Management.Automation.CmdletInvocationException'
        $failure.Exception.InnerException.InnerException | Should -BeOfType 'System.ArgumentException'
        $failure.Exception.InnerException.InnerException.Message | Should -BeExactly 'end-terminating-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'clean-throw-exception'
    }

    It "Terminating errors from both 'End' (ErrorActionStop) and 'Clean' (ThrowException) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ErrorActionStop' -ErrorFromCleanBlock 'ThrowException'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException | Should -BeOfType 'System.Management.Automation.ActionPreferenceStopException'
        $failure.Exception.InnerException.Message | should -BeLike "*'NonExistEnd'*"

        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'clean-throw-exception'
    }

    It "Terminating errors from both 'End' (ThrowException) and 'Clean' (ThrowException) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowException' -ErrorFromCleanBlock 'ThrowException'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException.Message | should -BeExactly 'end-throw-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'clean-throw-exception'
    }

    It "Terminating errors from both 'End' (ThrowException) and 'Clean' (ThrowException) with ErrorAction '<ErrorAction>' should work properly" -TestCases @(
        @{ ErrorAction = 'Ignore' }
        @{ ErrorAction = 'SilentlyContinue' }
    ) {
        param($ErrorAction)

        ClearDollarError

        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowException' -ErrorFromCleanBlock 'ThrowException' -ErrorAction $ErrorAction

        $pwsh.Streams.Error.Count | Should -Be 0
        $pwsh.Streams.Verbose.Count | Should -Be 2
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
        $pwsh.Streams.Verbose[1] | Should -BeExactly 'clean-verbose-message'

        $ers = GetAllErrors
        $ers.Count | Should -Be 2
        $ers[0].Exception.Message | Should -BeExactly 'clean-throw-exception'
        $ers[1].Exception.Message | Should -BeExactly 'end-throw-exception'
    }

    It "Non-terminating error from 'End' (WriteErrorAPI) and terminating error from 'Clean' (ThrowTerminatingError) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'WriteErrorAPI' -ErrorFromCleanBlock 'ThrowTerminatingError'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'end-arg-exception'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-terminating-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (WriteErrorAPI) and terminating error from 'Clean' (ErrorActionStop) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'WriteErrorAPI' -ErrorFromCleanBlock 'ErrorActionStop'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'end-arg-exception'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeLike "*'NonExistClean'*"

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (WriteErrorAPI) and terminating error from 'Clean' (ThrowException) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'WriteErrorAPI' -ErrorFromCleanBlock 'ThrowException'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'end-arg-exception'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-throw-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (WriteErrorCmdlet) and terminating error from 'Clean' (ThrowTerminatingError) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'WriteErrorCmdlet' -ErrorFromCleanBlock 'ThrowTerminatingError'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'end-write-error-cmdlet'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-terminating-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (WriteErrorCmdlet) and terminating error from 'Clean' (ErrorActionStop) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'WriteErrorCmdlet' -ErrorFromCleanBlock 'ErrorActionStop'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'end-write-error-cmdlet'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeLike "*'NonExistClean'*"

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (WriteErrorCmdlet) and terminating error from 'Clean' (ThrowException) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'WriteErrorCmdlet' -ErrorFromCleanBlock 'ThrowException'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'end-write-error-cmdlet'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-throw-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (MethodInvocationThrowException) and terminating error from 'Clean' (ThrowTerminatingError) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'MethodInvocationThrowException' -ErrorFromCleanBlock 'ThrowTerminatingError'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-terminating-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (MethodInvocationThrowException) and terminating error from 'Clean' (ErrorActionStop) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'MethodInvocationThrowException' -ErrorFromCleanBlock 'ErrorActionStop'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeLike "*'NonExistClean'*"

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (MethodInvocationThrowException) and terminating error from 'Clean' (ThrowException) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'MethodInvocationThrowException' -ErrorFromCleanBlock 'ThrowException'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-throw-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (ExpressionThrowException) and terminating error from 'Clean' (ThrowTerminatingError) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ExpressionThrowException' -ErrorFromCleanBlock 'ThrowTerminatingError'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.DivideByZeroException'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-terminating-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (ExpressionThrowException) and terminating error from 'Clean' (ErrorActionStop) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ExpressionThrowException' -ErrorFromCleanBlock 'ErrorActionStop'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.DivideByZeroException'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeLike "*'NonExistClean'*"

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Non-terminating error from 'End' (ExpressionThrowException) and terminating error from 'Clean' (ThrowException) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ExpressionThrowException' -ErrorFromCleanBlock 'ThrowException'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.DivideByZeroException'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-throw-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'end-verbose-message'
    }

    It "Terminating error from 'End' (ThrowException) and non-terminating error from 'Clean' (WriteErrorAPI) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowException' -ErrorFromCleanBlock 'WriteErrorAPI'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException.Message | should -BeExactly 'end-throw-exception'

        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'clean-arg-exception'
        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'clean-verbose-message'
    }

    It "Terminating error from 'End' (ThrowException) and non-terminating error from 'Clean' (WriteErrorCmdlet) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowException' -ErrorFromCleanBlock 'WriteErrorCmdlet'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException.Message | should -BeExactly 'end-throw-exception'

        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'clean-write-error-cmdlet'
        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'clean-verbose-message'
    }

    It "Terminating error from 'End' (ThrowException) and non-terminating error from 'Clean' (MethodInvocationThrowException) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowException' -ErrorFromCleanBlock 'MethodInvocationThrowException'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException.Message | should -BeExactly 'end-throw-exception'

        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'
        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'clean-verbose-message'
    }

    It "Terminating error from 'End' (ThrowException) and non-terminating error from 'Clean' (ExpressionThrowException) should work properly" {
        $failure = $null
        try {
            RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'ThrowException' -ErrorFromCleanBlock 'ExpressionThrowException'
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException.Message | should -BeExactly 'end-throw-exception'

        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.DivideByZeroException'
        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Verbose[0] | Should -BeExactly 'clean-verbose-message'
    }

    It "Non-terminating error from 'End' (WriteErrorAPI) and non-terminating error from 'Clean' (WriteErrorCmdlet) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'WriteErrorAPI' -ErrorFromCleanBlock 'WriteErrorCmdlet'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'end-arg-exception'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-write-error-cmdlet'

        $pwsh.Streams.Verbose.Count | Should -Be 2
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'end-verbose-message'
        $pwsh.Streams.Verbose[1].Message | Should -BeExactly 'clean-verbose-message'
    }

    It "Non-terminating error from 'End' (WriteErrorAPI) and non-terminating error from 'Clean' (MethodInvocationThrowException) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'WriteErrorAPI' -ErrorFromCleanBlock 'MethodInvocationThrowException'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'end-arg-exception'
        $pwsh.Streams.Error[1].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'

        $pwsh.Streams.Verbose.Count | Should -Be 2
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'end-verbose-message'
        $pwsh.Streams.Verbose[1].Message | Should -BeExactly 'clean-verbose-message'
    }

    It "Non-terminating error from 'End' (WriteErrorCmdlet) and non-terminating error from 'Clean' (WriteErrorAPI) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'WriteErrorCmdlet' -ErrorFromCleanBlock 'WriteErrorAPI'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'end-write-error-cmdlet'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-arg-exception'

        $pwsh.Streams.Verbose.Count | Should -Be 2
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'end-verbose-message'
        $pwsh.Streams.Verbose[1].Message | Should -BeExactly 'clean-verbose-message'
    }

    It "Non-terminating error from 'End' (WriteErrorCmdlet) and non-terminating error from 'Clean' (ExpressionThrowException) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'WriteErrorCmdlet' -ErrorFromCleanBlock 'ExpressionThrowException'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'end-write-error-cmdlet'
        $pwsh.Streams.Error[1].Exception.InnerException | Should -BeOfType 'System.DivideByZeroException'

        $pwsh.Streams.Verbose.Count | Should -Be 2
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'end-verbose-message'
        $pwsh.Streams.Verbose[1].Message | Should -BeExactly 'clean-verbose-message'
    }

    It "Non-terminating error from 'End' (MethodInvocationThrowException) and non-terminating error from 'Clean' (WriteErrorCmdlet) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'MethodInvocationThrowException' -ErrorFromCleanBlock 'WriteErrorCmdlet'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'clean-write-error-cmdlet'

        $pwsh.Streams.Verbose.Count | Should -Be 2
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'end-verbose-message'
        $pwsh.Streams.Verbose[1].Message | Should -BeExactly 'clean-verbose-message'
    }

    It "Non-terminating error from 'End' (MethodInvocationThrowException) and non-terminating error from 'Clean' (ExpressionThrowException) should work properly" {
        ## No exception should be thrown
        RunCommand -Command 'MultipleErrors' -ErrorFromEndBlock 'MethodInvocationThrowException' -ErrorFromCleanBlock 'ExpressionThrowException'

        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.InnerException | Should -BeOfType 'System.ArgumentNullException'
        $pwsh.Streams.Error[1].Exception.InnerException | Should -BeOfType 'System.DivideByZeroException'

        $pwsh.Streams.Verbose.Count | Should -Be 2
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'end-verbose-message'
        $pwsh.Streams.Verbose[1].Message | Should -BeExactly 'clean-verbose-message'
    }
}

Describe "Error handling within a pipeline (multiple commands with 'Clean' block)" -Tag 'CI' {
    BeforeAll {
        function test1 {
            param([switch] $EmitErrorInProcess)
            process {
                if ($EmitErrorInProcess) {
                    throw 'test1-process'
                }
                Write-Output 'process-obj'
            }
            clean {
                throw 'test1-clean'
                Write-Verbose -Verbose 'test1-clean-verbose'
            }
        }

        function test2 {
            param([switch] $EmitErrorInProcess)
            process {
                if ($EmitErrorInProcess) {
                    throw 'test2-process'
                }
                Write-Verbose -Verbose $_
            }
            clean {
                throw 'test2-clean'
                Write-Verbose -Verbose 'test2-clean-verbose'
            }
        }

        function test-1 {
            param([switch] $EmitException)
            process { 'output' }
            clean {
                if ($EmitException) {
                    throw 'test-1-clean-exception'
                }
                Write-Verbose -Verbose 'test-1-clean'
            }
        }

        function test-2 {
            param([switch] $EmitException)
            process { $_ }
            clean {
                if ($EmitException) {
                    throw 'test-2-clean-exception'
                }
                Write-Verbose -Verbose 'test-2-clean'
            }
        }

        function test-3 {
            param([switch] $EmitException)
            process { Write-Warning $_ }
            clean {
                if ($EmitException) {
                    throw 'test-3-clean-exception'
                }
                Write-Verbose -Verbose 'test-3-clean'
            }
        }

        #region Helper

        $pwsh = [PowerShell]::Create()
        $text = (Get-Command test1, test2, test-1, test-2, test-3).ScriptBlock.Ast.Extent.Text
        $pwsh.AddScript($text -join "`n").Invoke()

        function RunScript {
            param([string] $Script)

            $pwsh.Commands.Clear()
            $pwsh.Streams.ClearStreams()
            $pwsh.AddScript($Script).Invoke()
        }

        #endregion
    }

    It "Errors from multiple 'Clean' blocks should work properly" {
        ## No exception should be thrown
        RunScript -Script "test1 | test2"

        ## Exceptions thrown from 'throw' statement are not propagated up, but instead written to the error stream.
        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'test1-clean'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'test2-clean'
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'process-obj'
    }

    It "ErrorAction should be honored by 'Clean' blocks" {
        try {
            RunScript -Script '$ErrorActionPreference = "SilentlyContinue"'
            ## No exception should be thrown.
            RunScript -Script "test1 | test2"

            ## The exception from 'throw' statement should be suppressed by 'SilentlyContinue'.
            $pwsh.Streams.Error.Count | Should -Be 0
            $pwsh.Streams.Verbose.Count | Should -Be 3
            $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'process-obj'
            $pwsh.Streams.Verbose[1].Message | Should -BeExactly 'test1-clean-verbose'
            $pwsh.Streams.Verbose[2].Message | Should -BeExactly 'test2-clean-verbose'
        }
        finally {
            ## Revert error action back to 'Continue'
            RunScript -Script '$ErrorActionPreference = "Continue"'
        }
    }

    It "Errors from 'Clean' blocks should work properly when another named block emits error" {
        $failure = $null
        try {
            RunScript -Script "test1 | test2 -EmitErrorInProcess"
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException.Message | Should -BeExactly 'test2-process'

        $pwsh.Streams.Verbose.Count | Should -Be 0
        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'test1-clean'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'test2-clean'
    }

    It "The 'Clean' block should not run when the none of other named blocks from the same command get to run" {
        $failure = $null
        try {
            RunScript -Script "test1 -EmitErrorInProcess | test2"
        } catch {
            $failure = $_
        }

        $failure | Should -Not -BeNullOrEmpty
        $failure.Exception | Should -BeOfType 'System.Management.Automation.MethodInvocationException'
        $failure.Exception.InnerException.Message | Should -BeExactly 'test1-process'

        ## Only the 'Clean' block from 'test1' will run.
        ## The 'Clean' block from 'test2' won't run because none of the other blocks from 'test2' gets to run
        ## due to the terminating exception thrown from 'test1.Process'.
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'test1-clean'
    }

    It "Exception from the 'Clean' block at <Position> should not affect other 'Clean' blocks" -TestCases @(
        @{ Position = 'beginning-of-pipeline'; Script = 'test-1 -EmitException | test-2 | test-3'; ExceptionMessage = 'test-1-clean-exception'; VerboseMessages = @('test-2-clean', 'test-3-clean') }
        @{ Position = 'middle-of-pipeline';    Script = 'test-1 | test-2 -EmitException | test-3'; ExceptionMessage = 'test-2-clean-exception'; VerboseMessages = @('test-1-clean', 'test-3-clean') }
        @{ Position = 'end-of-pipeline';       Script = 'test-1 | test-2 | test-3 -EmitException'; ExceptionMessage = 'test-3-clean-exception'; VerboseMessages = @('test-1-clean', 'test-2-clean') }
    ) {
        param($Script, $ExceptionMessage, $VerboseMessages)

        RunScript -Script $Script
        $pwsh.Streams.Error.Count | Should -Be 1
        $pwsh.Streams.Verbose.Count | Should -Be 2
        $pwsh.Streams.Warning.Count | Should -Be 1

        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly $ExceptionMessage
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly $VerboseMessages[0]
        $pwsh.Streams.Verbose[1].Message | Should -BeExactly $VerboseMessages[1]
        $pwsh.Streams.Warning[0].Message | Should -BeExactly 'output'
    }

    It "Multiple exceptions from 'Clean' blocks should not affect other 'Clean' blocks" {
        RunScript -Script 'test-1 -EmitException | test-2 | test-3 -EmitException'
        $pwsh.Streams.Error.Count | Should -Be 2
        $pwsh.Streams.Verbose.Count | Should -Be 1
        $pwsh.Streams.Warning.Count | Should -Be 1

        $pwsh.Streams.Error[0].Exception.Message | Should -BeExactly 'test-1-clean-exception'
        $pwsh.Streams.Error[1].Exception.Message | Should -BeExactly 'test-3-clean-exception'
        $pwsh.Streams.Verbose[0].Message | Should -BeExactly 'test-2-clean'
        $pwsh.Streams.Warning[0].Message | Should -BeExactly 'output'
    }
}
